9. Lógica de negocio — event handlers, fábricas, callouts y procesos
This content is not available in your language yet.
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.
Por qué no ModelValidator
Sección titulada «Por qué no ModelValidator»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:
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:
@Overrideprotected 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:
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).
3. Procesos: @Process + SvrProcess
Sección titulada «3. Procesos: @Process + SvrProcess»Un proceso ejecutable desde el menú, un botón o la API REST extiende SvrProcess del core:
@Processpublic 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.
Cómo se combinan en la práctica
Sección titulada «Cómo se combinan en la práctica»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).
Fuentes
Sección titulada «Fuentes»finora-lve/plugins/com.finora.lve.base/src/com/finora/lve/base/event/BPartnerTaxIdHandler.javafinora-lve/plugins/com.finora.lve.base/src/com/finora/lve/base/event/fx/DocumentRateFreezer.javafinora-lve/plugins/com.finora.lve.base/src/com/finora/lve/base/MappedFactories.javafinora-lve/plugins/com.finora.lve.base/src/com/finora/lve/base/process/fx/ImportBCVRates.javafinora-lve/decisions/ADR.md, ADR-LVE-004 (event handlers OSGi y fábricas mapped, sinModelValidator)finora-lve/docs/ARCHITECTURE.md, §4.1 a §4.3 (mecanismos, integración con acciones de documento, trampas de runtime)