Subject
Symptoms
Platform/Tools
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/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.
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
Setup cron
Pretty easy, details can be checked here
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 ...
Scheduler Task implementation
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) { ... } } ...
... 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