Custom Processes and Forms

Level: Intermediate Module: Plugin Development 17 min read Lesson 23 of 47

Overview

  • What you’ll learn:
    • How to create custom server processes using SvrProcess, including the lifecycle (prepare/doIt), parameters, logging, and return messages
    • How to register processes in the Application Dictionary and make them accessible from the menu or toolbar
    • How to build custom interactive forms using IFormController with ZK components, layout, and lifecycle management
  • Prerequisites: Lessons 1–22 (especially Lessons 19 and 21: Extension Points and Creating Your First Plugin)
  • Estimated reading time: 26 minutes

Introduction

Event handlers react to data changes initiated by users or other processes. But what about operations that users initiate deliberately — generating a batch of invoices, recalculating prices across a product catalog, importing data from an external file, or running a custom report? These are the domain of processes. And what about specialized user interfaces that do not fit the standard window/tab/field paradigm — a visual configuration tool, a drag-and-drop scheduler, or a multi-step wizard? These are the domain of forms.

Processes and forms are the two remaining major extension points in iDempiere’s plugin architecture. This lesson covers both in depth, from the Java implementation to the Application Dictionary registration to the deployment workflow.

Custom Processes: SvrProcess

A process in iDempiere is a server-side operation that can be triggered from the menu, from a toolbar button, or programmatically from other code. Every custom process extends the org.compiere.process.SvrProcess base class.

The SvrProcess Lifecycle

When a process is executed, iDempiere calls two methods in sequence:

  1. prepare() — Called first. This is where you read and store the process parameters that the user provided. You should not perform any business logic here — just parameter parsing.
  2. doIt() — Called after prepare() completes. This is where you implement the actual business logic. The return value is a message string displayed to the user when the process finishes.
package com.example.myplugin.process;

import org.compiere.process.SvrProcess;

public class MyCustomProcess extends SvrProcess {

    @Override
    protected void prepare() {
        // Read parameters here
    }

    @Override
    protected String doIt() throws Exception {
        // Business logic here
        return "Process completed successfully";
    }
}

Process Parameters

Processes can accept parameters that the user fills in before execution. These parameters are defined in the Application Dictionary (AD_Process_Para) and passed to your process at runtime. In the prepare() method, you read them using getParameter():

package com.example.myplugin.process;

import java.math.BigDecimal;
import java.sql.Timestamp;
import org.compiere.process.ProcessInfoParameter;
import org.compiere.process.SvrProcess;

public class GenerateInvoicesProcess extends SvrProcess {

    private int p_C_BPartner_ID = 0;
    private Timestamp p_DateFrom = null;
    private Timestamp p_DateTo = null;
    private BigDecimal p_MinAmount = BigDecimal.ZERO;
    private boolean p_IsTestRun = false;

    @Override
    protected void prepare() {
        // Iterate through all parameters
        ProcessInfoParameter[] params = getParameter();
        for (ProcessInfoParameter param : params) {
            String name = param.getParameterName();

            if ("C_BPartner_ID".equals(name)) {
                p_C_BPartner_ID = param.getParameterAsInt();
            } else if ("DateFrom".equals(name)) {
                p_DateFrom = param.getParameterAsTimestamp();
            } else if ("DateTo".equals(name)) {
                p_DateTo = param.getParameterAsTimestamp();
            } else if ("MinAmount".equals(name)) {
                p_MinAmount = param.getParameterAsBigDecimal();
            } else if ("IsTestRun".equals(name)) {
                p_IsTestRun = param.getParameterAsBoolean();
            }
        }

        // You can also access the record ID if the process was
        // triggered from a specific record's toolbar
        int recordId = getRecord_ID();
    }

    @Override
    protected String doIt() throws Exception {
        // Use the parsed parameters
        // (implementation continues below)
        return "";
    }
}

The addLog() Method: User Feedback

Long-running processes should provide feedback to the user about their progress and results. The addLog() method writes messages to the process log, which the user can view during and after execution:

@Override
protected String doIt() throws Exception {
    int invoicesGenerated = 0;
    int errors = 0;

    // Query for uninvoiced orders
    String sql = "SELECT C_Order_ID FROM C_Order "
        + "WHERE IsSOTrx='Y' AND DocStatus='CO' "
        + "AND IsInvoiced='N'";

    if (p_C_BPartner_ID > 0) {
        sql += " AND C_BPartner_ID=" + p_C_BPartner_ID;
    }
    if (p_DateFrom != null) {
        sql += " AND DateOrdered >= " + DB.TO_DATE(p_DateFrom);
    }
    if (p_DateTo != null) {
        sql += " AND DateOrdered <= " + DB.TO_DATE(p_DateTo);
    }

    // Process each order
    java.sql.PreparedStatement pstmt = null;
    java.sql.ResultSet rs = null;
    try {
        pstmt = DB.prepareStatement(sql, get_TrxName());
        rs = pstmt.executeQuery();

        while (rs.next()) {
            int orderId = rs.getInt("C_Order_ID");
            try {
                if (p_IsTestRun) {
                    // Log what would happen without actually doing it
                    addLog(0, null, null,
                        "Would generate invoice for Order #"
                        + orderId);
                } else {
                    generateInvoice(orderId);
                    invoicesGenerated++;
                    addLog(0, null, null,
                        "Invoice generated for Order #" + orderId);
                }
            } catch (Exception e) {
                errors++;
                addLog(0, null, null,
                    "ERROR processing Order #" + orderId
                    + ": " + e.getMessage());
            }
        }
    } finally {
        DB.close(rs, pstmt);
    }

    // Return a summary message
    String msg = invoicesGenerated + " invoices generated";
    if (errors > 0) {
        msg += ", " + errors + " errors";
    }
    if (p_IsTestRun) {
        msg = "TEST RUN: " + msg;
    }

    return msg;
}

private void generateInvoice(int orderId) {
    // Invoice generation logic here
    // This would use MOrder.createInvoice() or similar
}

The addLog() Overloads

The addLog() method has several overloads for different types of log entries:

// Simple text message
addLog(0, null, null, "Processing started");

// Message with a date
addLog(0, new Timestamp(System.currentTimeMillis()),
    null, "Batch started at this time");

// Message with a numeric value
addLog(0, null, new BigDecimal("1500.00"),
    "Total amount processed");

// Message with record reference (creates a clickable link in the log)
addLog(0, null, null, "Created Invoice",
    MTable.getTable_ID("C_Invoice"), invoiceId);

Return Messages

The string returned by doIt() is displayed to the user as the process result. iDempiere recognizes special message prefixes:

  • "@Error@" — Indicates the process failed. The message is displayed with an error indicator. The transaction is rolled back.
  • "@OK@" — Indicates success (the default if no prefix is used).
  • "@ProcessOK@" — Success with the standard “Process completed” message.
@Override
protected String doIt() throws Exception {
    try {
        int count = processRecords();
        if (count == 0) {
            return "@Error@ No records found matching the criteria";
        }
        return "@OK@ " + count + " records processed successfully";
    } catch (Exception e) {
        log.severe(e.getMessage());
        return "@Error@ " + e.getMessage();
    }
}

Accessing Context Information

Within a process, you can access the user’s context (client, organization, role, etc.):

@Override
protected String doIt() throws Exception {
    // Get context values
    Properties ctx = getCtx();
    int clientId = Env.getAD_Client_ID(ctx);
    int orgId = Env.getAD_Org_ID(ctx);
    int userId = Env.getAD_User_ID(ctx);
    int roleId = Env.getAD_Role_ID(ctx);

    // Get the transaction name (for database operations)
    String trxName = get_TrxName();

    // Get the process info (contains the AD_Process_ID,
    // AD_PInstance_ID, etc.)
    ProcessInfo pi = getProcessInfo();

    return "";
}

Registering a Process in the Application Dictionary

A process class on its own is not accessible to users. You must register it in the Application Dictionary so that iDempiere knows about it and can present it in the UI.

Step 1: Create the AD_Process Record

Navigate to System Admin > General Rules > System Rules > Report & Process (or search for “Report & Process”). Create a new record:

  • Search Key (Value): A unique identifier (e.g., GenerateInvoices)
  • Name: The display name (e.g., Generate Invoices from Orders)
  • Description: What the process does
  • Active: Yes
  • Entity Type: User Maintained (for custom code)
  • Data Access Level: Client+Organization (typical for business processes)
  • Type: Java
  • Classname: The fully qualified class name (e.g., com.example.myplugin.process.GenerateInvoicesProcess)

Step 2: Define Process Parameters (AD_Process_Para)

Switch to the Parameter tab and add a record for each parameter your process accepts:

Field Example for C_BPartner_ID Example for DateFrom
Name Business Partner Date From
DB Column Name C_BPartner_ID DateFrom
Reference Table Direct Date
Mandatory No No
Sequence 10 20
Default Logic (empty) @#Date@

The parameter names in AD_Process_Para must match the names you use in getParameterName() in your prepare() method. The Reference type determines what kind of input widget the user sees (text field, date picker, dropdown, lookup, etc.).

Step 3: Add a Menu Entry

To make the process accessible from the menu, navigate to System Admin > General Rules > System Rules > Menu. Create a new menu entry:

  • Name: Generate Invoices from Orders
  • Action: Process
  • Process: (select your AD_Process record)
  • Summary Level: No (this is a leaf item, not a folder)

Place the menu entry under the appropriate parent folder in the menu tree.

Step 4: Grant Role Access

Navigate to the Role window, select the role that should have access, switch to the Process Access tab, and add your process with Read/Write access.

Packaging with 2Pack

For distribution, use 2Pack (covered in Lesson 20) to export the AD_Process, AD_Process_Para, AD_Menu, and role access records. Include the 2Pack file in your plugin’s migration/ directory so it is automatically applied during installation.

Registering a Process via IProcessFactory

When iDempiere needs to instantiate a process class, it uses the IProcessFactory service (covered in Lesson 19). If your process class is in your plugin bundle, you must register an IProcessFactory so the framework can find and instantiate it:

package com.example.myplugin.factory;

import org.adempiere.base.IProcessFactory;
import org.compiere.process.ProcessCall;

public class MyProcessFactory implements IProcessFactory {

    @Override
    public ProcessCall newProcessInstance(String className) {
        switch (className) {
            case "com.example.myplugin.process.GenerateInvoicesProcess":
                return new com.example.myplugin.process
                    .GenerateInvoicesProcess();
            case "com.example.myplugin.process.RecalculatePrices":
                return new com.example.myplugin.process
                    .RecalculatePrices();
            default:
                return null; // Defer to the next factory
        }
    }
}

Register this factory as a DS component as described in Lesson 19. Without this factory registration, iDempiere’s default factory will not be able to find your process class (because it lives in a separate OSGi bundle with its own classloader).

Custom Forms: IFormController

Forms are interactive user interfaces for tasks that require a custom layout beyond what the standard window/tab/field model provides. Examples include visual scheduling tools, configuration wizards, data import screens, and specialized search interfaces.

The IFormController Interface

In the ZK web client, custom forms implement the org.adempiere.webui.panel.IFormController interface:

public interface IFormController {

    /**
     * Initialize the form. This is called when the form is first opened.
     * @param WindowNo the window number assigned to this form
     * @param panel the ADForm panel that hosts this form
     */
    public void initForm();

    /**
     * Get the form panel component.
     * @return the ZK Component that contains the form's UI
     */
    public Component getComponent();
}

Implementing a Custom Form

A custom form typically extends a ZK layout component and implements IFormController. Here is a complete example of a simple data entry form:

package com.example.myplugin.form;

import java.util.logging.Level;
import org.adempiere.webui.component.Button;
import org.adempiere.webui.component.Grid;
import org.adempiere.webui.component.GridFactory;
import org.adempiere.webui.component.Label;
import org.adempiere.webui.component.Listbox;
import org.adempiere.webui.component.ListItem;
import org.adempiere.webui.component.Row;
import org.adempiere.webui.component.Rows;
import org.adempiere.webui.component.Textbox;
import org.adempiere.webui.panel.ADForm;
import org.adempiere.webui.panel.IFormController;
import org.compiere.util.CLogger;
import org.compiere.util.Env;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Borderlayout;
import org.zkoss.zul.Center;
import org.zkoss.zul.North;
import org.zkoss.zul.South;
import org.zkoss.zul.Separator;

public class SimpleDataEntryForm implements IFormController,
        EventListener<Event> {

    private static final CLogger logger =
        CLogger.getCLogger(SimpleDataEntryForm.class);

    private ADForm adForm;
    private Borderlayout mainLayout;
    private Textbox nameField;
    private Textbox descriptionField;
    private Listbox categoryList;
    private Button submitButton;
    private Button clearButton;
    private Label statusLabel;

    @Override
    public void initForm() {
        buildUI();
    }

    @Override
    public Component getComponent() {
        return mainLayout;
    }

    public void setADForm(ADForm form) {
        this.adForm = form;
    }

    private void buildUI() {
        // Main layout using BorderLayout
        mainLayout = new Borderlayout();
        mainLayout.setWidth("100%");
        mainLayout.setHeight("100%");

        // North section: title and instructions
        North north = new North();
        mainLayout.appendChild(north);
        Label titleLabel = new Label("Custom Data Entry Form");
        titleLabel.setStyle(
            "font-size: 16px; font-weight: bold; padding: 10px;");
        north.appendChild(titleLabel);

        // Center section: input form
        Center center = new Center();
        mainLayout.appendChild(center);

        Grid grid = GridFactory.newGridLayout();
        grid.setWidth("500px");
        grid.setStyle("margin: 20px;");
        center.appendChild(grid);

        Rows rows = grid.newRows();

        // Name field
        Row nameRow = rows.newRow();
        nameRow.appendChild(new Label("Name:"));
        nameField = new Textbox();
        nameField.setWidth("300px");
        nameField.setMaxlength(100);
        nameRow.appendChild(nameField);

        // Description field
        Row descRow = rows.newRow();
        descRow.appendChild(new Label("Description:"));
        descriptionField = new Textbox();
        descriptionField.setWidth("300px");
        descriptionField.setRows(3);
        descriptionField.setMultiline(true);
        descRow.appendChild(descriptionField);

        // Category dropdown
        Row catRow = rows.newRow();
        catRow.appendChild(new Label("Category:"));
        categoryList = new Listbox();
        categoryList.setMold("select");
        categoryList.appendItem("Select...", "");
        categoryList.appendItem("Category A", "A");
        categoryList.appendItem("Category B", "B");
        categoryList.appendItem("Category C", "C");
        catRow.appendChild(categoryList);

        // Separator
        Row sepRow = rows.newRow();
        sepRow.appendChild(new Separator());
        sepRow.appendChild(new Separator());

        // Buttons
        Row buttonRow = rows.newRow();
        submitButton = new Button("Submit");
        submitButton.addEventListener(Events.ON_CLICK, this);
        clearButton = new Button("Clear");
        clearButton.addEventListener(Events.ON_CLICK, this);

        org.zkoss.zul.Hbox buttonBox = new org.zkoss.zul.Hbox();
        buttonBox.appendChild(submitButton);
        buttonBox.appendChild(clearButton);
        buttonRow.appendChild(new Label(""));
        buttonRow.appendChild(buttonBox);

        // South section: status bar
        South south = new South();
        mainLayout.appendChild(south);
        statusLabel = new Label("Ready");
        statusLabel.setStyle("padding: 5px; color: #666;");
        south.appendChild(statusLabel);
    }

    @Override
    public void onEvent(Event event) throws Exception {
        Component comp = event.getTarget();

        if (comp == submitButton) {
            handleSubmit();
        } else if (comp == clearButton) {
            handleClear();
        }
    }

    private void handleSubmit() {
        // Validate input
        String name = nameField.getValue();
        if (name == null || name.trim().isEmpty()) {
            statusLabel.setValue("Error: Name is required");
            statusLabel.setStyle("padding: 5px; color: red;");
            return;
        }

        ListItem selectedCategory = categoryList.getSelectedItem();
        if (selectedCategory == null
                || "".equals(selectedCategory.getValue())) {
            statusLabel.setValue("Error: Please select a category");
            statusLabel.setStyle("padding: 5px; color: red;");
            return;
        }

        // Process the data
        String description = descriptionField.getValue();
        String category = (String) selectedCategory.getValue();

        try {
            // Here you would save to the database, call a process,
            // or perform other business logic
            logger.info("Form submitted: name=" + name
                + ", description=" + description
                + ", category=" + category);

            statusLabel.setValue("Data submitted successfully: "
                + name);
            statusLabel.setStyle("padding: 5px; color: green;");

            // Optionally clear the form after successful submission
            handleClear();
        } catch (Exception e) {
            logger.log(Level.SEVERE, "Error submitting form", e);
            statusLabel.setValue("Error: " + e.getMessage());
            statusLabel.setStyle("padding: 5px; color: red;");
        }
    }

    private void handleClear() {
        nameField.setValue("");
        descriptionField.setValue("");
        categoryList.setSelectedIndex(0);
    }
}

ZK Components Reference

iDempiere wraps many ZK components in its own classes (in the org.adempiere.webui.component package) that provide additional functionality. Here are the most commonly used ones:

Component Purpose Key Methods
Textbox Single or multi-line text input getValue(), setValue(), setMaxlength()
NumberBox Numeric input with formatting getValue(), setValue()
Datebox Date picker getValue(), setValue()
Listbox Dropdown or list selection appendItem(), getSelectedItem()
Button Clickable button addEventListener(), setEnabled()
Label Read-only text display setValue()
Grid Two-column form layout newRows(), via GridFactory
Tab/Tabbox Tabbed interface appendChild()
Checkbox Boolean toggle isChecked(), setChecked()
WListbox Data grid with sortable columns setData(), getSelectedRowKey()

Form Layout

ZK provides several layout containers for organizing form components:

  • Borderlayout — Divides space into North, South, East, West, and Center regions. Ideal for full-page forms with a header, footer, and main content area.
  • Grid — Organizes components in rows and columns. Best for label-field pairs in a form layout.
  • Hbox/Vbox — Horizontal or vertical arrangement of components. Good for button bars or simple groupings.
  • Tabbox — Tabbed panels. Useful when a form has multiple sections.

Form Lifecycle

A form’s lifecycle in iDempiere follows these steps:

  1. User clicks a menu item or toolbar button that is linked to a Form in the AD
  2. iDempiere calls IFormFactory to instantiate the form class
  3. The ADForm panel calls setADForm() if the method exists
  4. The ADForm panel calls initForm()
  5. The form’s UI components are rendered in the user’s browser
  6. User interactions trigger event listeners (onClick, onChange, etc.)
  7. When the user closes the form tab, any cleanup should happen

Registering Forms in the Application Dictionary

Similar to processes, forms must be registered in the AD to be accessible.

Step 1: Create the AD_Form Record

Navigate to System Admin > General Rules > System Rules > Form. Create a new record:

  • Name: Simple Data Entry Form
  • Description: A custom data entry form for demonstration
  • Classname: com.example.myplugin.form.SimpleDataEntryForm
  • Entity Type: User Maintained
  • Active: Yes

Step 2: Add a Menu Entry

Create a menu entry with:

  • Action: Form
  • Form: (select your AD_Form record)

Step 3: Register IFormFactory

Create a factory to instantiate your form:

package com.example.myplugin.factory;

import org.adempiere.base.IFormFactory;

public class MyFormFactory implements IFormFactory {

    @Override
    public Object newFormInstance(String formClassName) {
        if ("com.example.myplugin.form.SimpleDataEntryForm"
                .equals(formClassName)) {
            return new com.example.myplugin.form.SimpleDataEntryForm();
        }
        return null;
    }
}

Register this in component.xml:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
    name="com.example.myplugin.factory.MyFormFactory">
  <implementation class="com.example.myplugin.factory.MyFormFactory"/>
  <service>
    <provide interface="org.adempiere.base.IFormFactory"/>
  </service>
  <property name="service.ranking" type="Integer" value="100"/>
</scr:component>

Practical Example: A Report Generation Process

Let us build a complete process that queries data and generates a summary report. This process finds all overdue invoices for a given business partner and creates a log summary:

package com.example.myplugin.process;

import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.util.logging.Level;
import org.compiere.model.MBPartner;
import org.compiere.process.ProcessInfoParameter;
import org.compiere.process.SvrProcess;
import org.compiere.util.DB;
import org.compiere.util.Env;

public class OverdueInvoiceReport extends SvrProcess {

    private int p_C_BPartner_ID = 0;
    private int p_DaysOverdue = 30;

    @Override
    protected void prepare() {
        ProcessInfoParameter[] params = getParameter();
        for (ProcessInfoParameter param : params) {
            String name = param.getParameterName();
            if ("C_BPartner_ID".equals(name)) {
                p_C_BPartner_ID = param.getParameterAsInt();
            } else if ("DaysOverdue".equals(name)) {
                p_DaysOverdue = param.getParameterAsInt();
            }
        }
    }

    @Override
    protected String doIt() throws Exception {
        addLog("Starting Overdue Invoice Report");
        addLog("Parameters: Days Overdue = " + p_DaysOverdue
            + (p_C_BPartner_ID > 0
                ? ", BPartner ID = " + p_C_BPartner_ID : ""));

        StringBuilder sql = new StringBuilder();
        sql.append("SELECT i.C_Invoice_ID, i.DocumentNo, ")
           .append("i.DateInvoiced, i.GrandTotal, i.OpenAmt, ")
           .append("bp.Name AS BPartnerName, ")
           .append("EXTRACT(DAY FROM now() - i.DateInvoiced) ")
           .append("AS DaysOutstanding ")
           .append("FROM C_Invoice i ")
           .append("JOIN C_BPartner bp ")
           .append("ON i.C_BPartner_ID = bp.C_BPartner_ID ")
           .append("WHERE i.IsSOTrx = 'Y' ")
           .append("AND i.IsPaid = 'N' ")
           .append("AND i.DocStatus = 'CO' ")
           .append("AND i.AD_Client_ID = ? ")
           .append("AND EXTRACT(DAY FROM now() - i.DateInvoiced) > ? ");

        if (p_C_BPartner_ID > 0) {
            sql.append("AND i.C_BPartner_ID = ? ");
        }
        sql.append("ORDER BY DaysOutstanding DESC");

        int count = 0;
        BigDecimal totalOverdue = BigDecimal.ZERO;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            pstmt = DB.prepareStatement(
                sql.toString(), get_TrxName());
            int idx = 1;
            pstmt.setInt(idx++, Env.getAD_Client_ID(getCtx()));
            pstmt.setInt(idx++, p_DaysOverdue);
            if (p_C_BPartner_ID > 0) {
                pstmt.setInt(idx++, p_C_BPartner_ID);
            }

            rs = pstmt.executeQuery();
            addLog("-----------------------------------");
            addLog("Invoice# | Partner | Amount | Days");
            addLog("-----------------------------------");

            while (rs.next()) {
                String docNo = rs.getString("DocumentNo");
                String bpName = rs.getString("BPartnerName");
                BigDecimal openAmt = rs.getBigDecimal("OpenAmt");
                int days = rs.getInt("DaysOutstanding");

                addLog(docNo + " | " + bpName + " | "
                    + openAmt + " | " + days + " days");

                totalOverdue = totalOverdue.add(openAmt);
                count++;
            }
        } finally {
            DB.close(rs, pstmt);
        }

        addLog("-----------------------------------");
        addLog("Total overdue invoices: " + count);
        addLog("Total overdue amount: " + totalOverdue);

        if (count == 0) {
            return "@OK@ No overdue invoices found";
        }
        return "@OK@ Found " + count + " overdue invoices "
            + "totaling " + totalOverdue;
    }
}

Plugin Structure: Bringing Processes and Forms Together

A complete plugin that includes both processes and forms has this structure:

com.example.myplugin/
├── META-INF/
│   └── MANIFEST.MF
├── OSGI-INF/
│   ├── ProcessFactory.xml
│   ├── FormFactory.xml
│   └── EventHandler.xml
├── src/
│   └── com/example/myplugin/
│       ├── Activator.java
│       ├── factory/
│       │   ├── MyProcessFactory.java
│       │   └── MyFormFactory.java
│       ├── process/
│       │   ├── GenerateInvoicesProcess.java
│       │   └── OverdueInvoiceReport.java
│       ├── form/
│       │   └── SimpleDataEntryForm.java
│       └── event/
│           └── OrderLineEventHandler.java
├── migration/
│   └── local/
│       └── 202501150930_RegisterProcessAndForm.zip
└── build.properties

The MANIFEST.MF would include all necessary imports:

Import-Package: org.compiere.model;version="0.0.0",
 org.compiere.process;version="0.0.0",
 org.compiere.util;version="0.0.0",
 org.adempiere.base;version="0.0.0",
 org.adempiere.base.event;version="0.0.0",
 org.adempiere.webui.component;version="0.0.0",
 org.adempiere.webui.panel;version="0.0.0",
 org.osgi.framework;version="1.9.0",
 org.osgi.service.event;version="1.4.0",
 org.zkoss.zk.ui;version="0.0.0",
 org.zkoss.zk.ui.event;version="0.0.0",
 org.zkoss.zul;version="0.0.0"
Service-Component: OSGI-INF/*.xml

Key Takeaways

  • Custom processes extend SvrProcess and implement two methods: prepare() for reading parameters and doIt() for business logic.
  • Use getParameter() in prepare() to read user-provided parameters, and addLog() in doIt() to provide progress feedback.
  • Return messages from doIt() support @Error@ and @OK@ prefixes for status indication.
  • Processes must be registered in the AD (AD_Process, AD_Process_Para, AD_Menu) and resolved via IProcessFactory.
  • Custom forms implement IFormController and use ZK components (Textbox, Listbox, Grid, Button, Borderlayout) to build interactive interfaces.
  • Forms must be registered in the AD (AD_Form, AD_Menu) and resolved via IFormFactory.
  • Use 2Pack to distribute the AD configuration that your processes and forms require.

What’s Next

With processes, forms, and event handlers in your toolkit, you have the core skills for iDempiere plugin development. Future lessons will explore advanced topics including custom REST API endpoints, integration with external systems, reporting with JasperReports, and performance optimization techniques for large-scale deployments.

You Missed