Developing Plug-in Callouts
๐ฏ Goalโ
This tutorial shows you how to develop callouts in your own plug-in.
What is a Callout?โ
A callout is a piece of code that executes automatically when a user changes the value of a field in a window. Callouts are commonly used to:
- Validate input data before it's saved
- Auto-fill related fields based on the changed value
- Perform calculations (e.g., calculate totals, discounts, or taxes)
- Show warnings or messages to guide the user
- Dynamically enable/disable fields based on conditions
- Load data from related records (e.g., populate product details when selecting a product)
For example, when a user selects a Business Partner in a Sales Order, a callout can automatically fill in the payment terms, price list, and shipping address.
Callouts only work when data is entered through:
- The iDempiere UI
- CSV Importer
Callouts do NOT execute when records are created or modified via:
- REST API calls
- Background processes
- Direct database operations
- Java code using PO.save()
If you need validation or logic that works across all entry points, use an Event Handler instead, which intercepts all record changes regardless of the source.
Implementation Approachesโ
This guide covers the following methods for implementing callouts:
Mapped Column Callout Factory (iDempiere 9+) - Modern approach with three registration methods:
- Direct registration via Bundle Activator
- OSGi Component with service binding
- Factory subclass approach
Annotation-Based Callouts (iDempiere 9+) - (Recommended) Use
@Calloutannotations to declare callouts declarativelyLegacy CalloutFactory (Pre-iDempiere 9) - Traditional approach using
IColumnCalloutFactoryinterface
For iDempiere 9+, use annotation-based callouts for the cleanest and most maintainable code.
For earlier versions, use the CalloutFactory approach.
โ Prerequisitesโ
Before starting, review:
You should already know how to create a plug-in, as this guide will not cover that in detail.
๐ Implementation Methodsโ
Modern Approach (iDempiere 9+): Mapped Column Callout Factoryโ
Starting with iDempiere 9, a new column callout factory base class was introduced that's backed by Map and Lambda functional objects. This approach supports:
- Direct registration via Bundle Activator
- OSGi Component with service binding
- Annotation-based callout registration (recommended)
The annotation-based approach is the simplest and most maintainable method for iDempiere 9+.
Method 1: Register at Plugin Activator Startโ
Register your callout directly in the plugin's activator start() method:
public class MyTestAmountCallout implements IColumnCallout {
@Override
public String start(Properties ctx, int WindowNo, GridTab mTab,
GridField mField, Object value, Object oldValue) {
// Your callout logic here
return null;
}
}
public void start(BundleContext context) throws Exception {
var factory = Core.getMappedColumnCalloutFactory();
factory.addMapping(MTest.Table_Name, MTest.COLUMNNAME_T_Amount,
() -> new MyTestAmountCallout());
}
Core.getMappedColumnCalloutFactory() can return null if your bundle activates before the IMappedColumnCalloutFactory service. Use Method 2 or 3 for safer registration.
Method 2: Bind via OSGi Component Serviceโ
Create an OSGi component and bind to the IMappedColumnCalloutFactory service:
@Component(immediate = true)
public class MyCalloutComponent {
@Reference
public void bindService(IMappedColumnCalloutFactory factory) {
factory.addMapping(MTest.Table_Name, MTest.COLUMNNAME_T_Amount,
() -> new MyTestAmountCallout());
}
}
Method 3: Extend MappedColumnCalloutFactoryโ
Create a subclass of MappedColumnCalloutFactory and register it as an IColumnCalloutFactory service:
@Component(service = IColumnCalloutFactory.class)
public class MyFactory extends MappedColumnCalloutFactory {
public MyFactory() {
addMapping(MTest.Table_Name, MTest.COLUMNNAME_T_Amount,
() -> new MyTestAmountCallout());
}
}
Do NOT register as IMappedColumnCalloutFactory service when extending MappedColumnCalloutFactory.
Annotation-Based Callouts (Recommended for iDempiere 9+)โ
The annotation approach allows you to declare callouts directly on your callout classes using the @Callout annotation.
Step 1: Annotate Your Callout Classโ
Use the @Callout annotation with tableName and columnName parameters. You can use * to match any table or column name.
Single Column Example:
import org.adempiere.base.annotation.Callout;
import org.adempiere.base.IColumnCallout;
@Callout(tableName = "AD_InfoWindow", columnName = "AD_Table_ID")
public class CalloutInfoWindow implements IColumnCallout {
@Override
public String start(Properties ctx, int WindowNo, GridTab mTab,
GridField mField, Object value, Object oldValue) {
// Your callout logic
return null;
}
}
Multiple Columns Example:
@Callout(tableName = "AD_InfoColumn",
columnName = {"AD_Element_ID", "AD_Reference_ID"})
public class CalloutInfoColumn implements IColumnCallout {
@Override
public String start(Properties ctx, int WindowNo, GridTab mTab,
GridField mField, Object value, Object oldValue) {
// Your callout logic
return null;
}
}
Step 2: Register Annotated Calloutsโ
You have two options for scanning and registering annotated callout classes:
Option A: Create an OSGi Component (Recommended)
Extend AnnotationBasedColumnCalloutFactory and register it as an IColumnCalloutFactory service:
@Component(service = IColumnCalloutFactory.class)
public class MyAnnotationCalloutFactory extends AnnotationBasedColumnCalloutFactory {
// The base class automatically scans for @Callout annotations
}
Option B: Scan in Activator with Safe Service Binding
Use @Component, @Reference, and @Activate to ensure safe registration:
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
@Component(immediate = true)
public class MyActivator implements BundleActivator {
@Reference(service = IMappedColumnCalloutFactory.class,
cardinality = ReferenceCardinality.MANDATORY)
private IMappedColumnCalloutFactory mappedCalloutFactory;
public MyActivator() {
}
@Override
public void start(BundleContext context) throws Exception {
}
@Override
public void stop(BundleContext context) throws Exception {
}
// activate() is called only after mappedCalloutFactory reference is injected
@Activate
public void activate(BundleContext context) {
// Replace with your package name containing annotated callout classes
mappedCalloutFactory.scan(context, "org.mycompany.callout");
}
}
The @Activate approach ensures that your callouts are only registered after the IMappedColumnCalloutFactory service is available, avoiding null pointer exceptions.
Example Project:
For a complete working example of annotation-based callouts, see:
idempiere-examples: Callout Annotation Example
Technical References:
Using a CalloutFactory (iDempiere < 9)โ
This approach is recommended for versions before iDempiere 9.
Step 1: Create the Callout Classโ
Create a class that implements the IColumnCallout interface.
Example: CalloutFromFactory.java
package com.example.callout;
import org.adempiere.base.IColumnCallout;
import org.compiere.model.GridField;
import org.compiere.model.GridTab;
import java.util.Properties;
public class CalloutFromFactory implements IColumnCallout {
@Override
public String start(Properties ctx, int WindowNo, GridTab mTab,
GridField mField, Object value, Object oldValue) {
// Your callout logic here
System.out.println("Callout triggered on: " + mField.getColumnName());
System.out.println("New value: " + value);
return null; // Return null if no error, or an error message string
}
}
Step 2: Create the Callout Factoryโ
Create a factory class that implements IColumnCalloutFactory.
Example: CalloutFactory.java
package com.example.callout;
import org.adempiere.base.IColumnCallout;
import org.adempiere.base.IColumnCalloutFactory;
import org.compiere.model.MProduct;
public class CalloutFactory implements IColumnCalloutFactory {
@Override
public IColumnCallout[] getColumnCallouts(String tableName, String columnName) {
// Check if this is the column we want to intercept
if (tableName.equals(MProduct.Table_Name) &&
columnName.equals(MProduct.COLUMNNAME_DocumentNote)) {
return new IColumnCallout[] { new CalloutFromFactory() };
}
return null; // No callout for other columns
}
}
Step 3: Register as an OSGi Componentโ
Option A: Using Annotations (Recommended)
Add the @Component annotation to your factory class. See Event Handling with Annotations for details.
import org.osgi.service.component.annotations.Component;
@Component(
service = IColumnCalloutFactory.class,
immediate = true
)
public class CalloutFactory implements IColumnCalloutFactory {
// ... implementation
}
Option B: Manual Component Definition
If not using annotations, create a component definition XML file manually.
- Create a directory named
OSGI-INFin your project root (this is the OSGi convention) - Create a file
OSGI-INF/calloutfactory.xml:
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
name="com.example.callout.CalloutFactory">
<implementation class="com.example.callout.CalloutFactory"/>
<service>
<provide interface="org.adempiere.base.IColumnCalloutFactory"/>
</service>
</scr:component>
- Update your
META-INF/MANIFEST.MFto include the component:
Service-Component: OSGI-INF/calloutfactory.xml
- Ensure
build.propertiesincludes the OSGI-INF directory in the binary build:
bin.includes = META-INF/,\
.,\
OSGI-INF/
๐งช Testing Your Calloutโ
- Start the iDempiere client
- Log in as GardenAdmin to GardenWorld
- Open the Product window
- Enter or modify a value in the Document Note field
- Tab out of the field to trigger the callout
- Check your console log for the output
You should see:
Callout triggered on: DocumentNote
New value: [your entered text]
๐ฅ Video Tutorialโ
Watch this video walkthrough:
https://www.youtube.com/watch?v=twC6Dy8R5g4
๐ฆ Exporting Your Plug-inโ
Before exporting your plug-in as a JAR:
- Open
build.properties - Verify that
OSGI-INF/is listed in Binary Build - Ensure your component definition XML files are included
This ensures your OSGi component definitions are packaged correctly.