Skip to main content

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 parameters
  • doIt() - 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());
}
danger

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());
}
}
Important

Do NOT register as IMappedProcessFactory service when extending MappedProcessFactory.

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โ€‹

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();
Why Two StringBuffers?

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.
info

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โ€‹

  1. Deploy your plug-in to iDempiere
  2. Create a new process record in the Process window (AD_Process)
  3. Set the Classname to your process's fully qualified class name
  4. Add parameters in the Process Parameter tab if needed
  5. Run the process from a window, button, or menu
  6. Check the Process Audit window for results and logs

๐Ÿ“ฆ Exporting Your Plug-inโ€‹

Before exporting:

  1. Verify OSGI-INF/ is in build.properties under Binary Build
  2. Ensure all component definition XML files are included
  3. Test in a clean iDempiere instance

๐ŸŽฅ Video Tutorialโ€‹

Watch this video walkthrough:
https://www.youtube.com/watch?v=5SDqnbYrpQc