Ir al contenido
FINORA Academy
SistemaFINORA ERP

9. Lógica de negocio — event handlers, fábricas, callouts y procesos

Avanzado20 minVerificado el 25 sept. 2026

iDempiere ofrece varios mecanismos para engancharse al ciclo de vida de un registro o de un documento sin modificar la clase que lo implementa. FINORA LVE usa cuatro de ellos, todos registrados como componentes OSGi (ver Tutorial 6), y deliberadamente evita un quinto que el core también ofrece: el ModelValidator clásico.

El ModelValidator clásico de iDempiere exige registrarse por tenant en la tabla AD_ModelValidator — es decir, requiere un paso de configuración de datos además de instalar el plugin. FINORA LVE prefiere mecanismos que se activan solo con que el bundle esté instalado, sin depender de que alguien lo dé de alta en cada tenant después (finora-lve/decisions/ADR.md, ADR-LVE-004).

1. Event handlers: reaccionar a un evento de PO o de documento

Sección titulada «1. Event handlers: reaccionar a un evento de PO o de documento»

AbstractEventHandler (del core, org.adempiere.base.event) permite suscribirse a temas como “antes de guardar”, “antes de completar un documento” o “después de completarlo” (IEventTopics.PO_BEFORE_NEW, DOC_BEFORE_COMPLETE, DOC_AFTER_COMPLETE, etc.). Es el mecanismo más usado en FINORA LVE. Ejemplo real, simplificado — valida el RIF de un tercero al guardarlo:

finora-lve/plugins/com.finora.lve.base/src/com/finora/lve/base/event/BPartnerTaxIdHandler.java
public class BPartnerTaxIdHandler extends AbstractEventHandler {
@Override
protected void initialize() {
registerTableEvent(IEventTopics.PO_BEFORE_NEW, I_C_BPartner.Table_Name);
registerTableEvent(IEventTopics.PO_BEFORE_CHANGE, I_C_BPartner.Table_Name);
}
@Override
protected void doHandleEvent(Event event) {
PO po = getPO(event);
if (!FveConfig.getBoolean(FveSysConfig.RIF_VALIDATION, false, po.getAD_Client_ID(), po.getAD_Org_ID()))
return; // apagado por defecto: se activa por tenant/organización
TaxIdPolicy.Assessment a = TaxIdPolicy.evaluate(/* ...datos del PO... */);
if (!a.isValid()) {
addErrorMessage(event, String.join("; ", a.errors())); // rechaza el guardado
return;
}
if (a.normalizedTaxId() != null)
po.set_ValueOfColumn(I_C_BPartner.COLUMNNAME_TaxID, a.normalizedTaxId());
}
}

Tres cosas a notar:

  • initialize() declara a qué eventos se suscribe — se puede registrar a varias tablas y varios temas en el mismo handler.
  • getPO(event) obtiene el registro que disparó el evento; los cambios que hace sobre ese objeto (set_ValueOfColumn) quedan reflejados cuando el core continúe el guardado.
  • addErrorMessage(event, ...) rechaza la operación completa — el mismo mecanismo que usa el core para sus propias validaciones, así que el mensaje aparece igual en ZK y como error en la API REST (ver Tutorial 10).

Un segundo ejemplo, más elaborado, corre en dos momentos del ciclo de un documento (preparar y completar) para congelar la tasa de cambio BCV:

finora-lve/plugins/com.finora.lve.base/src/com/finora/lve/base/event/fx/DocumentRateFreezer.java
@Override
protected void initialize() {
for (String topic : new String[] {IEventTopics.DOC_BEFORE_PREPARE, IEventTopics.DOC_BEFORE_COMPLETE}) {
registerTableEvent(topic, MInvoice.Table_Name);
registerTableEvent(topic, MPayment.Table_Name);
}
}

2. Fábricas mapped: registrar modelos, procesos y callouts por anotación

Sección titulada «2. Fábricas mapped: registrar modelos, procesos y callouts por anotación»

En vez de una lista escrita a mano de clases a registrar, FINORA usa las fábricas mapped del core (IMappedModelFactory, IMappedProcessFactory, IMappedColumnCalloutFactory), que escanean un paquete en busca de clases con una anotación y las registran automáticamente:

finora-lve/plugins/com.finora.lve.base/src/com/finora/lve/base/MappedFactories.java
public void activate(BundleContext context) {
mappedModelFactory.scan(context, new String[] { "com.finora.lve.base.model" }); // clases @Model
mappedProcessFactory.scan(context, new String[] { "com.finora.lve.base.process.fx" }); // clases @Process
mappedColumnCalloutFactory.scan(context, new String[] { "com.finora.lve.base.callout" }); // clases @Callout
}

Esto es lo que evita que dar de alta un proceso nuevo signifique editar un MANIFEST.MF o una lista central: basta con poner la clase en el paquete correcto con la anotación correcta, y el próximo arranque del bundle la registra (ver el ejemplo completo de componente DS en el Tutorial 6).

Un proceso ejecutable desde el menú, un botón o la API REST extiende SvrProcess del core:

finora-lve/plugins/com.finora.lve.base/src/com/finora/lve/base/process/fx/ImportBCVRates.java
@Process
public class ImportBCVRates extends SvrProcess {
private String sourceType = "BCV_HOME";
@Override
protected void prepare() {
for (ProcessInfoParameter p : getParameter()) {
if ("FVE_RateSourceType".equals(p.getParameterName()))
sourceType = p.getParameterAsString();
}
}
@Override
protected String doIt() throws Exception {
// ... lee la fuente, guarda tasas, cuenta resultados ...
return "Tasas BCV: " + rates.size() + " leídas; " + counts;
}
}

prepare() lee los parámetros que la persona (o la llamada REST) pasó al lanzar el proceso; doIt() hace el trabajo y devuelve el texto de resumen que se muestra al terminar. El mismo proceso puede lanzarse desde ZK, desde el menú, o con POST /processes/fve_importbcvrates por REST — es una sola implementación (ver Tutorial 10, §6.1 sobre parámetros de rango).

4. Callouts: reaccionar a un cambio de campo en pantalla

Sección titulada «4. Callouts: reaccionar a un cambio de campo en pantalla»

Un callout (@Callout, IColumnCallout) corre cuando una persona cambia un campo específico en una ventana — por ejemplo, proponer un porcentaje de retención en cuanto se elige el proveedor en una factura. FINORA LVE los registra con IMappedColumnCalloutFactory, igual que los modelos y procesos.

El flujo real de una factura de compra con retención usa los cuatro mecanismos juntos: un callout sugiere el tipo de retención al elegir el proveedor; un event handler en DOC_BEFORE_PREPARE calcula y muestra el borrador de retención; otro en DOC_AFTER_COMPLETE la materializa como un documento C_Invoice de tipo comprobante; y un proceso por lotes (FVE_ExportIVAWithholdingTXT) exporta el archivo quincenal para el SENIAT. Cada pieza es independiente y se prueba por separado (finora-lve/docs/ARCHITECTURE.md, §5.3).

  • finora-lve/plugins/com.finora.lve.base/src/com/finora/lve/base/event/BPartnerTaxIdHandler.java
  • finora-lve/plugins/com.finora.lve.base/src/com/finora/lve/base/event/fx/DocumentRateFreezer.java
  • finora-lve/plugins/com.finora.lve.base/src/com/finora/lve/base/MappedFactories.java
  • finora-lve/plugins/com.finora.lve.base/src/com/finora/lve/base/process/fx/ImportBCVRates.java
  • finora-lve/decisions/ADR.md, ADR-LVE-004 (event handlers OSGi y fábricas mapped, sin ModelValidator)
  • finora-lve/docs/ARCHITECTURE.md, §4.1 a §4.3 (mecanismos, integración con acciones de documento, trampas de runtime)