Opportunities, Tasks and ToDoist for SugarCRM — part I

Subject

When new Opportunity created or its status changed, new Task to be created and also new Task in ToDoist application to be created.
Optional: when Opportunity is closed, all open Tasks to be closed as well, also on ToDoist.

Symptoms

Certain parts of this logic were already in place, relaying on the Process Manager add-on module. It stopped working after SugarCRM upgrade and then I learned that it is not upgrade safe add-on, so I threw it out in favor of logic hooks.

Platform/Tools

SugarCRM CE patch 6.5.16.

Solution

Sales Stages for Opportunity

 

Value Label Desired action
ToNote To Note Create follow-up Task due in 7 days
Sent Sent Create follow-up Task due in 15 days
InProgress In Progress None
Negotiation/Review Negotiation/Review None
ClosedArchived Closed — Archived Close all open associated Tasks
ClosedNegative Closed — Negative Close all open associated Tasks
ClosedNoanswer Closed — No answer Close all open associated Tasks
ClosedAccepted Closed — Accepted None
Custom drop-down lists are stored in custom/include/language/en_us.lang.php:. This list can be edited via Studio too.
 
...
['app_list_strings']['sales_stage_dom']=array (
'ToNote' => 'To Note',
'Sent' => 'Sent',
'InProgress' => 'In Progress',
'Negotiation/Review' => 'Negotiation/Review',
'ClosedArchived' => 'Closed -- Archived',
'ClosedNegative' => 'Closed -- Negative',
'ClosedNoanswer' => 'Closed -- No answer',
'ClosedAccepted' => 'Closed -- Accepted',
);
...

Logical hooks for Opportunity

Logical hooks for Opportunity will be located in custom/modules/Opportunities/logic_hooks.php. There is already some content in it created by my past implementation with Process Manager, I will kick it out and insert new code. You’ll also remember, of course, that the logic hook file needs to contain:

  • The priority of the business rule
  • The name of the businesses rule
  • The file containing the business rule
  • The business rule class
  • The business rule function

So, here we go:

...
$hook_array = Array();
// position, file, function
$hook_array['before_save'] = Array();
$hook_array['before_save'][] = Array(1, 'Opportunities push feed', 'modules/Opportunities/SugarFeeds/OppFeed.php','OppFeed', 'pushFeed');
$hook_array['before_save'][] = Array(2, 'Create Task Before', 'custom/include/Opportunity/SalesStageHooks.php','SalesStageHooks', 'workflow_before');
$hook_array['before_save'][] = Array(99, 'Opportunities push feed for status change', 'custom/modules/Opportunities/SugarFeeds/OppFeed2.php','OppFeed2', 'pushFeed');
$hook_array['after_ui_frame'] = Array();
$hook_array['after_ui_frame'][] = Array(1, 'Opportunities InsideView frame', 'modules/Connectors/connectors/sources/ext/rest/insideview/InsideViewLogicHook.php','InsideViewLogicHook', 'showFrame');
$hook_array['after_save'] = Array();
$hook_array['after_save'][] = Array(1, 'Create Task After', 'custom/include/Opportunity/SalesStageHooks.php','SalesStageHooks', 'workflow_after');
...

Lines 6 and 11 contain new code. Here SalesStageHooks is a name of the call, which will implement needed logic and workflow_before and workflow_after are the functions to be run on specific events. We will use both before_save and after_save events to implement an advanced logic to work on sales_status changes from certain state to a certain state and to avoid for example firing the workflow, if an opportunity is saved, but without sales_status change.

!!!THIS WAS OVERWRITTEN DURING UPGRADE. TAKE CARE!!!

Logic hooks implementation

In the previous section we specified the hooks, now we will implement hooks logic. This will be in the file custom/include/Opportunity/SalesStageHooks.php.

 
...
class SalesStageHooks {
	const STATUS_TO_NOTE = 'ToNote';
	const STATUS_SENT = 'Sent';
	const STATUS_CLOSED_ARCHIVED = 'ClosedArchived';
	const STATUS_CLOSED_NEGATIVE = 'ClosedNegative';
	const STATUS_CLOSED_NOANSWER = 'ClosedNoanswer';

	protected static $salesStageAction; 

	//Will only set static action flag, real action happens in after workflow
	function workflow_before (&$bean, $event, $arguments) {
		$GLOBALS['log']->debug("SalesStageHooks::workflow_before: called for the bean {$bean->id}, event $event");
		if (!$bean->fetched_row) {
			$GLOBALS['log']->debug("SalesStageHooks::workflow_before: new bean");
			//Will only act on ToNote aand Sent for new beans
			self::$salesStageAction = $bean->sales_stage;
		} else {
			if ($bean->fetched_row['sales_stage'] != $bean->sales_stage) {
				$GLOBALS['log']->debug("SalesStageHooks::workflow_before: changing sales stage from {$bean->fetched_row['sales_stage']} to {$bean->sales_stage}");
				switch ($bean->sales_stage) {
					case self::STATUS_TO_NOTE:
					case self::STATUS_SENT:
					case self::STATUS_CLOSED_ARCHIVED:
					case self::STATUS_CLOSED_NEGATIVE:
					case self::STATUS_CLOSED_NOANSWER:
						self::$salesStageAction = $bean->sales_stage;
						$GLOBALS['log']->debug("SalesStageHooks::workflow_before: salesStageAction set to " . self::$salesStageAction);
				}
			}
		}
	}

	function workflow_after (&$bean, $event, $arguments) {
		$GLOBALS['log']->debug("SalesStageHooks::workflow_after: called for the bean {$bean->id}, event $event, salesStageAction " . self::$salesStageAction);
		if (self::$salesStageAction) {
			$GLOBALS['log']->debug("SalesStageHooks::workflow_after: sales_state_action is " . self::$salesStageAction . " will create a scheduler job");
			...
		}
	}
}
...

In short, we will take a decision about further action in the workflow_before method and take this action in the workflow_after method. Static variable $salesStageAction will be used to pass the next action between the methods. Watch out for using self:: qualifier, when referring to class variables.

Currently we stick to the following logic. For new Opportunities sales_state must be ToNote or Sent to trigger the workflow and for existing ones sales_state has to be changing to ToNote, Sent, ClosedArchived, ClosedNoanswer or ClosedNegative.

Logic hooks – creating new Tasks with Scheduler

We will create tasks asynchronously to make sure Opportunity processing is not delayed. We will be using new Job Queue mechanism. It will be handled by cron in the background.

Setup cron

Pretty easy, details can be checked here

http://support.sugarcrm.com/04_Knowledge_Base/02Administration/100Schedulers/Introduction_to_Cron_Jobs/.

In our case new cron job has to be added for the user running apache web server. It is www-data in ubuntu.

root@www:/var/www/sugar#
root@www:/var/www/sugar# crontab -e -u www-data
...
# m h  dom mon dow   command
* * * * * cd /var/www/sugar && /usr/bin/php -c /etc/php5/apache2/php.ini -f cron.php 2>&1
...
Now it’s time to implement the action.

Scheduler Task implementation

This will be in custom/Extension/modules/Schedulers/Ext/ScheduledTasks/TaskForOpportunity.php. Class TaskForOpportunity will implement RunnableSchedulerJob interface. Processing logic will be in the method run, its argument will be opportunity_id.
...
class TaskForOpportunity implements RunnableSchedulerJob
{
...
    public function setJob(SchedulersJob $job) {
	$this->job = $job;
    }

    public function run($data) {
        ...
    }
}
...
So, our hook logic class will create a new instance of the Scheduler Task class TaskForOpportunity and put it into the job queue:
 
...
class SalesStageHooks {
...
	protected static $salesStageAction;
...
	function workflow_after (&$bean, $event, $arguments) {
		$GLOBALS['log']->debug("SalesStageHooks::workflow_after: called for the bean {$bean->id}, event $event, salesStageAction " . self::$salesStageAction);
		if (self::$salesStageAction) {
			$GLOBALS['log']->debug("SalesStageHooks::workflow_after: sales_state_action is " . self::$salesStageAction . " will create a scheduler job");
			require_once('include/SugarQueue/SugarJobQueue.php');

			// First, let's create the new job
			$job = new SchedulersJob();
			$job->name = "New Task Opportunity  - {$bean->name}";
			$job->data = $bean->id;   // key piece, this is data we are passing to the job that it can use to run it.
			$job->target = "class::TaskForOpportunity";   // class that has the job to run; implements RunnableSchedulerJob
			$job->assigned_user_id = $bean->assigned_user_id;
			// Now push into the queue to run
			$jq = new SugarJobQueue();
			$jobid = $jq->submitJob($job);
		}
	}
}
...

Job queue will be checked by cron every minute and once our task TaskForOpportunity is placed there, its method run() will be executed.

...
class TaskForOpportunity implements RunnableSchedulerJob
{
...
	 public function run($data) {
		$opp = BeanFactory::getBean('Opportunities',$data);
		if (self::STATUS_TO_NOTE == $opp->sales_stage or self::STATUS_SENT == $opp->sales_stage) {
			$task = BeanFactory::newBean("Tasks");
			$task->assigned_user_id = $opp->assigned_user_id;
			$task->parent_type = "Opportunities";
			$task->parent_id = $opp->id;
			switch ($opp->sales_stage) {
				case self::STATUS_TO_NOTE:
					$task->date_due = TimeDate::getInstance()->getNow(true)->modify("+7 days")->asDb();
					$task->name = "Review the Opportunity \"{$opp->name}\" in status {$opp->sales_stage}";
                                        break;
				case self::STATUS_SENT:
					$task->name = "Check feedback for the sent Opportunity \"{$opp->name}\"";
					$task->date_due = TimeDate::getInstance()->getNow(true)->modify("+14 days")->asDb();
			}
			$task->save();
		} else {
			$params = array(
				'where' => array(
					'lhs_field' => 'status',
					'operator' => '!=',
					'rhs_value' => self::STATUS_TASK_CLOSED,
					),
				);
			$opp->load_relationships('Tasks');
			$tasks = $opp->tasks->getBeans($params);
			if ($tasks) {
				foreach ($tasks as $task) {
					$task->status = self::STATUS_TASK_CLOSED;
					$task->save();
				}
			} else {
				$GLOBALS['log']->debug("TaskForOpportunity::run: no open tasks found.");
			}
		}
		return true;
	}
}
...

The logic is as follows. For Opportunities with sales_stage ToNote and Sent we will create new Task and attach it to the opportunity. For closed Opportunities (with the sales_stage ClosedArchived, ClosedNoAnswer, ClosedNegative) we will select existing Tasks that are not yet closed and close them, changing status field to Close.

Few interesting points to note. See lines 14 and 18 for date manipulation, this is how to come to some dates in the future. Also line 30 to select related beans based on a search criteria.

Discussion

Let’s summarize. We created a logical hook to react on changes in Opportunity sales_stage field. We used a combination of before_save and after_save to deal with before and after value of the sales_stage field.

Then the hook logic will create a Scheduler Task and place it into the job queue for execution. Thus we implement an asynchronous handling of the remaining logic and decouple two actions – creating or changing an Opportunity and creation of a new Task related to this Opportunity. Probably we could combine Opportunity and Task creation on one step, but looping through the existing tasks and changing their status to Closed could already delay Opportunity creation.

Third piece of the picture is our Scheduler Task. It implements the logic to create new Task, based on the Opportunity status or to close existing Tasks linked to the parent Opportunity.

In the next article I’ll explain how to duplicate created Tasks to the ToDoist service. Stay tuned 😉

Caveats

First challenge was to run Quick Repair and Rebuild function in SugarCRM every time I changed TaskForOpportunity  class. The reason is that the actual code, which gets executed is in the file custom/modules/Schedulers/Ext/ScheduledTasks/scheduledtasks.ext.php, which is auto-generated. (Compare with custom/Extension/modules/Schedulers/Ext/ScheduledTasks/TaskForOpportunity.php). If you experience ClassNotFound exception, you have not run Quick Repair for the first time.

Another challenge, and this is a real one, is to debug Scheduler Tasks. They are silently run by cron and don’t report errors into sugar log file. To see what’s going on there you have to add a lot of logging statements in the code and also make your cron to report errors and not to kill them via usual 2>&1 in the crontab.

I assume that using some unit testing framework for PHP one can find a way to run these Scheduler Task jobs manually, but this is something for one of my future articles.

Further reading:

Opportunities, Tasks and ToDoist for SugarCRM — part II

Comments

comments

Leave a Reply