Implement event-driven business logic with model validators in iDempiere
Model Validators are event handlers that intercept and respond to document and data model events throughout the system. They’re the primary mechanism for implementing business rules that must execute when data changes.
Document events fire during the document lifecycle:
// Document timing eventsMODEL_VALIDATOR.TIMING_BEFORE_PREPARE // Before document prepareMODEL_VALIDATOR.TIMING_AFTER_PREPARE // After document prepareMODEL_VALIDATOR.TIMING_BEFORE_VOID // Before document voidMODEL_VALIDATOR.TIMING_AFTER_VOID // After document voidMODEL_VALIDATOR.TIMING_BEFORE_CLOSE // Before document closeMODEL_VALIDATOR.TIMING_AFTER_CLOSE // After document closeMODEL_VALIDATOR.TIMING_BEFORE_REACTIVATE // Before document reactivateMODEL_VALIDATOR.TIMING_AFTER_REACTIVATE // After document reactivateMODEL_VALIDATOR.TIMING_BEFORE_REVERSECORRECT // Before reverse correctionMODEL_VALIDATOR.TIMING_AFTER_REVERSECORRECT // After reverse correctionMODEL_VALIDATOR.TIMING_BEFORE_REVERSEACCRUAL // Before reverse accrualMODEL_VALIDATOR.TIMING_AFTER_REVERSEACCRUAL // After reverse accrualMODEL_VALIDATOR.TIMING_BEFORE_COMPLETE // Before document completeMODEL_VALIDATOR.TIMING_AFTER_COMPLETE // After document completeMODEL_VALIDATOR.TIMING_BEFORE_POST // Before accounting postMODEL_VALIDATOR.TIMING_AFTER_POST // After accounting post
// Table-level eventsMODEL_VALIDATOR.TYPE_BEFORE_NEW // Before creating new recordMODEL_VALIDATOR.TYPE_BEFORE_CHANGE // Before updating existing recordMODEL_VALIDATOR.TYPE_BEFORE_DELETE // Before deleting recordMODEL_VALIDATOR.TYPE_AFTER_NEW // After creating new recordMODEL_VALIDATOR.TYPE_AFTER_CHANGE // After updating existing recordMODEL_VALIDATOR.TYPE_AFTER_DELETE // After deleting recordMODEL_VALIDATOR.TYPE_AFTER_NEW_REPLICATION // After replication of new recordMODEL_VALIDATOR.TYPE_AFTER_CHANGE_REPLICATION // After replication of change
From org.compiere.model.FactsValidator in the iDempiere source:
org.compiere.model.FactsValidator
package org.compiere.model;import java.util.List;import org.compiere.acct.Fact;/** * Interface for posting validator * Validates accounting facts before posting */public interface FactsValidator { /** * Get AD_Client_ID * @return client ID */ public int getAD_Client_ID(); /** * Validate accounting facts * @param schema Accounting schema * @param facts List of facts to validate * @param po Persistent object being posted * @return error message or null - * if not null, the document will be marked as Invalid */ public String factsValidate(MAcctSchema schema, List<Fact> facts, PO po);}
This specialized validator checks accounting entries before they’re posted.
// Check if value changedif (po.is_ValueChanged("C_BPartner_ID")) { // Value changed}// Get old valueInteger oldBP = (Integer) po.get_ValueOld("C_BPartner_ID");// Get new value Integer newBP = po.get_ValueAsInt("C_BPartner_ID");// Check multiple fieldsif (po.is_ValueChanged("Qty") || po.is_ValueChanged("Price")) { // Recalculate total}
private String orderDocValidate(PO po, int timing) { // Cast to specific type MOrder order = (MOrder) po; // Access standard getters String docNo = order.getDocumentNo(); int bpID = order.getC_BPartner_ID(); BigDecimal total = order.getGrandTotal(); // Access via PO methods String docStatus = po.get_ValueAsString("DocStatus"); Integer orgID = po.get_ValueAsInt("AD_Org_ID"); // Get related records MBPartner bp = new MBPartner(po.getCtx(), bpID, po.get_TrxName()); MOrderLine[] lines = order.getLines(); // Check document type if (order.isSOTrx()) { // Sales order logic } else { // Purchase order logic } return null;}
Validators execute for every save/change - keep them fast
Avoid complex queries in validators
Don’t load unnecessary related records
Cache frequently accessed data
Use indexes on custom validation columns
// Bad - loads all lines unnecessarilyMOrderLine[] lines = order.getLines();for (MOrderLine line : lines) { // Process each line}// Good - use SQL count if you just need quantityint lineCount = DB.getSQLValue(po.get_TrxName(), "SELECT COUNT(*) FROM C_OrderLine WHERE C_Order_ID=?", order.getC_Order_ID());
Model validators execute synchronously in the save/process transaction. Slow validators will impact user experience and system performance.