Custom Processes and Forms
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:
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.doIt()— Called afterprepare()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:
- User clicks a menu item or toolbar button that is linked to a Form in the AD
- iDempiere calls
IFormFactoryto instantiate the form class - The
ADFormpanel callssetADForm()if the method exists - The
ADFormpanel callsinitForm() - The form’s UI components are rendered in the user’s browser
- User interactions trigger event listeners (onClick, onChange, etc.)
- 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
SvrProcessand implement two methods:prepare()for reading parameters anddoIt()for business logic. - Use
getParameter()inprepare()to read user-provided parameters, andaddLog()indoIt()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
IFormControllerand 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.
繁體中文翻譯
概覽
- 您將學到:
- 如何使用 SvrProcess 建立自訂伺服器流程,包括生命週期(prepare/doIt)、參數、日誌記錄和返回訊息
- 如何在應用程式字典中註冊流程,並從選單或工具列存取它們
- 如何使用 IFormController 搭配 ZK 元件、版面配置和生命週期管理來建立自訂互動式表單
- 先決條件:第 1–22 課(特別是第 19 和 21 課:擴充點與建立您的第一個外掛)
- 預估閱讀時間:26 分鐘
簡介
事件處理器對使用者或其他流程發起的資料變更做出反應。但如果是使用者刻意發起的操作呢 — 產生一批發票、重新計算產品目錄中的價格、從外部檔案匯入資料或執行自訂報表?這些是流程的領域。如果是不符合標準視窗/頁籤/欄位範式的專門使用者介面呢 — 視覺化設定工具、拖放排程器或多步驟精靈?這些是表單的領域。
流程和表單是 iDempiere 外掛架構中剩餘的兩個主要擴充點。本課深入涵蓋兩者,從 Java 實作到應用程式字典註冊再到部署工作流程。
自訂流程:SvrProcess
iDempiere 中的流程是一個伺服器端操作,可以從選單、工具列按鈕或其他程式碼中以程式方式觸發。每個自訂流程都擴展 org.compiere.process.SvrProcess 基礎類別。
SvrProcess 生命週期
當流程執行時,iDempiere 依序呼叫兩個方法:
prepare()— 首先呼叫。這是您讀取和儲存使用者提供的流程參數的地方。您不應在此執行任何業務邏輯 — 只做參數解析。doIt()— 在prepare()完成後呼叫。這是您實作實際業務邏輯的地方。返回值是流程完成時顯示給使用者的訊息字串。
流程參數
流程可以接受使用者在執行前填入的參數。這些參數在應用程式字典(AD_Process_Para)中定義,並在執行時傳遞給您的流程。在 prepare() 方法中,您使用 getParameter() 讀取它們。
addLog() 方法:使用者回饋
長時間執行的流程應向使用者提供有關進度和結果的回饋。addLog() 方法將訊息寫入流程日誌,使用者可以在執行期間和之後查看。
返回訊息
doIt() 返回的字串以流程結果的形式顯示給使用者。iDempiere 識別特殊的訊息前綴:
"@Error@"— 表示流程失敗。訊息以錯誤指示器顯示。交易會被回滾。"@OK@"— 表示成功(未使用前綴時的預設值)。"@ProcessOK@"— 帶有標準「流程已完成」訊息的成功。
存取上下文資訊
在流程中,您可以存取使用者的上下文(客戶端、組織、角色等)。
在應用程式字典中註冊流程
流程類別本身對使用者來說是不可存取的。您必須在應用程式字典中註冊它,以便 iDempiere 知道它並可以在 UI 中展示。
步驟 1:建立 AD_Process 記錄
導覽至 System Admin > General Rules > System Rules > Report & Process(或搜尋 “Report & Process”)。建立新記錄。
步驟 2:定義流程參數(AD_Process_Para)
切換到參數頁籤,為您的流程接受的每個參數新增一條記錄。AD_Process_Para 中的參數名稱必須與您在 prepare() 方法中使用的 getParameterName() 名稱相符。
步驟 3:新增選單項目
要使流程可從選單存取,請導覽至選單視窗。建立新的選單項目,動作設為「流程」。
步驟 4:授予角色存取權限
導覽至角色視窗,選擇應該有存取權限的角色,切換到流程存取頁籤,並新增您的流程並授予讀/寫權限。
使用 2Pack 打包
如需發佈,使用 2Pack(第 20 課中介紹的)來匯出 AD_Process、AD_Process_Para、AD_Menu 和角色存取記錄。將 2Pack 檔案包含在您的外掛的 migration/ 目錄中,以便在安裝期間自動套用。
透過 IProcessFactory 註冊流程
當 iDempiere 需要實例化流程類別時,它使用 IProcessFactory 服務(第 19 課中介紹的)。如果您的流程類別在您的外掛 bundle 中,您必須註冊一個 IProcessFactory,以便框架可以找到並實例化它。
自訂表單:IFormController
表單是用於需要超出標準視窗/頁籤/欄位模型的自訂版面配置的互動式使用者介面。範例包括視覺化排程工具、設定精靈、資料匯入畫面和專門的搜尋介面。
IFormController 介面
在 ZK 網頁客戶端中,自訂表單實作 org.adempiere.webui.panel.IFormController 介面。
實作自訂表單
自訂表單通常擴展一個 ZK 版面配置元件並實作 IFormController。
ZK 元件參考
iDempiere 將許多 ZK 元件封裝在自己的類別中(在 org.adempiere.webui.component 套件中),提供額外的功能。常用的包括:
| 元件 | 用途 | 關鍵方法 |
|---|---|---|
Textbox |
單行或多行文字輸入 | getValue()、setValue()、setMaxlength() |
NumberBox |
帶格式化的數值輸入 | getValue()、setValue() |
Datebox |
日期選擇器 | getValue()、setValue() |
Listbox |
下拉選單或清單選擇 | appendItem()、getSelectedItem() |
Button |
可點擊按鈕 | addEventListener()、setEnabled() |
Label |
唯讀文字顯示 | setValue() |
Grid |
兩欄表單版面配置 | newRows(),透過 GridFactory |
Tab/Tabbox |
分頁式介面 | appendChild() |
Checkbox |
布林值切換 | isChecked()、setChecked() |
WListbox |
可排序欄的資料網格 | setData()、getSelectedRowKey() |
表單版面配置
ZK 提供幾種版面配置容器來組織表單元件:
- Borderlayout — 將空間分為北、南、東、西和中心區域。適合有標題、頁尾和主要內容區域的全頁表單。
- Grid — 以列和行組織元件。最適合表單版面配置中的標籤-欄位配對。
- Hbox/Vbox — 水平或垂直排列元件。適合按鈕列或簡單分組。
- Tabbox — 分頁面板。當表單有多個區段時很有用。
表單生命週期
iDempiere 中表單的生命週期遵循以下步驟:
- 使用者點擊連結到 AD 中表單的選單項目或工具列按鈕
- iDempiere 呼叫
IFormFactory來實例化表單類別 ADForm面板呼叫setADForm()(如果方法存在)ADForm面板呼叫initForm()- 表單的 UI 元件在使用者的瀏覽器中呈現
- 使用者互動觸發事件監聽器(onClick、onChange 等)
- 當使用者關閉表單頁籤時,應執行任何清理工作
在應用程式字典中註冊表單
與流程類似,表單必須在 AD 中註冊才能存取。
步驟 1:建立 AD_Form 記錄
導覽至 System Admin > General Rules > System Rules > Form。建立新記錄。
步驟 2:新增選單項目
建立選單項目,動作設為「表單」。
步驟 3:註冊 IFormFactory
建立工廠以實例化您的表單,並在 component.xml 中註冊。
外掛結構:整合流程和表單
一個包含流程和表單的完整外掛具有以下結構:
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
重點摘要
- 自訂流程擴展
SvrProcess並實作兩個方法:prepare()用於讀取參數,doIt()用於業務邏輯。 - 在
prepare()中使用getParameter()讀取使用者提供的參數,在doIt()中使用addLog()提供進度回饋。 doIt()的返回訊息支援@Error@和@OK@前綴用於狀態指示。- 流程必須在 AD(AD_Process、AD_Process_Para、AD_Menu)中註冊,並透過
IProcessFactory解析。 - 自訂表單實作
IFormController,並使用 ZK 元件(Textbox、Listbox、Grid、Button、Borderlayout)建立互動式介面。 - 表單必須在 AD(AD_Form、AD_Menu)中註冊,並透過
IFormFactory解析。 - 使用 2Pack 來發佈您的流程和表單所需的 AD 設定。
下一步
有了流程、表單和事件處理器,您已具備 iDempiere 外掛開發的核心技能。未來的課程將探討進階主題,包括自訂 REST API 端點、與外部系統整合、使用 JasperReports 的報表,以及大規模部署的效能最佳化技術。
日本語翻訳
概要
- 学習内容:
- SvrProcess を使用したカスタムサーバープロセスの作成方法(ライフサイクル(prepare/doIt)、パラメータ、ログ、戻りメッセージを含む)
- アプリケーション辞書にプロセスを登録し、メニューやツールバーからアクセス可能にする方法
- IFormController と ZK コンポーネント、レイアウト、ライフサイクル管理を使用したカスタムインタラクティブフォームの構築方法
- 前提条件:第1〜22課(特に第19課と第21課:拡張ポイントと初めてのプラグイン作成)
- 推定読了時間:26分
はじめに
イベントハンドラはユーザーや他のプロセスによって開始されたデータ変更に反応します。しかし、ユーザーが意図的に開始する操作はどうでしょうか — 請求書のバッチ生成、商品カタログ全体の価格再計算、外部ファイルからのデータインポート、カスタムレポートの実行など?これらはプロセスの領域です。標準的なウィンドウ/タブ/フィールドのパラダイムに収まらない特殊なユーザーインターフェースはどうでしょうか — ビジュアル設定ツール、ドラッグ&ドロップスケジューラ、マルチステップウィザードなど?これらはフォームの領域です。
プロセスとフォームは、iDempiere のプラグインアーキテクチャにおける残りの2つの主要な拡張ポイントです。この課では両方を深く取り上げ、Java 実装からアプリケーション辞書への登録、デプロイワークフローまでカバーします。
カスタムプロセス:SvrProcess
iDempiere のプロセスは、メニュー、ツールバーボタン、または他のコードからプログラム的にトリガーできるサーバーサイドの操作です。すべてのカスタムプロセスは org.compiere.process.SvrProcess 基底クラスを拡張します。
SvrProcess ライフサイクル
プロセスが実行されると、iDempiere は2つのメソッドを順番に呼び出します:
prepare()— 最初に呼び出されます。ユーザーが提供したプロセスパラメータを読み取り、保存する場所です。ここではビジネスロジックを実行すべきではありません — パラメータの解析のみです。doIt()—prepare()の完了後に呼び出されます。実際のビジネスロジックを実装する場所です。戻り値はプロセス完了時にユーザーに表示されるメッセージ文字列です。
プロセスパラメータ
プロセスは、ユーザーが実行前に入力するパラメータを受け取ることができます。これらのパラメータはアプリケーション辞書(AD_Process_Para)で定義され、実行時にプロセスに渡されます。prepare() メソッドで getParameter() を使用して読み取ります。
addLog() メソッド:ユーザーフィードバック
長時間実行されるプロセスは、進捗と結果についてユーザーにフィードバックを提供すべきです。addLog() メソッドはプロセスログにメッセージを書き込み、ユーザーは実行中および実行後に確認できます。
戻りメッセージ
doIt() から返される文字列は、プロセス結果としてユーザーに表示されます。iDempiere は特別なメッセージプレフィックスを認識します:
"@Error@"— プロセスが失敗したことを示します。メッセージはエラーインジケータ付きで表示されます。トランザクションはロールバックされます。"@OK@"— 成功を示します(プレフィックスなしの場合のデフォルト)。"@ProcessOK@"— 標準的な「プロセス完了」メッセージ付きの成功。
コンテキスト情報へのアクセス
プロセス内で、ユーザーのコンテキスト(クライアント、組織、ロールなど)にアクセスできます。
アプリケーション辞書にプロセスを登録する
プロセスクラスだけではユーザーからアクセスできません。iDempiere がそれを認識し UI で提示できるように、アプリケーション辞書に登録する必要があります。
ステップ 1:AD_Process レコードの作成
System Admin > General Rules > System Rules > Report & Process(または「Report & Process」を検索)に移動します。新しいレコードを作成します。
ステップ 2:プロセスパラメータの定義(AD_Process_Para)
パラメータタブに切り替え、プロセスが受け取る各パラメータのレコードを追加します。AD_Process_Para のパラメータ名は、prepare() メソッドの getParameterName() で使用する名前と一致する必要があります。
ステップ 3:メニューエントリの追加
プロセスをメニューからアクセス可能にするには、メニューウィンドウに移動します。アクションを「プロセス」に設定した新しいメニューエントリを作成します。
ステップ 4:ロールアクセスの付与
ロールウィンドウに移動し、アクセスすべきロールを選択し、プロセスアクセスタブに切り替えて、読み取り/書き込みアクセスでプロセスを追加します。
2Pack でのパッケージング
配布用に、2Pack(第20課でカバー)を使用して AD_Process、AD_Process_Para、AD_Menu、ロールアクセスレコードをエクスポートします。2Pack ファイルをプラグインの migration/ ディレクトリに含め、インストール時に自動的に適用されるようにします。
IProcessFactory 経由でのプロセス登録
iDempiere がプロセスクラスをインスタンス化する必要がある場合、IProcessFactory サービス(第19課でカバー)を使用します。プロセスクラスがプラグインバンドル内にある場合、フレームワークがそれを見つけてインスタンス化できるように IProcessFactory を登録する必要があります。
カスタムフォーム:IFormController
フォームは、標準的なウィンドウ/タブ/フィールドモデルを超えたカスタムレイアウトが必要なタスク用のインタラクティブなユーザーインターフェースです。例としては、ビジュアルスケジューリングツール、設定ウィザード、データインポート画面、特殊な検索インターフェースなどがあります。
IFormController インターフェース
ZK Web クライアントでは、カスタムフォームは org.adempiere.webui.panel.IFormController インターフェースを実装します。
カスタムフォームの実装
カスタムフォームは通常、ZK レイアウトコンポーネントを拡張し、IFormController を実装します。
ZK コンポーネントリファレンス
iDempiere は多くの ZK コンポーネントを独自のクラス(org.adempiere.webui.component パッケージ内)でラップし、追加機能を提供します。最もよく使用されるものは:
| コンポーネント | 用途 | 主要メソッド |
|---|---|---|
Textbox |
単一行または複数行テキスト入力 | getValue()、setValue()、setMaxlength() |
NumberBox |
フォーマット付き数値入力 | getValue()、setValue() |
Datebox |
日付ピッカー | getValue()、setValue() |
Listbox |
ドロップダウンまたはリスト選択 | appendItem()、getSelectedItem() |
Button |
クリック可能なボタン | addEventListener()、setEnabled() |
Label |
読み取り専用テキスト表示 | setValue() |
Grid |
2カラムフォームレイアウト | newRows()、GridFactory 経由 |
Tab/Tabbox |
タブインターフェース | appendChild() |
Checkbox |
ブール値トグル | isChecked()、setChecked() |
WListbox |
ソート可能なカラムを持つデータグリッド | setData()、getSelectedRowKey() |
フォームレイアウト
ZK はフォームコンポーネントを整理するためのいくつかのレイアウトコンテナを提供します:
- Borderlayout — 空間を北、南、東、西、中央の領域に分割します。ヘッダー、フッター、メインコンテンツ領域を持つフルページフォームに最適です。
- Grid — コンポーネントを行と列で整理します。フォームレイアウトのラベルとフィールドのペアに最適です。
- Hbox/Vbox — コンポーネントの水平または垂直配置。ボタンバーや単純なグループ化に適しています。
- Tabbox — タブパネル。フォームに複数のセクションがある場合に便利です。
フォームのライフサイクル
iDempiere でのフォームのライフサイクルは以下のステップに従います:
- ユーザーが AD のフォームにリンクされたメニュー項目またはツールバーボタンをクリック
- iDempiere が
IFormFactoryを呼び出してフォームクラスをインスタンス化 ADFormパネルがメソッドが存在する場合setADForm()を呼び出すADFormパネルがinitForm()を呼び出す- フォームの UI コンポーネントがユーザーのブラウザにレンダリングされる
- ユーザーの操作がイベントリスナーをトリガー(onClick、onChange など)
- ユーザーがフォームタブを閉じるとき、クリーンアップが実行されるべき
アプリケーション辞書にフォームを登録する
プロセスと同様に、フォームもアクセス可能にするために AD に登録する必要があります。
ステップ 1:AD_Form レコードの作成
System Admin > General Rules > System Rules > Form に移動します。新しいレコードを作成します。
ステップ 2:メニューエントリの追加
アクションを「フォーム」に設定したメニューエントリを作成します。
ステップ 3:IFormFactory の登録
フォームをインスタンス化するファクトリを作成し、component.xml に登録します。
プラグイン構造:プロセスとフォームの統合
プロセスとフォームの両方を含む完全なプラグインは以下の構造を持ちます:
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
重要なポイント
- カスタムプロセスは
SvrProcessを拡張し、2つのメソッドを実装します:パラメータ読み取り用のprepare()とビジネスロジック用のdoIt()。 prepare()でgetParameter()を使用してユーザー提供のパラメータを読み取り、doIt()でaddLog()を使用して進捗フィードバックを提供します。doIt()の戻りメッセージはステータス表示用の@Error@と@OK@プレフィックスをサポートします。- プロセスは AD(AD_Process、AD_Process_Para、AD_Menu)に登録し、
IProcessFactoryで解決する必要があります。 - カスタムフォームは
IFormControllerを実装し、ZK コンポーネント(Textbox、Listbox、Grid、Button、Borderlayout)を使用してインタラクティブなインターフェースを構築します。 - フォームは AD(AD_Form、AD_Menu)に登録し、
IFormFactoryで解決する必要があります。 - 2Pack を使用してプロセスとフォームに必要な AD 設定を配布します。
次のステップ
プロセス、フォーム、イベントハンドラをツールキットに備え、iDempiere プラグイン開発のコアスキルを身につけました。今後の課では、カスタム REST API エンドポイント、外部システムとの統合、JasperReports によるレポート作成、大規模デプロイのパフォーマンス最適化テクニックなどの高度なトピックを探ります。