How to prevent multiple copies when retrieving records in after-save logic hook

In an 'after save' logic hook, there is a process that checks for any records that match the ID of the Opportunity, and the Special Key of the Revenue Line Item to match the same values in the Quoted Line Item (Special Key - QLI_key_c - is the ID of the Quoted Line Item). If none exist, then the code will initiate the creation of a new Revenue Line Item. What is happening is the code runs multiple times, then is halted by Sugar's own processes.

I tried using two different methods to prevent multiple runs of the code after the initial action, but none were successful. And 'save()' must be used to store the data in this 'after save' logic hook. 

Is there a way using the code to prevent the loops that create extra copies?

[Must include that a separate JS file has deleted previous Revenue Line Items, and then this logic hook should add new ones from a new set of Quoted Line Items.]

<?php

    if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');

    class QuotedLineConvert
    {
        static $already_ran = false;

        function saveQuotedLineItems($bean, $event, $arguments)
        {
            if(self::$already_ran == true) return;
            self::$already_ran = true;

            // if (!isset($bean->ignore_update_c) || $bean->ignore_update_c === false)
            {
                //  get the Quotes related to the Opportunities
                $oppquo = 'opportunities_qm_quotes_module_1';
                if($bean->load_relationship($oppquo)){
                    $quotes = $bean->{$oppquo}->getBeans();
                    foreach($quotes as $quo){

                    //  get the Quoted Line Items ($qli) in each Quote
                    $quoqli = 'qm_quotes_module_qli_quoted_line_items_1';
                    if($quo->load_relationship($quoqli)){
                        $qlines = $quo->{$quoqli}->getBeans();
                        foreach($qlines as $qli){
                            
                            //  establish Revenue Line Item ($rli) for each $qli and add values to the $rli
                            $newrli = $this->getRLILinks($bean, $qli->id);
                            if(!$newrli){
                                continue;
                            }
                            $newrli->sales_stage        = 'Proposal/Price Quote';
                            $newrli->qli_key_id_c       = $qli->id;
                            $newrli->name               = $qli->name;
                            $newrli->item_no_c          = $qli->inventory_id_c;
                            $newrli->discount_price     = $qli->discounted_unit_price_c;
                            $newrli->requested_on_c     = $qli->requested_onn_c;
                            $newrli->quantity           = $qli->order_qty_c;
                            $newrli->likely_case        = ($qli->discounted_unit_price_c * $qli->order_qty_c);
                            $newrli->assigned_user_id   = $qli->assigned_user_id;
                            $newrli->opportunity_id     = $bean->id;
                            $bean->assigned_user_id     = $qli->assigned_user_id;
                            $rlidate                    = $quo->quote_expiration_date_c;
                            $rlidate                    = date('Y-m-d',strtotime('+30 days',strtotime($rlidate)));
                            $newrli->date_closed        = $rlidate;
                                                
                            // add relationship between $rli and $qli
                            $revquo = 'revenuelineitems_qli_quoted_line_items_1';
                            $newrli->load_relationship($revquo);
                            $newrli->{$revquo}->add($qli->id);
                            $newrli->save();
                            }
                        }
                    }
                }
            }
        }
        //  find any links the $rli to the $qli using 'qli_key_c' and create the new $rli if none exist.
        public function getRLILinks($bean, $qli_id)
        {
            $sugarQuery = new SugarQuery();
            $sugarQuery->from(BeanFactory::getBean('RevenueLineItems'));
            $sugarQuery->select(['id']);
            $sugarQuery->where()
                ->equals('qli_key_id_c', $qli_id)
                ->equals('opportunity_id', $bean->id);
            $result = $sugarQuery->getOne();
            if($result){
                return false;
            }else{
                $newrli = BeanFactory::newBean('RevenueLineItems');
                    return $newrli;
            }
        }
    }
?>

  • Just to update, it is running on both the chosen Quote and the Quote not chosen from the subpanel rowaction.

  • It seems you are running such after_save LogicHook on Opportunities. From inside this Hook you fetch all related Quotes and then all related QLIs. Finally you look for a RLI related to that QLI and create a new one if it is missing. Correct?

    The big deal is: On saving a RLI it will invoke Opportunity.save() which will run again you LogicHook which will create a RLI and then run again the Opportunity and so on.

    I would ask in which specific scenario should that LogicHook to run?

    Once you identify that specific scenario I would suggest you to turn that LogicHook into a before_save instead and then update a flag in that Opportunity, so you can evaluate that flag from insde the LogicHook, this way it will be pretty easy to avoid looping it.

    Based on that specific scenario you need to figure out, you can decide whether to turn that flag so the LogicHook can be invoked accordingly.

    I hope that helps you.

    André Lopes
    Lampada Global
    Skype: andre.lampada
  • The customizations are happening after a javascript button is pressed. So, it would be assumed that after the button changes the saved record that the actions would take place after it saves? or is that also a weak assessment?

    Here is that JS code:

    ({
        extendsFrom: 'SubpanelListView',
        
        initialize: function(options) {
            this._super('initialize', [options]);
            this.context.on('button:replace-quote:click', this.replaceQuote, this);
            },
    
        replaceQuote: function(model) {
            var self = this;
            debugger;
            self.model.attributes.id = model.attributes.id;
            app.alert.show('message-id', {
                level: 'confirmation',
                messages: 'Replace with this Quote?',
                autoClose: false,
                onConfirm: function(){
                    app.alert.show('message-id', {
                        level: 'process',
                        title: 'Replacing Quote...',
                        autoClose: false
                    });
    
                    var relatedOpp   = app.data.createBean('Opportunities', {id:self.context.attributes.parentModel.id})   
                    var relatedQuote = app.data.createBean('QM_Quotes_Module', {id: self.model.attributes.id});
    
                    relatedQuote.fetch({
                        success: function(){
                            relatedOpp.fetch({
                                success: function() {
                                    var rlis = relatedOpp.getRelatedCollection('revenuelineitems');
                                    rlis.fetch({
                                        success:function() {
                                            _.each(rlis.models, function(rli) {
                                                var url = app.api.buildURL('RevenueLineItems/'+rli.id);
                                                app.api.call('delete',url);
                                            })
                                            relatedOpp.set('name', relatedQuote.get('description'));
                                            relatedOpp.set('opportunities_qm_quotes_module_1qm_quotes_module_idb', relatedQuote.get('id'));
                                            relatedOpp.set('account_id', relatedQuote.get('accounts_qm_quotes_module_1accounts_ida'));
                                            relatedOpp.set('date_entered', relatedQuote.get('date_entered'));
                                            relatedOpp.set('requested_on_c', relatedQuote.get('requested_on_c'));
                                            relatedOpp.save({},{
                                                success: function(){
                                                    app.alert.dismissAll();
                                                    app.alert.show('message-id', {
                                                    level: 'success',
                                                    messages: 'Quote Replaced.',
                                                    autoClose: true
                                                    });
                                                    app.router.refresh();
                                                }
                                            })
                                        }
                                    })
                                }
                            })
                        }
                    })
                },
                onCancel: function(){
                    app.alert.show('message-id', {
                        level: 'process',
                        messages: 'Aborted.',
                        autoClose: true
                    })
                }
            })
        }
    })

  • Once this LogicHook should be called specificaly under that scenario (clicking a button), then you may want to create a checkbox field on Opportunities and set that field as selected at js layer. You LogicHook will only run when that flag has been changed from unselected to selected, so you really need a LogicHook after_save and you must make sure your hook evaluates the attribute dataChanges. Once your LogicHook has finished creating all RLIs, you can turn of that flag and save again the Opportunity.

    Cheers

    André Lopes
    Lampada Global
    Skype: andre.lampada
  • Thank you!!! The combined help of you and my partner Shad Mickelberry has helped to make this a great fix for our clients. I appreciate you guys very much!