iDempiere’s plugin architecture is built on OSGi (Open Services Gateway initiative), enabling modular development where functionality can be added, removed, or updated without modifying core code.
The Bundle-SymbolicName with ;singleton:=true ensures only one version of your plugin is active. The Service-Component directive enables Declarative Services for automatic service registration.
package org.example.customplugin.validator;import org.compiere.model.*;import org.compiere.util.CLogger;/** * Custom Model Validator * Intercepts PO lifecycle events */public class CustomValidator implements ModelValidator { private static CLogger log = CLogger.getCLogger(CustomValidator.class); private int m_AD_Client_ID = -1; @Override public void initialize(ModelValidationEngine engine, MClient client) { // Register for specific tables engine.addModelChange(MOrder.Table_Name, this); engine.addModelChange(MInvoice.Table_Name, this); // Register for document validation engine.addDocValidate(MOrder.Table_Name, this); m_AD_Client_ID = client.getAD_Client_ID(); log.info("Initialized for client: " + client.getName()); } @Override public int getAD_Client_ID() { return m_AD_Client_ID; } @Override public String login(int AD_Org_ID, int AD_Role_ID, int AD_User_ID) { log.info("User login: " + AD_User_ID); return null; } @Override public String modelChange(PO po, int type) throws Exception { // Dispatch to specific handlers if (po.get_TableName().equals(MOrder.Table_Name)) { return orderChanged((MOrder)po, type); } if (po.get_TableName().equals(MInvoice.Table_Name)) { return invoiceChanged((MInvoice)po, type); } return null; } /** * Handle order changes */ private String orderChanged(MOrder order, int type) { // Before new record is created if (type == TYPE_BEFORE_NEW) { log.info("Creating new order for partner: " + order.getC_BPartner_ID()); // Set default values if (order.getM_Warehouse_ID() <= 0) { order.setM_Warehouse_ID(getDefaultWarehouse()); } } // After record is changed if (type == TYPE_AFTER_CHANGE) { // Partner changed - update related data if (order.is_ValueChanged("C_BPartner_ID")) { updatePartnerDependencies(order); } // Payment term changed - recalculate due date if (order.is_ValueChanged("C_PaymentTerm_ID")) { calculateDueDate(order); } } // Before delete - check references if (type == TYPE_BEFORE_DELETE) { if (hasShipments(order)) { return "Cannot delete order with shipments"; } } return null; // Validation passed } /** * Handle invoice changes */ private String invoiceChanged(MInvoice invoice, int type) { if (type == TYPE_BEFORE_NEW) { // Custom invoice validation if (invoice.getGrandTotal().signum() < 0 && !invoice.getDocumentNo().startsWith("CM-")) { return "Credit memos must start with CM- prefix"; } } return null; } @Override public String docValidate(PO po, int timing) { if (!(po instanceof DocAction)) return null; if (po.get_TableName().equals(MOrder.Table_Name)) { return orderDocValidate((MOrder)po, timing); } return null; } /** * Validate order document actions */ private String orderDocValidate(MOrder order, int timing) { // Before complete if (timing == TIMING_BEFORE_COMPLETE) { // Check credit limit MBPartner partner = new MBPartner(order.getCtx(), order.getC_BPartner_ID(), order.get_TrxName()); BigDecimal creditAvailable = partner.getSO_CreditLimit() .subtract(partner.getSO_CreditUsed()); if (order.getGrandTotal().compareTo(creditAvailable) > 0) { return "Order exceeds customer credit limit. Available: " + creditAvailable; } // Validate inventory availability for (MOrderLine line : order.getLines()) { if (line.getM_Product_ID() > 0) { BigDecimal qtyAvailable = MStorage.getQtyAvailable( line.getM_Warehouse_ID(), line.getM_Product_ID(), line.getM_AttributeSetInstance_ID(), order.get_TrxName()); if (line.getQtyOrdered().compareTo(qtyAvailable) > 0) { return "Insufficient inventory for product: " + line.getProduct().getName(); } } } } // After complete if (timing == TIMING_AFTER_COMPLETE) { // Send notification email sendOrderConfirmation(order); // Update custom statistics updateSalesStatistics(order); } return null; } private int getDefaultWarehouse() { // Implementation return 0; } private void updatePartnerDependencies(MOrder order) { // Implementation } private void calculateDueDate(MOrder order) { // Implementation } private boolean hasShipments(MOrder order) { // Implementation return false; } private void sendOrderConfirmation(MOrder order) { // Implementation } private void updateSalesStatistics(MOrder order) { // Implementation }}
Model validators run synchronously in the save/delete transaction. Keep operations fast to avoid performance issues. For long-running tasks, use event handlers instead.
package org.example.customplugin.handler;import org.osgi.service.event.Event;import org.osgi.service.event.EventHandler;import org.compiere.model.*;import org.compiere.util.CLogger;import org.adempiere.base.event.IEventTopics;/** * Asynchronous event handler for order processing */public class OrderEventHandler implements EventHandler { private static CLogger log = CLogger.getCLogger(OrderEventHandler.class); @Override public void handleEvent(Event event) { // Extract event properties String topic = event.getTopic(); PO po = (PO) event.getProperty("po"); if (po == null) return; try { // Handle specific table events if (MOrder.Table_Name.equals(po.get_TableName())) { handleOrderEvent((MOrder)po, topic); } } catch (Exception e) { log.severe("Error handling event: " + e.getMessage()); } } private void handleOrderEvent(MOrder order, String topic) { if (IEventTopics.PO_AFTER_NEW.equals(topic)) { // New order created log.info("New order created: " + order.getDocumentNo()); sendWelcomeEmail(order); } if (IEventTopics.DOC_AFTER_COMPLETE.equals(topic)) { // Order completed log.info("Order completed: " + order.getDocumentNo()); // Update inventory predictions updateInventoryForecast(order); // Trigger fulfillment workflow triggerFulfillment(order); // Update customer loyalty points updateLoyaltyPoints(order); } } private void sendWelcomeEmail(MOrder order) { // Implementation } private void updateInventoryForecast(MOrder order) { // Implementation } private void triggerFulfillment(MOrder order) { // Implementation } private void updateLoyaltyPoints(MOrder order) { // Implementation }}
Event handlers run asynchronously and outside the original transaction. They’re ideal for non-critical operations like notifications, analytics, and integration tasks.
# Install via OSGi consoletelnet localhost 12612osgi> install file:///path/to/org.example.customplugin-1.0.0.jarBundle id is 234osgi> start 234# Verify bundle is activeosgi> ss | grep example234 ACTIVE org.example.customplugin_1.0.0
Always test plugins in a development environment before deploying to production. Faulty plugins can affect system stability.