Developing Plug-in Processes
Goal
This tutorial shows you how to develop processes within your own plug-in, enabling you to create custom business logic that users can execute on demand.
What is a Process?
A process in iDempiere is a unit of business logic that performs specific operations, typically triggered by users through buttons, menus, or scheduled tasks. Processes can:
- Perform batch operations (e.g., generate invoices, update prices)
- Generate reports and exports (e.g., export data to XML, create PDFs)
- Execute calculations (e.g., calculate commissions, consolidate data)
- Integrate with external systems (e.g., send data to APIs, import files)
- Automate workflows (e.g., send notifications, update records)
What You Will Learn
- Create and run a new process in your custom plug-in
- Define and read process parameters
- Log information during process execution
- Handle errors properly during and after process execution
- Interact with users during process execution
Prerequisites
Before starting, review:
You should already know how to create a plug-in, as this guide will not cover that in detail.
Creating a Process
Step 1: Create the Process Class
Create a new class that extends SvrProcess and implements the required methods:
prepare()- Reads and validates process parametersdoIt()- Contains the main business logic
Example: Simple Process with Annotation
import org.adempiere.base.annotation.Process;
import org.adempiere.base.annotation.Parameter;
import org.compiere.process.SvrProcess;
import org.compiere.model.MProduct;
import java.io.File;
import java.io.FileWriter;
@Process
public class PrintProductXML extends SvrProcess {
@Parameter
private int p_M_Product_ID;
public PrintProductXML() {
}
@Override
protected void prepare() {
// Parameters are automatically injected via @Parameter annotation
}
@Override
protected String doIt() throws Exception {
MProduct product = MProduct.get(p_M_Product_ID);
File exportFile = File.createTempFile(product.getValue(), ".xml");
FileWriter writer = new FileWriter(exportFile);
writer.write(product.get_xmlString(null).toString());
writer.close();
if (processUI != null)
processUI.download(exportFile);
return "@Ok@";
}
}
Example Project:
For a complete working example, see:
idempiere-examples: Process Annotation Example
Registering Your Process
Modern Approach (iDempiere 9+): Mapped Process Factory
Starting with iDempiere 9, a new process factory base class was introduced that's backed by Map and Lambda functional objects. This approach supports multiple registration methods.
Method 1: Register at Plugin Activator Start
public void start(BundleContext context) throws Exception {
IMappedProcessFactory mappedFactory = Core.getMappedProcessFactory();
mappedFactory.addMapping(MyTest.class.getName(), () -> new MyTest());
}
Core.getMappedProcessFactory() can return null if your bundle activates before the service. Use Method 4 for safer registration.
Method 2: Bind via OSGi Component Service
@Component(immediate = true)
public class MyProcessComponent {
@Reference
public void bindService(IMappedProcessFactory factory) {
factory.addMapping(MyTest.class.getName(), () -> new MyTest());
}
}
Method 3: Extend MappedProcessFactory
@Component(service = IProcessFactory.class)
public class MyFactory extends MappedProcessFactory {
public MyFactory() {
addMapping(MyTest.class.getName(), () -> new MyTest());
}
}
Do NOT register as IMappedProcessFactory service when extending MappedProcessFactory.
Method 4: Scan Package with Annotations (Recommended)
Use @Component, @Reference, and @Activate for 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 = IMappedProcessFactory.class,
cardinality = ReferenceCardinality.MANDATORY)
private IMappedProcessFactory mappedProcessFactory;
public MyActivator() {
}
@Override
public void start(BundleContext context) throws Exception {
}
@Override
public void stop(BundleContext context) throws Exception {
}
// activate() is called only after mappedProcessFactory reference is injected
@Activate
public void activate(BundleContext context) {
// Replace with your package name containing process classes
mappedProcessFactory.scan(context, "org.mycompany.process");
}
}
Using @Process Annotation
The @Process annotation allows you to declaratively register your process class. The annotation has an optional name parameter to provide an alternate registration name.
Example:
@Process(name = "MyCustomProcess")
public class BPartnerValidate extends SvrProcess {
@Override
protected void prepare() {
}
@Override
protected String doIt() throws Exception {
// Your logic here
return "@Success@";
}
}
To enable annotation scanning, create an OSGi component that extends AnnotationBasedProcessFactory:
@Component(
immediate = true,
service = IProcessFactory.class,
property = {"service.ranking:Integer=1"}
)
public class MyAnnotationProcessFactory extends AnnotationBasedProcessFactory {
// The base class automatically scans for @Process annotations
}
Technical References:
Legacy Approach: Using a Process Factory
For versions before iDempiere 9, use the IProcessFactory interface.
Step 1: Create the Process Factory Class
package com.example.process;
import org.adempiere.base.IProcessFactory;
import org.compiere.process.ProcessCall;
public class MyProcessFactory implements IProcessFactory {
@Override
public ProcessCall newProcessInstance(String className) {
if (className.equals("com.example.process.MyProcess")) {
return new MyProcess();
}
return null;
}
}
Step 2: Register as OSGi Component
Option A: Using Annotations
@Component(
service = IProcessFactory.class,
immediate = true,
property = {"service.ranking:Integer=100"}
)
public class MyProcessFactory implements IProcessFactory {
// ... implementation
}
Option B: Manual Component Definition
Create OSGI-INF/processfactory.xml:
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
name="com.example.process.factory">
<implementation class="com.example.process.MyProcessFactory"/>
<property name="service.ranking" type="Integer" value="100"/>
<service>
<provide interface="org.adempiere.base.IProcessFactory"/>
</service>
</scr:component>
Update META-INF/MANIFEST.MF:
Service-Component: OSGI-INF/*.xml
Reading Process Parameters
Using @Parameter Annotation (Recommended)
The simplest way to read parameters is using the @Parameter annotation:
@Process
public class MyProcess extends SvrProcess {
@Parameter
private int p_M_Product_ID;
@Parameter
private String p_Name;
@Parameter
private boolean p_IsActive;
@Override
protected void prepare() {
// Parameters are automatically injected
}
@Override
protected String doIt() throws Exception {
// Use the parameters directly
log.info("Product ID: " + p_M_Product_ID);
return "@Success@";
}
}
Manual Parameter Reading
For more control, you can manually read parameters in the prepare() method:
public class MyProcess extends SvrProcess {
private boolean p_IsPrinted = false;
private int p_M_Product_ID = 0;
@Override
protected void prepare() {
ProcessInfoParameter[] para = getParameter();
for (int i = 0; i < para.length; i++) {
String name = para[i].getParameterName();
if (para[i].getParameter() == null)
continue;
else if (name.equals("IsPrinted"))
p_IsPrinted = para[i].getParameterAsBoolean();
else if (name.equals("M_Product_ID"))
p_M_Product_ID = para[i].getParameterAsInt();
}
}
@Override
protected String doIt() throws Exception {
// Your logic using the parameters
return "@Success@";
}
}
Logging Information
Using CLogger
Every process inheriting from SvrProcess has a log instance variable:
@Override
protected String doIt() throws Exception {
log.info("Starting process...");
log.warning("This is a warning");
log.severe("This is an error");
return "@Success@";
}
These logs appear in the console and iDempiere logs (if configured).
Using addLog() for Process Audit
The addLog() methods create entries in the Process Audit window, visible to users:
@Override
protected String doIt() throws Exception {
// Simple log entry
addLog(getProcessInfo().getAD_Process_ID(),
new Timestamp(System.currentTimeMillis()),
new BigDecimal(getProcessInfo().getAD_PInstance_ID()),
"Processing started");
// Log entry with link to a record
addLog(getProcessInfo().getAD_Process_ID(),
new Timestamp(System.currentTimeMillis()),
new BigDecimal(getProcessInfo().getAD_PInstance_ID()),
"Processing product: " + product.getName(),
MProduct.Table_ID,
product.get_ID());
return "@Success@";
}
Using addBufferLog()
For simpler logging with record links:
addBufferLog(id, date, number, "Message", MProduct.Table_ID, productId);
If the table ID and record ID are valid, this creates a clickable link in the process results.
Error Handling
Returning Status Messages
The doIt() method returns a String that's shown to the user:
@Override
protected String doIt() throws Exception {
if (success) {
return "@Success@"; // Uses message from AD_Message
} else {
return "Process completed with warnings";
}
}
Throwing Exceptions
To stop the process and show an error:
@Override
protected String doIt() throws Exception {
if (p_M_Product_ID <= 0) {
throw new AdempiereException("@Invalid@ @M_Product_ID@");
}
// Your logic here
return "@Success@";
}
In the Process Audit window:
- Result = 0 indicates an exception occurred
- Result = 1 indicates successful completion with your return message
Interacting with Users
Asking Yes/No Questions
You can ask users for input during process execution:
@Override
protected String doIt() throws Exception {
processUI.ask("Do you want to continue?", new Callback<Boolean>() {
public void onCallback(Boolean result) {
addLog("You selected " + (result ? "Yes" : "No"));
}
});
return null;
}
Asking for String Input
Request text input from the user:
processUI.askForInput("Please enter a value:", new Callback<String>() {
@Override
public void onCallback(String result) {
addLog("You entered: " + result);
}
});
Blocking Until User Response
Since processes continue running before receiving the answer, you may need to block execution:
final StringBuffer answer = new StringBuffer();
final StringBuffer retVal = new StringBuffer();
processUI.askForInput("Please enter some String", new Callback<String>() {
@Override
public void onCallback(String result) {
addLog("You entered: " + result);
retVal.append(result);
answer.append("done");
}
});
// Wait for user response
while (answer.length() == 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// Handle interruption
}
}
// Now you can use the value in retVal
String userInput = retVal.toString();
We use two separate StringBuffer objects because the user input could be empty. Using only one buffer would cause the process to wait forever if the user provides no input.
Asking for Secret Input (Passwords, PINs)
For sensitive information like passwords or TANs, use askForSecretInput() which obfuscates the characters as the user types:
final StringBuffer answer = new StringBuffer();
final StringBuffer retVal = new StringBuffer();
processUI.askForSecretInput("Please enter your password:", new Callback<String>() {
@Override
public void onCallback(String result) {
addLog("Password received");
retVal.append(result);
answer.append("done");
}
});
// Wait for user response
while (answer.length() == 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// Handle interruption
}
}
// Now you can use the password in retVal
String password = retVal.toString();
// Use the password for authentication, etc.
The askForSecretInput() method works exactly like askForInput(), but displays asterisks or dots instead of the actual characters being typed, providing better security for sensitive data.
Technical Reference: IDEMPIERE-2205
Testing Your Process
- Deploy your plug-in to iDempiere
- Create a new process record in the Process window (
AD_Process) - Set the Classname to your process's fully qualified class name
- Add parameters in the Process Parameter tab if needed
- Run the process from a window, button, or menu
- Check the Process Audit window for results and logs
Exporting Your Plug-in
Before exporting:
- Verify
OSGI-INF/is inbuild.propertiesunder Binary Build - Ensure all component definition XML files are included
- Test in a clean iDempiere instance
Video Tutorial
Watch this video walkthrough:
https://www.youtube.com/watch?v=5SDqnbYrpQc