Sales cloud, sincronización de campos custom de la oferta

Cuando se trabaja en la nube de sales, una de las funcionalidades más útiles que tenemos para formalizar una oferta presentada al cliente como la ganadora es la sincronización. Se trata de una funcionalidad que convierte los productos de la oferta en productos de la oportunidad.

En muchas ocasiones los clientes pueden pedirnos que este paso automático se trasladen otros campos que personalizados, por ejemplo un campo que indique si el producto se oferta por una renovación o la duración que tendrá este producto o la cuota de alta si existe.

Para resolver este problema vamos a ver cómo conseguir una sincronización bidireccional para todos los campos custom que necesitemos. Además, este proceso no solo funcionará para una oportunidad cada vez, funcionará para todas las que necesitemos de forma masiva (dentro de los límites de salesforce claro).

Es cierto que existen alternativas a utilizar clases de apex para este proceso, hay una appExchange (según la comunidad actualmente requiere un fix para funcionar en lightning). También se puede realizar a través de Lightning flow o un Process Builder pero no es una solución tan completa.

Cómo funciona realmente la sincronización en Salesforce

Cuando un usuario está gestionando una oportunidad en salesforce puede añadir una serie de productos en la oportunidad, OpportunityLineItems. Según la oportunidad va tomando forma, el usuario presenta varios presupuestos al cliente, Quotes, con diferentes configuraciones. Cuando el cliente le comunica al usuario cual de las ofertas le gusta más esta será la que se sincronice con la oportunidad.

Este es el modelo de datos que maneja salesforce por debajo.

¿Por donde empezamos?

Lo primero es capturar el evento de sincronización. Tras un primer análisis podemos ver que el botón Start Sync desencadena el trigger de Opportunity, concretamente, se rellena el campo “SyncedQuoteId”. La sincronización también desencadena la creación de los OpportunityLineItems asociados a la Oportunidad. Cada vez que empezamos la sincronización de una oferta, salesforce elimina y crea los OpportunityLineItem (OPLI), por lo tanto este será nuestro punto de inicio. Aprovecharemos el evento afterInsert para sincronizar los campos custom.

En el objeto QuoteLineItem (QOLI) existe el campo OpportunityLineItemId, este campo relaciona cada QOLI con un OPLI cuando la oferta está sincronizada. Vamos a crear en QOLI un campo tipo check llamado “IsSyncing” que utilizaremos para desencadenar el trigger de este mismo objeto.

Sabiendo todo esto vamos a empezar a desarrollar el Trigger de OPLI, centrandonos en el afterInsert. Dado que ya los registros disponen de Id, Salesforce ya habrá asociado los productos de la Oferta y la Oportunidad; sin embargo, al ser el evento after no podemos modificar los registros, debemos desencadenar el trigger de QOLI para actualizar los campos.

if(Trigger.isAfter && Trigger.isInsert){
Map<id, OpportunityLineItem> newMap = trigger.newMap;
Set<Id> oppId = new Set<Id>();
Set<Id> SyncQuotes = new Set<Id>();

Seguidamente obtenemos las ofertas de las oportunidades asociadas y posteriormente los QOLIs de las mismas.

for(OpportunityLineItem opli : newMap.values()){
    oppId.add(opli.OpportunityId);            
}
// Get SyncedQuotes Quotes
List<Opportunity> opps = [Select id , SyncedQuoteId
                    	From Opportunity 
                        where id =: oppId];
for(Opportunity opp : opps){
    SyncQuotes.add(opp.SyncedQuoteId);      
}
        
// Get QOLIs
List<QuoteLineItem> qolis = [Select id, IsSyncing__c
                             from QuoteLineItem
                             where QuoteId =: SyncQuotes];

Aquí aprovechamos el campo IsSyncing antes mencionado cambiandolo a True. Esto provocará la ejecución del trigger de QOLI. Por útlimo desencadenamos el Trigger de QOLI lanzando el update.

// Set IsSyncing true
for(QuoteLineItem q : qolis){
      q.IsSyncing__c = true;
}
update qolis;
}

 

Sincronizando los campos

Pasamos ahora al trigger de QOLI que acabamos de desencadenar. Utilizaremos el evento AfterUpdate ya que vamos a actulizar campos ajenos a la entidad.

Aquí empieza a complicarse la situación. El objetivo es crear un mapa que relacione el id del QOLI con el objeto OPLI asociado.

Arrancamos:

Lo primero es preparar una serie de variables y obtener los Id de aquellos productos que antes no estaban sincronizando y ahora si. Esto es muy importante.

Map<Id, QuoteLineItem> newMap = Trigger.newMap;
Map<Id, QuoteLineItem> oldMap = Trigger.oldMap;

Set<Id> setOplis = new Set<Id>();
List<QuoteLineItem> setQolis = new List<QuoteLineItem>();

// Get OPLIs being syncronized
for (Id i : newMap.keySet()){
    QuoteLineItem newQ = newMap.get(i);
    QuoteLineItem oldQ = oldMap.get(i);
    if(newQ.IsSyncing__c && !oldQ.IsSyncing__c){
        setOplis.add(newQ.OpportunityLineItemId);
        setQolis.add(newQ);
    }
}

Lanzamos la query para obtener los OPLI con los campos custom a sincronizar y generarmos un mapa.

// Get OPLIs
List <OpportunityLineItem> opliList =  [Select id, Renewal__c
    From OpportunityLineItem
    Where id =: setOplis];
Map<Id, OpportunityLineItem> opliMap = new Map<Id, OpportunityLineItem>();
for(OpportunityLineItem o : opliList){
	opliMap.put(o.id, o);
}

Relacionamos los productos de la oferta y la oportunidad y llamamos al método que sincroniza los campos customizados.

// Relate QOLIs with OPLIs
List <OpportunityLineItem> opliListUpdate = new List <OpportunityLineItem>();
// Relate QOLIs with OPLIs
List <OpportunityLineItem> opliListUpdate = new List <OpportunityLineItem>();
Map<Id, OpportunityLineItem> ql_opl = new Map<Id, OpportunityLineItem>();
for(QuoteLineItem q : setQolis){
	ql_opl.put(q.ID, opliMap.get(q.OpportunityLineItemId));
	opliListUpdate.add(updateCustomFields(q, opliMap.get(q.OpportunityLineItemId)));
}

El método en si no es complicado, simplemente asegura que los valores son iguales:

private OpportunityLineItem updateCustomFields (QuoteLineItem qoli, OpportunityLineItem opli){
   if(qoli.Renewal__c != opli.Renewal__c){
       opli.Renewal__c = qoli.Renewal__c;
   }
   return opli;
}

Para finalizar actualizamos los productos de la oportunidad.

if(opliListUpdate.size()>0){
    update opliListUpdate;       
}

Ahora podemos comprobar que los campos de OPLI y QOLI está alineados, tanto los estandar como los custom.

Manteniendo la sincronización

Llegados a este punto tenemos que mantener la consistencia del dato entre los Productos de la oportunidad y de la oferta mientras se mantenga la sincronización.

La solución lógica es en el Trigger de OPLI en el evento after update, buscar aquellos QOLIs cuyo campo OpportunityLineItemId contiene el id del registro que está siendo utilizado ¿no? Es lo más sencillo.

Tenemos un problema, salesforce no nos permite lanzar esta sentencia de Base de Datos:

SELECT OpportunityLineItemId FROM QuoteLineItem WHERE OpportunityLineItemId = 'ID de Opportunity Line Item'

Por desgracia, esto siempre va a devolver 0 registros. Por lo tanto debemos ir por una solución algo más elaborada.

Buscando los Productos de la oferta

Empezaremos por el trigger de OPLi en el evento after update. Los pasos a seguir serán los siguientes:

  • Encontrar los OpportunityLineItem que se están actulizando pertenecientes a una oportunidad sincronizada.
  • Obtener los QouteLineItems pertenecientes a estas oportunidades.
  • Hacer match entre los OpportunityLineItems y los QouteLineItems recien obtenidos.
  • Actulizar los datos y actualizar los registros.

En el trigger de OPLI empezamos obteniendo aquellas oportunidades que tienen un Oferta sincronizada.

Map<id, OpportunityLineItem> newMap = trigger.newMap;
Set<Id> oppId = new Set<Id>();

// Get opportunities
for(OpportunityLineItem opli : newMap.values()){
    oppId.add(opli.OpportunityId);            
}
List<Opportunity> opps = [Select id, SyncedQuoteId
                    		From Opportunity 
                    		where id =: oppId 
                            and SyncedQuoteId != null];
Set<Id> QuoteIdSet = new Set<Id>();
// Get Ids
for(Opportunity opp : opps){
    quoteIdSet.add(opp.SyncedQuoteId);

 

Hemos obtenido el Set que contiene las ofertas que están sincronizadas.

Vamos entonces a obtener todos los productos de estas ofertas. Además tenemos que obtener todos los campos que queremos sincronizar y los campos estandar. La razón es que al hacer este mecanismo estamos “anulando” en algunas ocasiones el funcionamiento estandar de sincronización. Estos solo ocurre cuando modificamos un campo estandar y un campo custom a la vez. Algo curioso pero que tenemos que contemplar.

// Get QOLIs
List<QuoteLineItem> qoliList = [Select id, OpportunityLineItemId, Renewal__c, Quantity, Description, UnitPrice, Discount 
                                from QuoteLineItem
                                where QuoteId =: quoteIdSet.values()];

Map<id, QuoteLineItem> qoliMap = new Map<id, QuoteLineItem>();
for(QuoteLineItem qoli : qoliList){
    qoliMap.put(qoli.OpportunityLineItemId, qoli);

Acabamos de conseguir un mapa donde en el key tenemos el OpportunityLineItemId y el QOLI asociado. A continuación, vamos a recorrer los registros del Trigger y comprobar si id del registro está en el Mapa recien obtenido.

List <QuoteLineItem> qolisUpdate = new List<QuoteLineItem>();
for(OpportunityLineItem opli : newMap.values()){
    if(qoliMap.get(opli.id)!=null){
        QuoteLineItem qoli1 = updateCustomFields(qoliMap.get(opli.id), opli);
    	if(qoli1 != null){
        	qolisUpdate.add(qoli1);
    	}
    } 
}
if(qolisUpdate.size()>0){
    update qolisUpdate;
}

Finalmente hemos actualizado los Productos de la oferta. Con esto podemos actualizar los prodcutos de la oportunidad y el cambio se reflejará en el producto de la oferta, pero no viceversa.

Para que en el momento de actualizar los productos de la oferta el cambio se refleje en los de la oportunidad solo tenemos que cambiar algunas líneas de código.

En el trigger de QOLI modificamos esta sentencia if:

// Get OPLIs being syncronized
for (Id i : newMap.keySet()){
    QuoteLineItem newQ = newMap.get(i);
    //QuoteLineItem oldQ = oldMap.get(i);
    if(newQ.IsSyncing__c /*&& !oldQ.IsSyncing__c*/){
        setOplis.add(newQ.OpportunityLineItemId);
        setQolis.add(newQ);
    }
}

Además de esto añadimos los campos custom a la query que recoje los OpportunityLineItems y modificamos el método updateCustomFields para que compruebe todos los campos.

Des-sincronizando la oferta

Ya casi hemos terminado solo nos queda deshacer el proceso de sincronización para que deje de ejecutarse.

Para esto solo es necesario modificar el trigger de Oportunidad. Como comentabamos al inicio, el trigger de Oportunidad es donde se detecta en primera instancia la sincronización y para el proceso de desincronización haremos exactamente lo mismo.

Recogeremos en el trigger todas las oportunidades que se están des-sincronizando.

Map<id, Opportunity> newMap = trigger.newMap;
Map<id, Opportunity> oldMap = trigger.oldMap;

Set<Id> quoteIds = new Set<Id>();
//getSyncedQuotes
for(Id i : newMap.keySet()){
	Opportunity newOpp = newMap.get(i);
	Opportunity oldOpp = oldMap.get(i);
	if(newOpp.SyncedQuoteId==null && oldOpp.SyncedQuoteId!=null){
		quoteIds.add(newOpp.SyncedQuoteId);
		
	} 
}

// Get QuoteLineItems to be updated
List<QuoteLineItem> qoliList = [Select id,
									IsSyncing__c
								from QuoteLineItem 
								where QuoteId =: quoteIds];

for(QuoteLineItem qoli : qoliList){
	qoli.IsSyncing__c = false;
}    
update qoliList; 

 

Puedes consultar todo el código utilizando en el repositorio de github