Add custom action button to the Opportunity screen in SuiteCRM (SugarCRM)

Subject

When working with an opportunity, I would like to change the status of the Opportunity also making some extra logic to execute. For example, when applied online for an opportunity, there is not much information to be added to the opportunity, just to change the status, create a log record (Activity) and to create a follow-up task.

Symptoms

So there are just way too many clicks to be performed if I go the normal way, like edit opportunity first then create an activity and then a task. Look at the picture below, this is what I want to achieve. As you can see there are some more items in this action menu, but I’ll describe only one of them, other ones are built absolutely the same way.

ScreenShot108

Platform/Tools

This is what my SuiteCRM reports: Version 7.1.5,Sugar Version 6.5.20 (Build 1001). I don’t see any reason this would not work in SugarCRM 6.x, but I haven’t tested. And it runs on Ubuntu 12.04.5 LTS.

Solution

Add custom action button to the Opportunity detail view

First step is to add the following code to custom/modules/Opportunities/metadata/detailviewdefs.php as below. This will add an additional item to the action menu on the Opportunity detail page and add the code to execute.

...
        array (
          'buttons' => 
          array (
            0 => 'EDIT',
            1 => 'DUPLICATE',
            2 => 'DELETE',
            3 => 'FIND_DUPLICATES',
            5 => 
            array (
              'customCode' => '<input type="hidden" name="isAppliedOnline">
                               <input title="{$APP.LBL_APPLIED_ONLINE_BUTTON_LABEL}" class="button" onclick="this.form.action.value=\'Save\'; this.form.sales_stage.value=\'Sent\'; this.form.isSaveFromDetailView.value=true; this.form.isAppliedOnline.value=true; this.form.return_module.value=\'Opportunities\'; this.form.return_action.value=\'DetailView\'; this.form.return_id.value=\'{$fields.id.value}\'; this.form.module.value=\'Opportunities\';" name="button_applied_online" value="{$APP.LBL_APPLIED_ONLINE_BUTTON_TITLE}" type="submit">',
            ),
...

We create an additional field sales_stage as a form field and then set it to the needed value and this form will be submitted. Obviously, this field name shall match the object Opportunity field name.

Two additional hidden fields isSaveFromDetailView and isAppliedOnline will help us during Save operation on the Opportunity bean to check if this was AppliedOnline action and perform additional steps, this will be described later.

Last note to that is that if you are going to use more than one button with custom code, hidden fields with the same names have to be placed into a separate section:

...
    'hidden' =>
          array (
            0 => '<input type="hidden" name="isSaveFromDetailView">',
            1 => '<input type="hidden" name="sales_stage">',
          ),
...

Another interesting piece is the localized string {$APP.LBL_APPLIED_ONLINE_BUTTON_LABEL} in the code snippet above. Where does it come from? 

UPDATE: Turned out, there was a mistake in the original version of this article. The right place for global application labels is: custom/Extension/application/Ext/Language/en_us.bandidor.php

...
$app_strings['LBL_APPLIED_ONLINE_BUTTON_TITLE'] = 'Applied online';//this one appears on the action menu
$app_strings['LBL_APPLIED_ONLINE_BUTTON_LABEL'] = 'Applied online label';
...

Quick Repair has to be run after changes to this file.

Create follow-up Task “Review sent opportunity…”

Next idea would be to create a follow-up Task after applying online to have a reminder in let say 14 days. The beauty of it is that this is already there, just check one of my previous articles Opportunities, Tasks and ToDoist for SugarCRM β€” part I. And it not only creates a follow-up Task in the CRM application, it also creates a task in my lovely ToDoIst tool. Wow!

 Log a call

Here is our next challenge. When applying to an opportunity I also used to create an activity (Log a Call) to keep track of my actions related to the opportunity. Can we also do it automatically? Sure we can. Referring to Opportunities, Tasks and ToDoist for SugarCRM β€” part I, we see that there is an asynchronous job to create a new Task, so we just need to extend it to also create a new Call object.

Here is the file where a new job is being created custom/include/Opportunity/SalesStageHooks.php

And here the job itself: custom/Extension/modules/Schedulers/Ext/ScheduledTasks/TaskForOpportunity.php

Checking the existing code we can see that the only parameter being passed to the new job is the bean id, which is not enough to create a Call object when “Applied Online” button is clicked. Let’s refactor the code to also pass a parameter, let’s call it activity, which will indicate the action.

This turned out to be a bit complicated. In the first implementation we create a new job and pass a parameter $data to it, which contains the opportunity id:

...
// First, let's create the new job
$job = new SchedulersJob();
$job->name = "New Task Opportunity  - {$bean->name}";

$job->data = $bean->id;
...

So my first idea was to pass an associative array instead of a string. But this didn’t work, I learned that this parameter is stored in the database in a text field in one single record per job. An alternative solution would be to create separate jobs for Tasks and Calls, but this will make Opportunity Save method more complicated and the execution would be slower. My final solution was to use an associative array as a parameter, but to convert is to a JSON string and also to base64 encode it to avoid conversion of double quotes in a JSON string to HTML codes like &quot;.

Let’s take a look at the job creation code first in the file custom/include/Opportunity/SalesStageHooks.php:

...
$job = new SchedulersJob();
$job->name = "New Task Opportunity  - {$bean->name}";

$attr = array(
	'id' => $bean->id,
	'isSaveFromDetailView' => (isset($_POST['isSaveFromDetailView']) && $_POST['isSaveFromDetailView'] == 'true'),
	'isAppliedOnline' => (isset($_POST['isAppliedOnline']) && $_POST['isAppliedOnline'] == 'true')
);
$job->data = base64_encode(json_encode($attr));
...

Here we create an array $attr with the Opportunity id, create a JSON representation, encode it and then this is used as a new job $data attribute. I also decided to use boolean attributes isSaveFromDetailView and isAppliedOnline exactly as they are being passed from the action button custom code.

Next step is to convert this encoded string back to an object in the job implementation class in the file custom/Extension/modules/Schedulers/Ext/ScheduledTasks/TaskForOpportunity.php:

...
	 public function run($data) {
		$param = json_decode(base64_decode($data),true);
		$GLOBALS['log']->debug("TaskForOpportunity::run: starting for bean id " . $param['id']);
		$opp = BeanFactory::getBean('Opportunities',$param['id']);

...

		//Extending the existing code to also create a "Log a Call" entry if "Applied Online" action button clicked on
		//the Opportunity detail view
		if (self::STATUS_SENT == $opp->sales_stage && $param['isSaveFromDetailView'] && $param['isAppliedOnline']) {
			$GLOBALS['log']->debug("TaskForOpportunity::run: will create a Call...");
			$call = BeanFactory::newBean("Calls");
			$call->assigned_user_id = $opp->assigned_user_id;
			$call->parent_type = "Opportunities";
			$call->parent_id = $opp->id;
			$call->parent_name = $opp->name;
			$call->name = "Applied online";
			$call->date_start = TimeDate::getInstance()->getNow(true)->asDb();
			$call->status = "Applied"; 
			$call->direction = "Outbound";
			$call->type = "online";
			
			$call->save();
			$GLOBALS['log']->debug("TaskForOpportunity::run: new Call saved, id = " . $call->id);
...

Here we create an associative array $param from the encoded string and then use its elements like $param['id'] to control our business logic. We use Opportunity id first and then add some new code to create a Call based on two logical attributes initially used by the action button on the Opportunity detail screen.

Please keep in mind, that after changing the job implementation class in the file custom/Extension/modules/Schedulers/Ext/ScheduledTasks/TaskForOpportunity.php the Repair process has to be run from the application Admin menu.

Locate automatically created Task “Review Opportunity in status “To Note”

Let’s assume we had an Opportunity in status To Note and for this Opportunity, we most probably have a Task, created automatically, which is called something like Review the Opportunity “XXX” in status YYY. When we use the Applied Online action button, our natural expectation would be that this task will be automatically closed if still open.

Let’s try to implement this. It could be a bit tricky, as the only way to search for such a task would be to search by name. Still, we created this task programmatically and its name could be easily reconstructed. We will be again working with the job implementation class in the file custom/Extension/modules/Schedulers/Ext/ScheduledTasks/TaskForOpportunity.php:

...
public function run($data) {
...
		if (self::STATUS_SENT == $opp->sales_stage) {
			$GLOBALS['log']->debug("TaskForOpportunity::run: Opportunity went into status Sent, will close associated ToNote tasks");
			$opp->load_relationships('Tasks');
			$params = array(
				'where' => "tasks.status!='" . self::STATUS_TASK_CLOSED ."' AND tasks.name LIKE '" . substr("Review the Opportunity \"{$opp->name}\" in status ToNote",0,49) . "%'",
				);
			$tasks_tonote = $opp->tasks->getBeans($params);
			if ($tasks_tonote) {
				$GLOBALS['log']->debug("TaskForOpportunity::run: " . count($tasks_tonote) ." open ToNote tasks found, will close them");	
				foreach ($tasks_tonote as $task_tonote) {
					$task_tonote->status = self::STATUS_TASK_CLOSED;
					$task_tonote->save();
					$GLOBALS['log']->debug("TaskForOpportunity::run: Task " . $task_tonote->id . " status updated to " . $task_tonote->status);
				}
			} else {
				$GLOBALS['log']->debug("TaskForOpportunity::run: no open ToNote tasks found.");
			}		
		}
...

Here, in line 9, we create search criteria to load Tasks that are not yet completed and where the name is built the same way we used when we created these tasks programmatically when Opportunity status was set to ToNote. There are examples on The Internet for the where clause in a different form, by providing the where clause as an array with left-hand side and right-hand side field names, but it turned out that it is not possible to use the AND operand in this form.

Also, I used the substr function and the LIKE operator due to the fact that the task name is being truncated when stored in the database; its field length is 50. I thought this logic will still work if this field will be enlarged in the future.

So we select the associated tasks, that we assume were created programmatically and set their status to Completed. That’s it.

Discussion

Probably just one interesting thing, when doing work on the Job implementation class I also spent some time learning the PHPUnit and its use for testing SugarCRM/SuiteCRM. This was useful to some extent, but probably I’ll describe it in one of my future articles.

Caveats

Not sure I saw any worth mentioning. Actually pretty straight forward staff. I’ll update this post if anything comes to my mind.

Comments

comments