Solution for needing an after_save hook that requires using a relationship that isn’t saved yet.
In my case I needed to loop through quote line items each time a quote is created or modified (saved) and add up any Non-Recurring Charges and also populate a text field with all the line items on the order. Sounds simple, but it’s not. The problem is that the line item relationships are saved in aos_products_quotes and you are running the hook from aos_quotes, so you have to load the relationship. In the case of new records, it doesn’t exist yet. With the current record (bean) that you are saving even if it is modified record, the current update is not saved yet to aos_products_quotes. So you can’t really use any kind of save hook for this. You need to schedule a job.
I found the documentation helpful, but not totally everything I needed.
So here goes:
Step1: Create a logic hook to fire the job
create the logic hook in:
/custom/modules//logic_hooks.php
$hook_array['before_save'][] = Array(
77, //order of execution
'MyCustomClass does....', //label for the hook, can be description
'custom/modules/<the module>/NameofFile.php', //location of hook file
'MyCustomClass', //the class of the hook to call
'MyCustomFunction'); // the method, function, in the class to call
Step 2: Create the trigger logic hook function
Create the file in: custom/modules//NameofFile.php as defined in your hook.
In this example logic hook, we will create a new SchedulersJob and submit it to the SugarJobQueue targeting our custom FinancialTotalsCalc that we will create next.
Note: you can run a subsequent function like:
$scheduledJob->target = "function::FinancialTotalsCalc";
Or in the case of a class…
$scheduledJob->target = "class::FinancialTotalsCalc";
In my case I needed a class because I wanted to run sub-routines inside the class.
./custom/modules/AOS_Quotes/QuoteCalculations.php
<?php
if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid EntryPoint');
class CustomQuoteCalc
{
function CalcQuoteTotals(&$bean, $event, $arguments)
{
require_once('include/SugarQueue/SugarJobQueue.php');
// First, let's create the new job
$scheduledJob = new SchedulersJob();
$scheduledJob->name = "Quote Financial Update Job - {$bean->name} {$bean->id}";
$scheduledJob->data = json_encode(array(
'id' => $bean->id,
'module' => $bean->module_name)
);
// key piece, this is data we are passing to the job that it can use to run it.
$scheduledJob->target = "class::FinancialTotalsCalc";
//function to call global
$beanid = $bean->id; //for logging
//$current_user;
//user the job runs as
$scheduledJob->assigned_user_id = '1';
// Now push into the queue to run
$queue = new SugarJobQueue();
if($queue->submitJob($scheduledJob)){
$GLOBALS['log']->fatal("Job has been successfully scheduled: $sheduledJob, $beanid");
}
}
}
Step 3: Create the Scheduler Job
This is the function or class that runs.
Note: file name must be the class or function name!! ./custom/Extension/modules/Schedulers/Ext/ScheduledTasks/QuoteCalculations.php
In this example we are looping through the line items of the quote to do some calculations and then saving the result to the quote.
<?php
class FinancialTotalsCalc implements RunnableSchedulerJob
{
//prevent save loop
static $already_ran = false;
public function run($arguments)
{
//prevent save loop
if(self::$already_ran == true) return true;
self::$already_ran = true;
//Get submitted arguments
$arguments =html_entity_decode($arguments);
$arguments = json_decode($arguments,1);
$bean = BeanFactory::getBean('AOS_Quotes',$arguments['id']);
$GLOBALS['log']->fatal('Arguments passed: ' . print_r($arguments, true));
// Update the record line items summary
$this->generateLineItemSummary($bean);
// Calculate non recurring charge
$this->calculateNRCLineItemsTotal($bean);
// Save the modified quote bean
$bean->save();
//Signify we have successfully ran the cron job
return true;
}
public function setJob(SchedulersJob $job)
{
$this->job = $job;
}
private function generateLineItemSummary($quoteBean) {
// Load the relationship to aos_products_quotes
$quoteBean->load_relationship('aos_products_quotes');
$GLOBALS['log']->fatal('Arguments passed to function: ' . print_r($quoteBean->id, true));
// Get the related beans from the loaded relationship
$productQuotes = $quoteBean->aos_products_quotes->getBeans();
$GLOBALS['log']->fatal('Arguments passed: ' . print_r($productQuotes->id, true));
$lineItemSummary = '';
foreach ($productQuotes as $productQuote) {
$description = $productQuote->description;
// Check if the description field does not contain the word "secondary"
if (stripos($description, 'secondary') === false) {
$productName = $productQuote->name;
$lineItemSummary .= $productName . ': ' . $description . PHP_EOL;
}
}
$quoteBean->line_item_summary_c = $lineItemSummary;
$GLOBALS['log']->fatal("Updated line_item_summary_c: " . $lineItemSummary);
}
private function calculateNRCLineItemsTotal($quoteBean) {
// Load the relationship to aos_products_quotes
$quoteBean->load_relationship('aos_products_quotes');
// Get the related beans from the loaded relationship
$productQuotes = $quoteBean->aos_products_quotes->getBeans();
$nrcLineItemsTotal = 0;
foreach ($productQuotes as $productQuote) {
// Check if the description field does not contain the word "Secondary"
$description = $productQuote->description;
if (stripos($description, 'secondary') === false) {
// Retrieve the related product bean
$productID = $productQuote->product_id;
$productBeanName = $GLOBALS['beanList']['AOS_Products'];
$productBean = new $productBeanName();
$productBean->retrieve($productID);
// Check if the product term is equal to NRC
$term = $productBean->term_c;
if ($term === 'NRC') {
$nrcLineItemsTotal += $productQuote->product_total_price;
}
}
}
$GLOBALS['log']->fatal("Updated nrc_c: " . $nrcLineItemsTotal);
$quoteBean->nrc_c = $nrcLineItemsTotal;
}
}
By using Save() you will initiate this and any other on save hook again. That’s why it’s necessary to control for an infinite loop.
Be careful if you have any other hooks (typically after_save that use the Save() function. It will create infinite loop).
Also note, missing from the documentation is that you need to return the run() function true or you’ll get an error log in SuiteCRM that the cron job has failed. (note I output “fatal” to log to see what’s going on for production you can change the level to debug.