An example scripted Workflow transition: How to configure Codebeamer to synchronize Leads tracker with others using a custom Workflow action
The requirement: Synchronizing various trackers on a Workflow transition
The Sales Template project contains the following trackers:
• Leads - An item representing a prospective customer that is created when an individual or business shows interest and provides his or her contact information.
• Accounts - The Account, along with contacts as related records, is like an address book. It is an entity to store a company’s name, address, phone number and other important pieces of information.
• Opportunities - An Opportunity represents a potential sale to a new or established customer. Helps us forecast future business demands and sales revenues.
• Concats - An individual’s personal information by which we can reach the individual to discuss their needs & what we can offer.
• Activities - An Activity is basically a record of actions undertaken by our sales team and other stakeholders.
The goal is that when a Lead finalizes, i.e. it changes to the "SQL" workflow state then we should create appropriate items in the Accounts, Opportunities, Contants trackers.
This diagram illustrates how these trackers relate to each other.
Synchronization mapping between trackers
Source Tracker fields | Mapping to... | |
---|
Lead | Contact | Account | Opportunity |
---|
Description | | Company Profile | |
First name | First name | | |
Last name | Last name | | |
Title | Title | | |
Department | Department | | |
Company | | Account name | |
Phone | Phone | Phone | |
E-mail contact | E-mail | | |
Street | Street | Street | |
Post code | Zip/Postal code | Post code | |
City | City | City | |
Country | Country | Country | |
Employees | | Employees | |
Website | | Company website | |
Lead source | | | Lead source, Opportunity, Description |
Comment | | Comment | |
Geolocation | Geolocation | | |
Industry | | Industry | |
| Account should refer the new "Account" | | Account should refer the new "Account". |
| | | Contacts should refer to the new "Contact". |
Here is the required mapping of fields. The Workflow should copy these fields from the "Leads" tracker to the fields of the other tracker as described here.
The script code
Here is the Groovy script, which contains the logic for the Workflow-action. For that you should create a new file as $CB_HOME/CB/tomcat/webapps/cb/WEB-INF/classes/synchronizeLeads.groovy. Paste this Groovy script below to that file, and save it.
// Groovy script implements a Workflow state-transition action as requested here: https://codebeamer.com/cb/issue/326159
// registered in my-applicationContext.xml on cb.com only
import com.intland.codebeamer.persistence.dto.*;
import com.intland.codebeamer.persistence.dto.base.*;
import com.intland.codebeamer.persistence.dao.*;
import com.intland.codebeamer.manager.*;
import com.intland.codebeamer.controller.importexport.*;
import org.apache.commons.lang3.*;
if (!beforeEvent) {
return; // do NOTHING on after-event, everything is already handled in the before-event!
}
logger.info("-------------------------------------");
logger.info("Synchronizing Lead issue:" + subject);
trackerDao = applicationContext.getBean(TrackerDao.class);
trackerItemManager = applicationContext.getBean(TrackerItemManager.class);
projectId = subject.tracker.project.id
// read custom fields in "Leads" tracker
// using a helper class to access fields by name
fieldAccessor = new com.intland.codebeamer.text.excel.FieldAccessor(applicationContext);
fieldAccessor.setUser(user);
def getByLabel = { fieldName -> fieldAccessor.getByLabel(subject, fieldName) };
// set a field on contact by finding the field using its label
def setField(issue, fieldName, value) {
field = fieldAccessor.getFieldByName(issue, fieldName);
if (field != null) {
field.setValue(issue, value);
} else {
logger.warn("Can not find field <" + fieldName +"> on " + issue);
}
};
def copyField(toIssue, fieldName, toFieldName=null, defaultValue=null) {
value = fieldAccessor.getByLabel(subject, fieldName);
if (value == null && defaultValue != null) {
value = defaultValue;
}
if (toFieldName == null) {
toFieldName = fieldName;
}
setField(toIssue, toFieldName, value);
}
def updateOriginal(fieldName, value) {
setField(subject, fieldName, value);
}
def getOrCreateChoice(issue, fieldName, value) {
if (StringUtils.isBlank(value)) {
return null;
}
choicesProvider = new ChoicesProvider(applicationContext);
choiceField = choicesProvider.getFieldByName(user, issue, fieldName);
if (choiceField == null) {
return null;
}
asChoice = choicesProvider.getOrCreateChoiceByName(user, issue.tracker, choiceField, value, null);
return asChoice;
}
// first check/create account if does not exist, because this is required by contact
account = getByLabel("Account");
if (account == null) {
try {
account = new TrackerItemDto();
accounts = trackerDao.findByNameAndProjectId("Accounts", projectId);
account.tracker = accounts;
// use the trackerItemManager to copy of the source issue, because this copies comments and attachments too
request = event.getRequest();
fieldMapping = new HashMap();
Map<TrackerItemDto,TrackerItemDto> copied = trackerItemManager.copy(request, user, Collections.singletonList(subject), null, account, null, fieldMapping);
account = copied.get(subject);
logger.info("copied account's id:" + account.id);
// required fields
copyField(account, "Company", "Account Name");
copyField(account, "Description", "Company Profile"); // must fill with something, this is a required field
copyField(account, "Street");
copyField(account, "Post code");
copyField(account, "City");
copyField(account, "Country");
copyField(account, "Employees");
copyField(account, "Website", "Company website");
copyField(account, "Comment");
copyField(account, "Phone");
// industry is a choice field in the target, creating a new choice if necessary
industry = getByLabel("Industry");
logger.warn("Creating industry:" + industry)
industryAsChoice = getOrCreateChoice(account, "Industry", industry);
logger.warn("industryAsChoice:" + industryAsChoice);
if (industryAsChoice != null) {
setField(account, "Industry", Arrays.asList(industryAsChoice));
}
logger.info("Created Account:" + account);
// trackerItemManager.create(user, account, event.getData());
trackerItemManager.update(user, account, event.getData());
updateOriginal("Account", Arrays.asList(account));
} catch (Throwable th) {
logger.warn("Failed to create Account for " + subject, th);
}
}
// only create a new contact if that does not exist yet!, this also avoids infinite event loops !
contact = getByLabel("contact");
if (contact == null) {
try {
// create a new Contact
contact = new TrackerItemDto();
contacts = trackerDao.findByNameAndProjectId("Contacts", projectId);
contact.tracker = contacts;
copyField(contact, "First Name");
copyField(contact, "Last Name");
copyField(contact, "Title"); // TODO: there is NO such field here
copyField(contact, "Department"); // TODO: there is NO such field here
copyField(contact,"Phone");
copyField(contact,"E-mail", "Email");
copyField(contact,"Street");
copyField(contact,"Post code", "Zip/Postal code");
copyField(contact,"City");
copyField(contact,"Country");
copyField(contact, "Geolocation"); // TODO: no such field!
// fill the Mandatory Account field
setField(contact, "Account", Arrays.asList(account));
trackerItemManager.create(user, contact, event.getData());
logger.info("Created Contact:" + contact);
updateOriginal("Contact", Arrays.asList(contact));
} catch (Throwable th) {
logger.warn("Failed to create Contact for " + subject, th);
}
}
try {
// Account - Main Contact field should have a default value for Contact person created upon conversion
mainContact = fieldAccessor.getByLabel(account, "Main Contact");
if (mainContact == null || mainContact.isEmpty()) {
setField(account, "Main Contact", Arrays.asList(contact));
trackerItemManager.update(user, account, event.getData());
}
} catch (Throwable th) {
logger.warn("Failed to set Main Contanct field", th);
}
opportunity = getByLabel("opportunity");
if (opportunity == null) {
try {
opportunities = trackerDao.findByNameAndProjectId("Opportunities", projectId);
// create a new opportunity
opportunity = new TrackerItemDto();
opportunity.tracker = opportunities;
defaultVal = getByLabel("Account"); // use this as default value if the "opportunity" field would be empty, because this is a required field
opportunity.name = account.name; // REQUIRED field
opportunity.description = "--"; // REQUIRED field
// copyField(opportunity, "Lead Source", "Opportunity", defaultVal); // REQUIRED field
// copyField(opportunity, "Lead Source", "Description", defaultVal); // REQUIRED field
copyField(opportunity, "Lead Source");
setField(opportunity, "Account", Arrays.asList(account));
// store the "Account" to the "Contacts" table
opportunityTables = new com.intland.codebeamer.manager.trackeritems.TableFields(user, opportunity, applicationContext);
contactsTable = opportunityTables.getTableByName("Contacts");
contactColumn = contactsTable.getTableColumnByName("Contact");
contactColumn.setReferenceValues(0, Arrays.asList(contact));
trackerItemManager.create(user, opportunity, event.getData());
logger.info("Created Opportunity:" + opportunity);
updateOriginal("opportunity", Arrays.asList(opportunity));
} catch (Throwable th) {
logger.warn("Failed to create Opportunity for " + subject, th);
}
}
logger.info("-------------------------------------");