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.
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.
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;
}
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.
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.
Empezaremos por el trigger de OPLi en el evento after update. Los pasos a seguir serán los siguientes:
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.
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