After Save Hook relationship aos_products_quotes

I’ve got an after save hook setup to go through the line items and do some calculations based on the type of products in the line items.

It works great when existing records are edited, but when creating a new quote, the aos_product_quotes bean is empty. I thought that using an after save hook instead of a before save hook would solve this problem. Anyone have any tips? My next thought if I can’t get this to work is do a scheduled job in the scheduler.

here’s what I’m trying to do (and it works great for already existing quotes:

private function updateTotals($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();

    $totalPrice = 0;
    $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;
            } else {
                $productTotalPrice = $productQuote->product_total_price;
                $totalPrice += $productTotalPrice;
                $GLOBALS['log']->debug("Product Total Price: " . $productTotalPrice);
            }
        }
    }

		$quoteBean->total_price_c = $totalPrice;
		$quoteBean->nrc_c = $nrcLineItemsTotal;

Hi Paul

This one isn’t going to be easy, but…

I would try to understand the order in which logic hooks are getting called, specifically the one creating the relationship, and the one creating the quote bean. You can just set breakpoints in the debugger for this.

And then look closely at the call stack. This will give you clues about which ones are getting called “inside” the other. When a bean saves, often there is additional work going on, either in logic hooks or in method overrides of the bean “save” function. One of these things is doing (afterwards) the work you wish was being done before. You might be able to just call into that code yourself earlier - but you have to understand the full flow for that.

I’m not sure this helps, or complicates things :man_shrugging: :sweat_smile:

Thanks @pgr I fought with it all day yesterday with no luck. Before save actually works better than after save, but neither are able to deal with new records. I’m going to try the scheduled job route see if that helps. I tried a custom task to run in the scheduler, but got stuck on that it runs as a method and I couldn’t include classes to be able to run sub routines as it looped through the line items, so I’m going to try a scheduled job, in the documentation it appears I can add a class, so I can add all my sub routines as it loops through the line items. I’m hoping I can trigger it on new and modified records. I’ll post the solution if I can get there. I’m thinking it should work because all the related records will definitely be saved by the time the scheduled job runs.

1 Like

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.

1 Like

Just a curiosity here an alternative is to use Workflows, you would still need a custom action, but you trigger on New/modified records and only run in the scheduler. This would mean everything gets saved then the next time the scheduler runs your action runs as well.

Mark

Hey Mark, in this case the logic is just too complex for workflow. Although you’re right it would solve the fundamental problem of having it run in the scheduler

Thats why I say create a custom action, it allows you whatever logic you want, plus allows the workflow to be enabled/disabled, logging etc all from th GUI.

Regards

Mark

When you say “custom action” do you mean a scheduled action? I tried that and it runs inside a class, so I can only use functions. I wanted to to run the whole thing in it’s own class with subroutines in private functions. That’s why I had to go to custom scheduled job.

No I mean in workflows there are 4 standard actions (Create Record/Modify Record/Send Email/Calculate Fields). you can add to this list with code to undertake new actions, they are effectively Classes with subroutines etc dropped into custom/modules/AOW_Actions/actions, the benefit is it allows you to trigger them in the cron on new/modified records with logging and the ability to enable/disable from the GUI etc.

Nothing wrong with pure code/Cron jobs etc I just prefer to have the ability for SysAdmins to do more fault checking etc by themselves pus it gives visiblity as to where things are happening.

Mark

Hmm, interesting. Now I understand what you’re saying. I didn’t know that was possible. Is there any docs or tutorials you can point me to? That sounds interesting.

Go here

and search for Custom actions in Workflows

Wow didn’t even know that existied. Thats awesome. Glad to see one of my posts made the list.! Thanks for sharing @pgr