Sugar Test Tools
For Sugar customers and partners,Â SugarCRM provides Test ToolsÂ that can be used to verify and perform quality assurance on Sugar customizations. Use of Sugar test technology requires familiarity with PHPUnit for PHP unit testing, Jasmine for JavaScript unit testing, and Apache JMeter for performance load testing.
Sugar Unit Tests
The Sugar Unit Test suites are the automated unit tests developed and maintained by SugarCRM Engineering during the process of building and releasing each new Sugar release. As part of our development process, these tests are required to run "green" (100% passing) at all times on each master release branch. Essentially, these tests form a regression test suite for an uncustomized version of Sugar running in our controlled build environment.
With that understanding, here is a recommended approach to take advantage of these unit tests.
Run test suite against an uncustomized copy of Sugar in your development environment to establish a baseline. Not all tests may immediately pass; some may fail due to configuration differences between your development environment and SugarCRM Engineering's controlled build environment.
Correct any observed failures or skip/remove the failing tests to create a base test suite that is 100% passing.
As you develop customizations on Sugar, ensure that your base test suite continues to pass.
Create new tests for new code customizations that you create.
Sugar Performance Tests
Instead of sanitizing data from a production environment for purposes of load testing, SugarCRM provides an open source tool called Tidbit that can be used to generate pseudo-random data to populate a Sugar instance with representable data.
We also provide Apache JMeter scenarios to Sugar customers and partners who request access. These JMeter scenarios can be used to drive a simulated load against the Sugar REST API interface used by Sugar. They can validate that your customizations have not had an unexpected impact on the performance of a Sugar instance.
DevOps


DevOps
In order to facilitate and streamline development processes, Sugar recommends implementing DevOps automation. Use a tool such as Jenkins to orchestrate test automation, stage changes in any environment (dev, QA, UAT, and prod) for manual verification, and manage the promotion of changes from one environment to the next.
Perform automated tests (e.g. unit tests) on each commit.
Stage development changes on at least a nightly basis for use by QA or demos.
Automate the rollback of changes in any environment as needed.
Automate notifications to affected and responsible parties whenever a test fails, or a build fails to deploy.
Co-Existing with Studio Customizations
Sugar Studio and Module Builder enable administrator users to implement quick changes to a Sugar environment. But Sugar Studio lacks the rigorous change control that larger CRM projects need. Sometimes, Studio changes can even break code-level Sugar customizations. For this reason, we often discourage using Studio on heavily customized Sugar instances.
However, if Sugar Studio is an important up-front requirement for a CRM project, then there are strategies you can adopt for your customizations to avoid conflicts.
Sugar Studio can be used to modify the layouts, fields, and relationships for the majority of modules in Sugar. Module Builder can be used to define new modules along with their layouts, fields, and relationships. In practice, this means that the fields and layouts for any module's record view, list view, mobile view, and subpanels can be customized using simple administration tools.
Studio users, therefore, could potentially break code customizations that are reliant on a particular field or having a field on a particular layout or location. Here are some practices to avoid conflicts between Studio customizations and custom code:
Avoid custom record, list, and subpanel view controllers because Studio users can change fields and layouts that affect expectations within your controller code.


Avoid custom record validations because Studio users can change fields and layouts that affect expectations within your validation code.
Avoid hard-coded integrations that rely on a particular field because Studio users can change fields that affect expectations within your integration.
Avoid new field and relationship vardefs customizations because Studio users can create new fields and relationships.
Avoid viewdefsÂ customizations for record, list, and subpanel layouts because Studio users perform these customizations.
Many customizations have a smaller risk of side effects related to Studio customizations. Here is a list of some changes that administrator users cannot perform via Studio:
Adding custom dashlets
Adding custom layouts or additions to footer or header
Adding custom actions in record view action dropdown
Adding metadata-aware integrations that discover fields and modules on the system
Adding logic hooks
Conversely, certain customizations can provide Studio users with additional tools:
Adding custom Sugar Logic functions
Adding custom Sugar field types
  	Last modified: 2023-02-03 21:04:03


Cookbook
Welcome to the SugarCRM Developer Cookbook! This library is filled with real-world code examples for common Sugar customizations and best practices. Sugar's DevClub is always cooking up new ideas, so be sure to check in often and see what's new. Get started by browsing the categories below.


TopicsAdding Buttons to the Record ViewThis example explains how to create additional buttons on the record view and add events. We will extend and override the stock Accounts record view to add a custom button. The custom button will be called "Validate Postal Code" and ping the Zippopotamus REST service to validate the records billing state and postal code.Adding Field Validation to the Record ViewThis page explains how to add additional field validation to the record view. In the following examples, we will extend and override the stock Accounts record view to add custom validation. The custom validation will require the Office Phone field when the account type is set to "Customer" and also require the user to enter at least one email address.Adding an Existing Note to an Email as AttachmentThere may be times when you want to reuse a file attached to one Note record as an attachment for an Email, similar to the ability in the Compose Email view to add an attachment using 'Sugar Document'.Adding the Email Field to a BeanThis example explains how to add an emails field for modules that don't extend the Person module template. We will create an email field and bring the email functionality module bean. The steps are applicable for stock and custom modules. In this example, we will add the email field into the Opportunities module.Converting Address' Country Field to a DropdownAddress fields in SugarÂ® are normally text fields, which allow users to enter in the appropriate information (e.g. street, city, and country) for the record. However, with multiple users working in Sugar, it is possible for data (e.g. country) to be entered in a variety of different ways (e.g. USA, U.S.A, and United States) when creating or editing the record. This can cause some issues when creating a report grouped by the Billing Country field, for example, as records with the same country will be grouped separately based on the different ways the country was entered.Changing the Default Module When Logging a New


grouped separately based on the different ways the country was entered.Changing the Default Module When Logging a New Call or MeetingWhen creating a call or meeting directly from the Calls or Meetings module in Sugar, the default module for the Related To field is Accounts. If your sales team frequently schedules calls and meetings related to records from a module other than Accounts, it may make sense to adjust the behavior so that the Related To field defaults to a more commonly used module. This article covers how to change the default related module for calls and meetings in Sugar.Creating Custom Field TypesIn this example, we create a custom field type called "Highlightfield", which will mimic the base text field type with the added feature that the displayed text for the field will be highlighted in a color chosen when the field is created in Studio.Creating an Auto-Incrementing FieldThis article will cover the two approaches to creating an auto-incrementing field in Sugar.Customizing Prefill Fields When Copying RecordsThe copy action on the record view allows for users to duplicate records. This article will cover the various ways to customize the prefill fields on the copy view.Customizing the Email Editor ButtonsThe Emails module in SugarÂ® displays commonly-used buttons in the HTML editor. This article explains how to modify the buttons on the editor's toolbar.Customizing the Start Speed of List View SearchWhen searching a Sidecar module's list view, SugarÂ® begins returning results automatically once a predefined number of milliseconds have passed. This article covers how to customize the start speed of the list view search for Sidecar modules in Sugar.Disabling RLI Alerts on OpportunitiesHow to disable Revenue Line Item (RLI) alerts on Opportunities using a custom JavaScript controller.Disabling TooltipsThis article will demonstrate how to disable the tooltips in Sugar.Dynamically Hiding Subpanels Based on Record ValuesThis article will demonstrate how to dynamically hide subpanels that are dependent on record values by overriding the subpanels layout for a module. This example is for an account record where


record values by overriding the subpanels layout for a module. This example is for an account record where we will hide the Contacts subpanel when the account type is not 'Customer'.Enabling Importing for Custom ModulesWhen designing a custom module in Module Builder, you have the option to enable importing for the module. If the custom module is deployed without enabling this option, it is not recommended that you redeploy the module since any changes made in Studio and potentially other areas of the application could be lost. This article will cover how to enable importing for custom modules via a code-level change to preserve any additional configurations made to the module since being deployed from Module Builder.Increasing the Number of Dashboards Displayed in the Home MenuBy default, the "Home Dashboard" comes out-of-the-box with SugarÂ® to display on the home page. In addition, Sugar users with a Sugar Serve and/or Sugar Sell license type will have access to specialized Home page dashboards called "Service Console" and "Renewals Console". Users have the ability to create new dashboards on their home page and build out its layout and dashlet set. Currently, Sugar imposes a limit of 50 dashboards that can be displayed under the Home module tab. So, when creating multiple dashboards, keep in mind that having more than 50 in the list of available dashboards will cause the ones in the beginning to be dropped off the list. This article covers how to increase the number of dashboards allowed in the Home menu via a code-level customization.Logic HooksThese pages demonstrate some common examples of working with logic hooks in Sugar.Modifying Calendar Item ColorsEach calendar event type (Meetings, Calls, and Tasks) has its own distinct color scheme. This article will review how to modify these colors, as well as demonstrate how to set the event color based on the event status.Modifying Layouts to Display Additional ColumnsBy default, the record view layout for each module displays two columns of fields. The number of


ColumnsBy default, the record view layout for each module displays two columns of fields. The number of columns to display can be customized on a per-module basis with the following steps.Modifying Subpanel ButtonsSeveral buttons exist on each subpanel by default such as "Create" (+ Plus symbol) and "Link Existing Record". These buttons are controlled by the panel-top view for each module. To make changes to the buttons included in a subpanel layout, you will need to create an override for the panel-top view in ./custom/Extension/modules/<module>/Ext/clients/base/views/panel-top/ and update the corresponding buttons property to include or exclude a button.Module Loadable PackagesExamples working with the module loadable packages.Moving Footer content to Sidebar NavThis example explains how to add additional buttons to a new section in the sidebar nav layout. You may have previously customized a button that was added to the footer layout, which can be repurposed here and added to the sidebar-nav layout. We will create a custom view and append the view component to the sidebar-nav layout metadata. The additional button will merely show an alert and a flyout menu with a link to the Accounts module but can be expanded to do much more.Passing Data to TemplatesThis page explains how to create a custom view component that passes data to the Handlebars template.Prepopulating the Compose Email ViewWhen composing an email in Sugar, it may be useful to modify the compose view to better suit your common business practices. In this article, we will use JavaScript to create a code-level customization which causes the To, CC, and Subject fields of the email to prepopulate with data from a related Opportunity.Refreshing Subpanels on the RecordViewHow to refresh specific subpanels on the Record View.Removing the Account Requirement on OpportunitiesThis article covers how to remove the Account field from being required on the Opportunities module.Web ServicesExamples when working with Sugar's web service endpoints.


Last modified: 2023-02-03 21:04:03


Adding the Email Field to a Bean
Overview
This example explains how to add an emails field for modules that don't extend the Person module template. We will create an email field and bring the email functionality module bean. The steps are applicable for stock and custom modules. In this example, we will add the email field into the Opportunities module.
Steps to Complete
This tutorial requires the following steps, which are explained in the sections below:
Creating a Custom Vardef file for email Field and Relationships
Creating a Custom Bean and Modify the save function
Replacing the stock bean with the custom bean
Adding Emails Field into Record View
Creating a Custom Vardef file for email Field and Relationships
You will create a file in the custom/Extension/modules/Opportunities/Â folder for the field and relationships definitions.
./custom/Extension/modules/Opportunities/Ext/Vardefs/custom_email_field.php
<?php
$module = 'Opportunity';
$table_name = 'opportunities';
$dictionary[$module]['fields']['email'] = array(
    'name' => 'email',
    'type' => 'email',
    'query_type' => 'default',
    'source' => 'non-db',
    'operator' => 'subquery',
    'subquery' => 'SELECT eabr.bean_id FROM email_addr_bean_rel eabr JOIN email_addresses ea ON (ea.id = eabr.email_address_id) WHERE eabr.deleted=0 AND ea.email_address LIKE',
    'db_field' => array(
        'id',
    ),
    'vname' => 'LBL_EMAIL_ADDRESS',
    'studio' => array(
        'visible' => true,
        'searchview' => true,
        'editview' => true,
        'editField' => true,
    ),


'editField' => true,
    ),
    'duplicate_on_record_copy' => 'always',
    'len' => 100,
    'link' => 'email_addresses_primary',
    'rname' => 'email_address',
    'module' => 'EmailAddresses',
    'full_text_search' => array(
        'enabled' => true,
        'searchable' => true,
        'boost' => 1.50,
    ),
    'audited' => true,
    'pii' => true,
);
$dictionary[$module]['fields']['email1'] = array(
    'name' => 'email1',
    'vname' => 'LBL_EMAIL_ADDRESS',
    'type' => 'varchar',
    'function' => array(
        'name' => 'getEmailAddressWidget',
        'returns' => 'html',
    ),
    'source' => 'non-db',
    'link' => 'email_addresses_primary',
    'rname' => 'email_address',
    'group' => 'email1',
    'merge_filter' => 'enabled',
    'module' => 'EmailAddresses',
    'studio' => false,
    'duplicate_on_record_copy' => 'always',
    'importable' => false,
);
$dictionary[$module]['fields']['email2'] = array(
    'name' => 'email2',
    'vname' => 'LBL_OTHER_EMAIL_ADDRESS',
    'type' => 'varchar',
    'function' => array(
        'name' => 'getEmailAddressWidget',
        'returns' => 'html',
    ),
    'source' => 'non-db',
    'group' => 'email2',


'source' => 'non-db',
    'group' => 'email2',
    'merge_filter' => 'enabled',
    'studio' => 'false',
    'duplicate_on_record_copy' => 'always',
    'importable' => false,
    'workflow' => false,
);
$dictionary[$module]['fields']['invalid_email'] = array(
    'name' => 'invalid_email',
    'vname' => 'LBL_INVALID_EMAIL',
    'source' => 'non-db',
    'type' => 'bool',
    'link' => 'email_addresses_primary',
    'rname' => 'invalid_email',
    'massupdate' => false,
    'studio' => 'false',
    'duplicate_on_record_copy' => 'always',
);
$dictionary[$module]['fields']['email_opt_out'] = array(
    'name' => 'email_opt_out',
    'vname' => 'LBL_EMAIL_OPT_OUT',
    'source' => 'non-db',
    'type' => 'bool',
    'link' => 'email_addresses_primary',
    'rname' => 'opt_out',
    'massupdate' => false,
    'studio' => 'false',
    'duplicate_on_record_copy' => 'always',
);
$dictionary[$module]['fields']['email_addresses_primary'] = array(
    'name' => 'email_addresses_primary',
    'type' => 'link',
    'relationship' => strtolower($table_name) . '_email_addresses_primary',
    'source' => 'non-db',
    'vname' => 'LBL_EMAIL_ADDRESS_PRIMARY',
    'duplicate_merge' => 'disabled',
    'primary_only' => true,
);


'primary_only' => true,
);
$dictionary[$module]['fields']['email_addresses'] = array(
    'name' => 'email_addresses',
    'type' => 'link',
    'relationship' => strtolower($table_name) . '_email_addresses',
    'source' => 'non-db',
    'vname' => 'LBL_EMAIL_ADDRESSES',
    'reportable' => false,
    'unified_search' => true,
    'rel_fields' => array('primary_address' => array('type' => 'bool')),
);
// Used for non-primary mail import
$dictionary[$module]['fields']['email_addresses_non_primary'] = array(
    'name' => 'email_addresses_non_primary',
    'type' => 'varchar',
    'source' => 'non-db',
    'vname' => 'LBL_EMAIL_NON_PRIMARY',
    'studio' => false,
    'reportable' => false,
    'massupdate' => false,
);
$dictionary[$module]['relationships'][strtolower($table_name) . '_email_addresses'] = array(
    'lhs_module' => $table_name,
    'lhs_table' => strtolower($table_name),
    'lhs_key' => 'id',
    'rhs_module' => 'EmailAddresses',
    'rhs_table' => 'email_addresses',
    'rhs_key' => 'id',
    'relationship_type' => 'many-to-many',
    'join_table' => 'email_addr_bean_rel',
    'join_key_lhs' => 'bean_id',
    'join_key_rhs' => 'email_address_id',
    'relationship_role_column' => 'bean_module',
    'relationship_role_column_value' => $table_name,
);


'relationship_role_column_value' => $table_name,
);
$dictionary[$module]['relationships'][strtolower($table_name) . '_email_addresses_primary'] = array(
    'lhs_module' => $table_name,
    'lhs_table' => strtolower($table_name),
    'lhs_key' => 'id',
    'rhs_module' => 'EmailAddresses',
    'rhs_table' => 'email_addresses',
    'rhs_key' => 'id',
    'relationship_type' => 'many-to-many',
    'join_table' => 'email_addr_bean_rel',
    'join_key_lhs' => 'bean_id',
    'join_key_rhs' => 'email_address_id',
    'relationship_role_column' => 'bean_module',
    'relationship_role_column_value' => $module,
    'primary_flag_column' => 'primary_address',
);
Creating a Custom Bean and Modifying the Save Function
Next step, you will need to create a custom bean that extends from the stock bean. In our example, we choose the Opportunities.Â
./custom/modules/Opportunities/CustomOpportunity.php
  <?php
class CustomOpportunity extends Opportunity
{
    /**
     * Constructor
     */
    public function __construct()
    {
        parent::__construct();
        $this->emailAddress = BeanFactory::newBean('EmailAddresses');
    }
    /**
     * Populate email address fields here instead of retrieve() so that they are properly available for logic hooks
     *
     * @see parent::fill_in_relationship_fields()
     */
    public function fill_in_relationship_fields()
    {
        parent::fill_in_relationship_fields();
        $this->emailAddress->handleLegacyRetrieve($this);
    }
    /**
     * @see parent::get_list_view_data()


}
    /**
     * @see parent::get_list_view_data()
     */
    public function get_list_view_data()
    {
        global $current_user;
        $temp_array = $this->get_list_view_array();
        $temp_array['EMAIL'] = $this->emailAddress->getPrimaryAddress($this);
        // Fill in the email1 field only if the user has access to it
        // This is a special case, because getEmailLink() uses email1 field for making the link
        // Otherwise get_list_view_data() shouldn't set any fields except fill the template data
        if ($this->ACLFieldAccess('email1', 'read')) {
            $this->email1 = $temp_array['EMAIL'];
        }
        $temp_array['EMAIL_LINK'] = $current_user->getEmailLink('email1', $this, '', '', 'ListView');
        return $temp_array;
    }
    /**
     *
     * @see parent::save()
     */
    public function save($check_notify = false)
    {
        if (static::inOperation('saving_related')) {
            parent::save($check_notify);
            return $this;
        }

        $ori_in_workflow = empty($this->in_workflow) ? false : true;
        $this->emailAddress->handleLegacySave($this, $this->module_dir);
        parent::save($check_notify);
        $override_email = array();
        if (!empty($this->email1_set_in_workflow)) {
            $override_email['emailAddress0'] = $this->email1_set_in_workflow;
        }
        if (!empty($this->email2_set_in_workflow)) {
            $override_email['emailAddress1'] = $this->email2_set_in_workflow;


$override_email['emailAddress1'] = $this->email2_set_in_workflow;
        }
        if (!isset($this->in_workflow)) {
            $this->in_workflow = false;
        }
        if ($ori_in_workflow === false || !empty($override_email)) {
            $result = $this->emailAddress->save($this->id, $this->module_dir, $override_email, '', '', '', '',
                $this->in_workflow);
        }
        return $this;
    }
}
Replacing the Stock Bean with a Custom Bean
Once you created the custom bean, you will need to show Sugar to use Custom bean. To do that you will create ./custom/modules/<modulename>/<modulename>.php and update the class name and file path in the $beanList and $beanFiles. (See: Modules $beanListÂ and Customizing Core SugarBeans)
custom/Extension/application/Ext/Include/customOpportunities.php
<?php
$objectList['Opportunities'] = 'Opportunity';
$beanList['Opportunities'] = 'CustomOpportunity';
$beanFiles['CustomOpportunity'] = 'custom/modules/Opportunities/CustomOpportunity.php';
Once you have created these files, you will need to navigate Admin > Repairs and perform Quick Repair and Rebuild.
Adding the Emails Field to the Record View
At the stage, you have already created the field and updated the bean. The last step is placing the email field to record view. You can perform this step using Studio. Once you navigate to Studio you will find an Email field on the left column where available fields are listed. After adding the field the record views of Opportunities will look like this:
./custom/modules/Opportunities/clients/base/views/record/record.php
<?php


./custom/modules/Opportunities/clients/base/views/record/record.php
<?php
$viewdefs['Opportunities'] =
array (
  'base' =>
  array (
    'view' =>
    array (
      'record' =>
      array (
        'buttons' =>
        array (
          0 =>
          array (
            'type' => 'button',
            'name' => 'cancel_button',
            'label' => 'LBL_CANCEL_BUTTON_LABEL',
            'css_class' => 'btn-invisible btn-link',
            'showOn' => 'edit',
            'events' =>
            array (
              'click' => 'button:cancel_button:click',
            ),
          ),
          1 =>
          array (
            'type' => 'rowaction',
            'event' => 'button:save_button:click',
            'name' => 'save_button',
            'label' => 'LBL_SAVE_BUTTON_LABEL',
            'css_class' => 'btn btn-primary',
            'showOn' => 'edit',
            'acl_action' => 'edit',
          ),
          2 =>
          array (
            'type' => 'actiondropdown',
            'name' => 'main_dropdown',
            'primary' => true,
            'showOn' => 'view',
            'buttons' =>
            array (
              0 =>
              array (
                'type' => 'rowaction',
                'event' => 'button:edit_button:click',
                'name' => 'edit_button',
                'label' => 'LBL_EDIT_BUTTON_LABEL',
                'acl_action' => 'edit',
              ),
              1 =>


'acl_action' => 'edit',
              ),
              1 =>
              array (
                'type' => 'shareaction',
                'name' => 'share',
                'label' => 'LBL_RECORD_SHARE_BUTTON',
                'acl_action' => 'view',
              ),
              2 =>
              array (
                'type' => 'pdfaction',
                'name' => 'download-pdf',
                'label' => 'LBL_PDF_VIEW',
                'action' => 'download',
                'acl_action' => 'view',
              ),
              3 =>
              array (
                'type' => 'pdfaction',
                'name' => 'email-pdf',
                'label' => 'LBL_PDF_EMAIL',
                'action' => 'email',
                'acl_action' => 'view',
              ),
              4 =>
              array (
                'type' => 'divider',
              ),
              5 =>
              array (
                'type' => 'rowaction',
                'event' => 'button:find_duplicates_button:click',
                'name' => 'find_duplicates_button',
                'label' => 'LBL_DUP_MERGE',
                'acl_action' => 'edit',
              ),
              6 =>
              array (
                'type' => 'rowaction',
                'event' => 'button:duplicate_button:click',
                'name' => 'duplicate_button',
                'label' => 'LBL_DUPLICATE_BUTTON_LABEL',
                'acl_module' => 'Opportunities',
                'acl_action' => 'create',
              ),
              7 =>
              array (
                'type' => 'rowaction',


7 =>
              array (
                'type' => 'rowaction',
                'event' => 'button:historical_summary_button:click',
                'name' => 'historical_summary_button',
                'label' => 'LBL_HISTORICAL_SUMMARY',
                'acl_action' => 'view',
              ),
              8 =>
              array (
                'type' => 'rowaction',
                'event' => 'button:audit_button:click',
                'name' => 'audit_button',
                'label' => 'LNK_VIEW_CHANGE_LOG',
                'acl_action' => 'view',
              ),
              9 =>
              array (
                'type' => 'divider',
              ),
              10 =>
              array (
                'type' => 'rowaction',
                'event' => 'button:delete_button:click',
                'name' => 'delete_button',
                'label' => 'LBL_DELETE_BUTTON_LABEL',
                'acl_action' => 'delete',
              ),
            ),
          ),
          3 =>
          array (
            'name' => 'sidebar_toggle',
            'type' => 'sidebartoggle',
          ),
        ),
        'panels' =>
        array (
          0 =>
          array (
            'name' => 'panel_header',
            'header' => true,
            'fields' =>
            array (
              0 =>
              array (
                'name' => 'picture',
                'type' => 'avatar',
                'size' => 'large',
                'dismiss_label' => true,
                'readonly' => true,
              ),
              1 =>
              array (


),
              1 =>
              array (
                'name' => 'name',
                'related_fields' =>
                array (
                  0 => 'total_revenue_line_items',
                  1 => 'closed_revenue_line_items',
                  2 => 'included_revenue_line_items',
                ),
              ),
              2 =>
              array (
                'name' => 'favorite',
                'label' => 'LBL_FAVORITE',
                'type' => 'favorite',
                'dismiss_label' => true,
              ),
              3 =>
              array (
                'name' => 'follow',
                'label' => 'LBL_FOLLOW',
                'type' => 'follow',
                'readonly' => true,
                'dismiss_label' => true,
              ),
            ),
          ),
          1 =>
          array (
            'name' => 'panel_body',
            'label' => 'LBL_RECORD_BODY',
            'columns' => 2,
            'labels' => true,
            'labelsOnTop' => true,
            'placeholders' => true,
            'newTab' => false,
            'panelDefault' => 'expanded',
            'fields' =>
            array (
              0 =>
              array (
                'name' => 'account_name',
                'related_fields' =>
                array (
                  0 => 'account_id',
                ),
              ),
              1 =>
              array (
                'name' => 'date_closed',
                'related_fields' =>
                array (
                  0 => 'date_closed_timestamp',
                ),
              ),
              2 =>
              array (


),
              ),
              2 =>
              array (
                'name' => 'amount',
                'type' => 'currency',
                'label' => 'LBL_LIKELY',
                'related_fields' =>
                array (
                  0 => 'amount',
                  1 => 'currency_id',
                  2 => 'base_rate',
                ),
                'currency_field' => 'currency_id',
                'base_rate_field' => 'base_rate',
                'span' => 12,
              ),
              3 =>
              array (
                'name' => 'best_case',
                'type' => 'currency',
                'label' => 'LBL_BEST',
                'related_fields' =>
                array (
                  0 => 'best_case',
                  1 => 'currency_id',
                  2 => 'base_rate',
                ),
                'currency_field' => 'currency_id',
                'base_rate_field' => 'base_rate',
              ),
              4 =>
              array (
                'name' => 'worst_case',
                'type' => 'currency',
                'label' => 'LBL_WORST',
                'related_fields' =>
                array (
                  0 => 'worst_case',
                  1 => 'currency_id',
                  2 => 'base_rate',
                ),
                'currency_field' => 'currency_id',
                'base_rate_field' => 'base_rate',
              ),
              5 =>
              array (
                'name' => 'tag',
                'span' => 6,
              ),
              6 =>
              array (
                'name' => 'sales_status',
                'readonly' => true,


'name' => 'sales_status',
                'readonly' => true,
                'studio' => true,
                'label' => 'LBL_SALES_STATUS',
                'span' => 6,
              ),
              7 =>
              array (
                'name' => 'email',
                'studio' =>
                array (
                  'visible' => true,
                  'searchview' => true,
                  'editview' => true,
                  'editField' => true,
                ),
                'label' => 'LBL_EMAIL_ADDRESS',
                'span' => 12,
              ),
            ),
          ),
          2 =>
          array (
            'name' => 'panel_hidden',
            'label' => 'LBL_RECORD_SHOWMORE',
            'hide' => true,
            'labelsOnTop' => true,
            'placeholders' => true,
            'columns' => 2,
            'newTab' => false,
            'panelDefault' => 'expanded',
            'fields' =>
            array (
              0 => 'next_step',
              1 => 'opportunity_type',
              2 => 'lead_source',
              3 => 'campaign_name',
              4 =>
              array (
                'name' => 'description',
                'span' => 12,
              ),
              5 => 'assigned_user_name',
              6 => 'team_name',
              7 =>
              array (
                'name' => 'date_entered_by',
                'readonly' => true,
                'type' => 'fieldset',
                'label' => 'LBL_DATE_ENTERED',
                'fields' =>
                array (
                  0 =>


'fields' =>
                array (
                  0 =>
                  array (
                    'name' => 'date_entered',
                  ),
                  1 =>
                  array (
                    'type' => 'label',
                    'default_value' => 'LBL_BY',
                  ),
                  2 =>
                  array (
                    'name' => 'created_by_name',
                  ),
                ),
              ),
              8 =>
              array (
                'name' => 'date_modified_by',
                'readonly' => true,
                'type' => 'fieldset',
                'label' => 'LBL_DATE_MODIFIED',
                'fields' =>
                array (
                  0 =>
                  array (
                    'name' => 'date_modified',
                  ),
                  1 =>
                  array (
                    'type' => 'label',
                    'default_value' => 'LBL_BY',
                  ),
                  2 =>
                  array (
                    'name' => 'modified_by_name',
                  ),
                ),
              ),
            ),
          ),
        ),
        'templateMeta' =>
        array (
          'useTabs' => false,
        ),
      ),
    ),
  ),
);
Once the files are in place, navigate to Admin > Repair > Quick Repair & Rebuild to regenerate the metadata.
  	Last modified: 2023-02-03 21:04:03


Enabling Importing for Custom Modules
Overview
When designing a custom module in Module Builder, you have the option to enable importing for the module. If the custom module is deployed without enabling this option, it is not recommended that you redeploy the module since any changes made in Studio and potentially other areas of the application could be lost. This article will cover how to enable importing for custom modules via a code-level changeÂ to preserve any additional configurations made to the module since being deployed from Module Builder.
Steps to Complete
To enable importing for your custom module, you mustÂ modify certain PHP files depending on the version of Sugar that you have. Please note that the instructions below apply to custom modules created via Admin > Module Builder and are not applicable to any stock modules which come out-of-the-box with Sugar. All of the directory paths are relative to the root directory of Sugar on the web server and require you to replace the <module_key> and <module_name> variables with appropriate values for your situation. For instance, if your module is installed in the directory of ./modules/abc_custom_module/ then the <module_key> would be abcÂ and <module_name> would be custom_module.
Edit the ./modules/<module_key>_<module_name>/<module_key>_<module_name>_sugar.php file in your Sugar file system.
Around line 24 of the file, locate and changeÂ public $importable = false;Â toÂ public $importable = true;.
Save your changes to the file.
Next, edit the ./modules/<module_key>_<module_name>/clients/base/menus/header/header.php file which should look similar to this:$viewdefs[$moduleName]['base']['menu']['header'] = array(
  array(
    'route' => "#$moduleName/create",
    'label' => 'LNK_NEW_RECORD',
    'acl_action' => 'create',


'acl_action' => 'create',
    'acl_module' => $moduleName,
    'icon' => 'fa-plus',
  ),
  array(
    'route' => "#$moduleName",
    'label' => 'LNK_LIST',
    'acl_action' => 'list',
    'acl_module' => $moduleName,
    'icon' => 'fa-bars',
  ),
);
Add the following line to the end of the file after the lastÂ  ),Â but before the ending );:array(
 'route' => "#bwc/index.php?module=Import&action=Step1&import_module=$moduleName&return_module=$moduleName&return_action=index",
 'label' => 'LBL_IMPORT',
 'acl_action' => 'import',
 'acl_module' => $moduleName,
 'icon' => 'icon-upload',
 ),
Save your changes to the file. The updated file should then look similar to this:$viewdefs[$moduleName]['base']['menu']['header'] = array(
  array(
    'route' => "#$moduleName/create",
    'label' => 'LNK_NEW_RECORD',
    'acl_action' => 'create',
    'acl_module' => $moduleName,
    'icon' => 'fa-plus',
  ),
  array(
    'route' => "#$moduleName",
    'label' => 'LNK_LIST',
    'acl_action' => 'list',
    'acl_module' => $moduleName,
    'icon' => 'fa-bars',
  ),
  array(
    'route' => "#bwc/index.php?module=Import&action=Step1&import_module=$moduleName&return_module=$moduleName&return_action=index",
    'label' => 'LBL_IMPORT',


'label' => 'LBL_IMPORT',
    'acl_action' => 'import',
    'acl_module' => $moduleName,
    'icon' => 'icon-upload',
 ),
);
Once the necessary changes have been made, log into Sugar and navigate to Admin > Repair and perform a "Quick Repair and Rebuild". This will rebuild the cached files to fully implementÂ the changes.Â
Application
After making theseÂ changes, the "Import {Module Name}" option will appear in the Actions menu of the custom module's module tab. Simply click the triangle in the module tab, then select the Import option to create or update records for your custom module. For instructions on using Sugar's Import tool, please refer to the Import documentation.
Â
  	Last modified: 2023-02-03 21:04:03


Modifying Layouts to Display Additional Columns
Overview
By default, the record view layout for each module displays two columns of fields. The number of columns to display can be customized on a per-module basis with the following steps.
Resolution
First, you will want to ensure your layouts are deployed in the custom directory. If you have not previously customized your layouts via Studio, go to Admin > Studio > {Module Name} > Layouts. From there, select each layout you wish to add additional columns to and click 'Save & Deploy'. This action will create a corresponding layout file under the ./custom/modules/{Module Name}/clients/base/views/record/ directory. The file will be named record.php.
In your custom record.php file, locate the maxColumnsÂ value under templateMeta array, and change it to the number of columns you would like to have on screen:
'maxColumns' => '3',
Once that is updated, locate the widths array to define the spacing for your each column(s). You should have a label and field entry for each column in your layout:
'widths' => array (
    0 => array (
        'label' => '10',
        'field' => '30',
    ),
    1 => array (
        'label' => '10',
        'field' => '30',
    ),
    2 => array (
        'label' => '10',
        'field' => '30',
    ),
),
Next, under the existingÂ panelsÂ array, each entry will already have a columns property, with a value of 2. Update each of these to the number of columns you set forÂ maxColumnsÂ :
'columns' => 3,


'columns' => 3,
Once the file has been modified, you can navigate to Studio and add fields to your new column in the layout. For any rows that already contain two fields, the second field will automatically span the second and third column. Simply click the minus (-) icon to contract the field to one column and expose the new column space:
After you have added the desired fields in Studio, click 'Save & Deploy' to finalize your changes
Column Widths
Studio does not support expanding a field beyond two column-widths. If you want a field to span the full layout (all three columns), you would need to manually update this in the custom record.php like so:
array (
    'name' => 'description',
    'span' => 12,
)
Changing the span value from 8 to 12. After saving the changes on the file, you would then run a Quick Repair and Rebuild. However, any updates in Studio will revert the span back to 8.
Â
  	Last modified: 2023-02-03 21:04:03


Customizing the Email Editor Buttons
Overview
The Emails module in SugarÂ® displays commonly-used buttons in the HTML editor. This article explains how to modify the buttons on the editor's toolbar.
Modifying The View
The following sections outline how to customize the button toolbar presented within the TinyMCE editor. In the example below, we will be adding buttons for Cut, Copy, and Paste to the toolbar.
Using Custom Module Metadata
To override the Emails moduleÂ compose-emailÂ view, we will need to copyÂ ./modules/Emails/clients/base/views/compose-email/compose-email.phpÂ toÂ ./custom/modules/Emails/clients/base/views/compose-email/compose-email.php. Once completed, we will then edit the ['tinyConfig']['toolbar']Â definitionÂ to includeÂ | cut copy pasteÂ .Â An example of this is shown below:
./custom/modules/Emails/clients/base/views/compose-email/compose-email.php
$viewdefs['Emails']['base']['view']['compose-email'] = array(
    ...
    'panels' => array(
        array(
            ...
            'fields' => array(
            	...
                array(
                    'name' => 'description_html',
                    'dismiss_label' => true,
                    'span' => 12,
                    'tinyConfig' => array(
                        'toolbar' => 'code | bold italic underline strikethrough | bullist numlist | ' .
                            'alignleft aligncenter alignright alignjustify | forecolor backcolor | ' .
                            'fontsizeselect | formatselect | fontselect | sugarattachment sugarsignature sugartemplate | cut copy paste',
                    ),
                ),
            ),
        ),
        ...
    ),
);


),
                ),
            ),
        ),
        ...
    ),
);
Note:Â We recommend modifying the modules customÂ metadata for local deployments when the customization is not part of a distributed package.
Once this file is in place, navigate to Admin > RepairÂ > Quick Repair and Rebuild. Once completed, your changes will be reflected in the system.
Using The Module Extension Framework
To extend the Emails moduleÂ compose-emailÂ viewÂ using the extension framework, we will need to loop through the existing views array definition to find and modify the description_htmlÂ field and update the relevant sub-arrayÂ ['tinyConfig']['toolbar']Â with the additional toolbar buttons we want to display. An example of this is shown below:
./custom/Extension/modules/Emails/Ext/clients/base/views/compose-email/add_toolbar_buttons.php
<?php
$target_view = 'compose-email';
$target_fieldname = 'description_html';
$default_toolbar =  'code | bold italic underline strikethrough | bullist numlist | ' .
                            'alignleft aligncenter alignright alignjustify | forecolor backcolor | ' .
                            'fontsizeselect | formatselect | fontselect | sugarattachment sugarsignature sugartemplate';
$custom_toolbar = ' | cut copy paste';
if (!empty($viewdefs['Emails']['base']['view'][$target_view]['panels'])) {
	$panels = $viewdefs['Emails']['base']['view'][$target_view]['panels'];
	foreach ($panels as $i => $panel) {
		if (!empty($panel['fields'])) {
			foreach ($panel['fields'] as $j => $field) {
				if ($field['name'] == $target_fieldname) {
					if(!isset($field['tinyConfig'])){


if(!isset($field['tinyConfig'])){
						$field['tinyConfig'] = array();
					}
					/* If toolbar already exists, add our custom buttons to the end,
					otherwise set it to the default and append that. */
					if(!isset($field['tinyConfig']['toolbar'])) {
						$field['tinyConfig']['toolbar'] = $default_toolbar;
					}
					$viewdefs['Emails']['base']['view'][$target_view]['panels'][$i]['fields'][$j]['tinyConfig']['toolbar'] = $field['tinyConfig']['toolbar'] . $custom_toolbar;
				}
			}
		}
	}
}
Note:Â We recommend using the extension framework when the code will be installed as part of a distributed package.Â
Once this file is in place, navigate to Admin > RepairÂ > Quick Repair and Rebuild. Once completed, your changes will be reflected in the system.
Toolbar Format
As demonstrated in the examples above, theÂ toolbarÂ value is a string with the button keywords space-separated. TheÂ |Â indicates a divider should be shown in the editor. If the goal were to replace the toolbar buttons with only text-modifier buttons, the value for toolbarÂ would be:
'bold italic | underline strikethrough'
The toolbar button keywords are documented atÂ TinyMCE Editor Control Identifiers. Note that at this time, only control keywords listed as "Core" are supported by SugarÂ®.
  	Last modified: 2023-02-03 21:04:03


Customizing Prefill Fields When Copying Records
Overview
The copy action on the record view allows for users to duplicate records. This article will cover the various ways to customize the prefill fields on the copy view.
Modifying the Copy Prefill View Using the Vardefs
The following section will outline how to modify the fields that are prefilled when copying a Bug record from the record view using the beans Vardefs. This is helpful when the list of copied fields are static and have no dependencies.Â You can apply this to any module in the system.
./custom/Extension/modules/Bugs/Ext/Vardefs/copyPrefill.php
<?php
//remove a field from copy
$dictionary['Bug']['fields']['description']['duplicate_on_record_copy'] = 'no';
//add a field to copy
$dictionary['Bug']['fields']['priority']['duplicate_on_record_copy'] = 'always';
Once in place, navigate to Admin > Repair > Quick Repair & Rebuild.
Note: You can name the file 'copyPrefill.php' anything you like. We advise against making these changes in theÂ ./custom/Extension/modules/<module>/Ext/Vardefs/sugarfield_<field>.phpÂ files as these changes may be removed during studio edits.
Modifying the Copy Prefill View Using JavaScript Controllers
The following section will outline how to modify the fields that are prefilled when copying a Bug record from the record view using the JavaScript Controller. This is helpful when you need to dependently determine the fields to copy by a field on the bean. You can apply this to any module in the system.
./custom/modules/Bugs/clients/base/views/record/record.js
({
    extendsFrom: 'RecordView',
    setupDuplicateFields: function (prefill) {
        this._super('setupDuplicateFields', prefill);
        var fields = [
            'name',


var fields = [
            'name',
            'assigned_user_id',
            'priority',
            'type',
            'product_category',
            'description'
        ];
        //determines whether the field list above is a set of allowlisted (allowed) or denylisted (denied) fields
        var denylist = false;
        if (denylist) {
            _.each(fields, function (field) {
                if (field && prefill.has(field)) {
                    //set denylist field to the default value if exists
                    if (!_.isUndefined(prefill.fields[field]) && !_.isUndefined(prefill.fields[field].default)) {
                        prefill.set(field, prefill.fields[field].default);
                    } else {
                        prefill.unset(field);
                    }
                }
            });
        } else {
            _.each(prefill.fields, function (value, field) {
                if (!_.contains(fields, field)) {
                    if (!_.isUndefined(prefill.fields[field].default)) {
                        prefill.set(field, prefill.fields[field].default);
                    } else {
                        prefill.unset(field);
                    }
                }
            });
        }
    }
})
Once in place, navigate to Admin > Repair > Quick Repair & Rebuild. The denylist variable will determine whether the list of fields are allowlisted or denylisted from the copy feature.
  	Last modified: 2023-02-03 21:04:03


Adding Field Validation to the Record View
Overview
This page explains how to add additional field validation to the record view. In the following examples, we will extend and override the stock Accounts record view to add custom validation. The custom validation will require the Office Phone field when the account type is set to "Customer" and also require the user to enter at least one email address.
Error Messages
When throwing a validation error, Sugar has several stock messages you may choose to use. They are shownÂ below:
Error Key
Label
Description
maxValue
ERROR_MAXVALUE
This maximum value of this field
minValue
ERROR_MINVALUE
This minimum value of this field
maxLength
ERROR_MAX_FIELD_LENGTH
The max length of this field
minLength
ERROR_MIN_FIELD_LENGTH
The min length of this field
datetime
ERROR_DATETIME
This field requires a valid date
required
ERROR_FIELD_REQUIRED
This field is required
email
ERROR_EMAIL
There is an invalid email address
primaryEmail
ERROR_PRIMARY_EMAIL
No primary email address is set
duplicateEmail
ERROR_DUPLICATE_EMAIL
There is a duplicate email address
number
ERROR_NUMBER
This field requires a valid number
isBefore
ERROR_IS_BEFORE
The date of this field can not be after another date
isAfter
ERROR_IS_AFTER
The date of this field can not be before another date
You also have the option of displaying multiple error messages at a time. The example below would throw an error message notifying the user that the selected field is required and that it is also not a number.
errors['<field name>'] = errors['<field name>'] || {};
errors['<field name>'].required = true;
errors['<field name>'].number = true;
Custom Error Messages


errors['<field name>'].number = true;
Custom Error Messages
Custom error message can be used by appending customÂ language keys to app.error.errorName2Keys when initializing anÂ extended controller. To accomplish this, create a new language key in the $app_strings. An example of this is shown below:
./custom/Extension/application/Ext/Language/en_us.error_custom_message.php
<?php
$app_strings['ERROR_CUSTOM_MESSAGE'] = 'My custom error message.';
Next, you will need to update your controller to use the new languageÂ key. To accomplish this, add your custom language key to theÂ app.error.errorName2Keys array in the initialize method:
initialize: function (options) {
    this._super('initialize', [options]);
    //add custom message key
    app.error.errorName2Keys['custom_message'] = 'ERROR_CUSTOM_MESSAGE';
    ....
},
Once completed, you can call your custom message by setting the app.error.errorName2Keys entry to true as shown below:
errors['phone_office'] = errors['phone_office'] || {};
errors['phone_office'].custom_message = true;
Method 1: Extending the RecordView and CreateView Controllers
One way to validate fields on record view is by creating record andÂ createÂ view controllers. This method requires a duplication of code due to the hierarchy design, however, it does organize the code by module and extendÂ the core functionality. To accomplish this, override and extend the create view controller. This handles the validation when a user is creating a new record. Once the controller has been properlyÂ extended, define the validation check and use the model.addValidationTask method to append your function to the save validation.
./custom/modules/Accounts/clients/base/views/create/create.js
({
    extendsFrom: 'CreateView',


({
    extendsFrom: 'CreateView',
    initialize: function (options) {
        this._super('initialize', [options]);
        //add validation tasks
        this.model.addValidationTask('check_account_type', _.bind(this._doValidateCheckType, this));
        this.model.addValidationTask('check_email', _.bind(this._doValidateEmail, this));
    },
    _doValidateCheckType: function(fields, errors, callback) {
        //validate type requirements
        if (this.model.get('account_type') == 'Customer' && _.isEmpty(this.model.get('phone_office')))
        {
            errors['phone_office'] = errors['phone_office'] || {};
            errors['phone_office'].required = true;
        }
        callback(null, fields, errors);
    },
    _doValidateEmail: function(fields, errors, callback) {
        //validate email requirements
        if (_.isEmpty(this.model.get('email')))
        {
            errors['email'] = errors['email'] || {};
            errors['email'].required = true;
        }
        callback(null, fields, errors);
    },
})
Next, duplicate the validation logic for the record view. This handles the validation when editing existing records.
./custom/modules/Accounts/clients/base/views/record/record.js
({
    /* because 'accounts' already has a record view, we need to extend it */
    extendsFrom: 'AccountsRecordView',
    initialize: function (options) {
        this._super('initialize', [options]);
        //add validation tasks
        this.model.addValidationTask('check_account_type', _.bind(this._doValidateCheckType, this));
        this.model.addValidationTask('check_email', _.bind(this._doValidateEmail, this));
    },


},
    _doValidateCheckType: function(fields, errors, callback) {
        //validate requirements
        if (this.model.get('account_type') == 'Customer' && _.isEmpty(this.model.get('phone_office')))
        {
            errors['phone_office'] = errors['phone_office'] || {};
            errors['phone_office'].required = true;
        }
        callback(null, fields, errors);
    },
    _doValidateEmail: function(fields, errors, callback) {
        //validate email requirements
        if (_.isEmpty(this.model.get('email')))
        {
            errors['email'] = errors['email'] || {};
            errors['email'].required = true;
        }
        callback(null, fields, errors);
    },
})
Once the files are in place, navigate to Admin > Repair > Quick Repair and Rebuild. More information on displaying custom error messages can be found in the Error Messages section.
Method 2: Overriding the RecordView and CreateView Layouts
Another method for defining your own custom validation is to override a module's record and create layouts to append a new view with your logic. The benefits of this method are that you can use the single view to house the validation logic, however, this means that you will have to override the layout. When overriding a layout, verify that the layout has not changed when upgrading your instance.Â Once the layouts are overridden, define the validation check and use theÂ model.addValidationTaskÂ method to append the function to the save validation.Â
First, create the custom view. For the accounts example, create the viewÂ validate-account:
./custom/modules/Accounts/clients/base/views/validate-account/validate-account.jsÂ
({
   className:"hidden",
   _render: function() {
       //No-op, Do nothing here
   },


_render: function() {
       //No-op, Do nothing here
   },
   bindDataChange: function() {
       //add validation tasks
       this.model.addValidationTask('check_account_type', _.bind(this._doValidateCheckType, this));
       this.model.addValidationTask('check_email', _.bind(this._doValidateEmail, this));
   },
   _doValidateCheckType: function(fields, errors, callback) {
       //validate type requirements
       if (this.model.get('account_type') == 'Customer' && _.isEmpty(this.model.get('phone_office')))
       {
           errors['phone_office'] = errors['phone_office'] || {};
           errors['phone_office'].required = true;
       }
       callback(null, fields, errors);
   },
   _doValidateEmail: function(fields, errors, callback) {
       //validate email requirements
       if (_.isEmpty(this.model.get('email')))
       {
           errors['email'] = errors['email'] || {};
           errors['email'].required = true;
       }
       callback(null, fields, errors);
   },
})
More information on displaying custom error messages can be found in the Error Messages section. Next, depending on the selected module, duplicate its createÂ layout to the modulesÂ custom folder to handle record creation. In ourÂ Accounts example, we have an existing ./modules/Accounts/clients/base/layouts/create/create.php file so we need to duplicate this file to ./custom/modules/Accounts/clients/base/layouts/create/create.php. After this has been completed, append the new custom view to the components. append:
array(
   'view' => 'validate-account',
),
As shown below:
./custom/modules/Accounts/clients/base/layouts/create/create.php
<?php


./custom/modules/Accounts/clients/base/layouts/create/create.php
<?php
$viewdefs['Accounts']['base']['layout']['create'] = array(
    'components' =>
        array(
            array(
                'layout' =>
                    array(
                        'components' =>
                            array(
                                array(
                                    'layout' =>
                                        array(
                                            'components' =>
                                                array(
                                                    array(
                                                        'view' => 'create',
                                                    ),
                                                    array(
                                                        'view' => 'validate-account',
                                                    ),
                                                ),
                                            'type' => 'simple',
                                            'name' => 'main-pane',
                                            'span' => 8,
                                        ),
                                ),
                                array(
                                    'layout' =>
                                        array(
                                            'components' =>
                                                array(),
                                            'type' => 'simple',
                                            'name' => 'side-pane',
                                            'span' => 4,
                                        ),
                                ),
                                array(
                                    'layout' =>
                                        array(
                                            'components' =>
                                                array(
                                                    array(
                                                        'view' =>
                                                            array (
                                                                'name' => 'dnb-account-create',
                                                                'label' => 'DNB Account Create',
                                                            ),
                                                        'width' => 12,
                                                    ),
                                                ),
                                            'type' => 'simple',
                                            'name' => 'dashboard-pane',
                                            'span' => 4,
                                        ),
                                ),
                                array(
                                    'layout' =>
                                        array(
                                            'components' =>
                                                array(
                                                    array(
                                                        'layout' => 'preview',
                                                    ),
                                                ),
                                            'type' => 'simple',
                                            'name' => 'preview-pane',


'type' => 'simple',
                                            'name' => 'preview-pane',
                                            'span' => 8,
                                        ),
                                ),
                            ),
                        'type' => 'default',
                        'name' => 'sidebar',
                        'span' => 12,
                    ),
            ),
        ),
    'type' => 'simple',
    'name' => 'base',
    'span' => 12,
);
Lastly, depending on the selected module, duplicate its record layout to the modulesÂ custom folder to handle editing a record. In the accounts example, we do not have an existing ./modules/Accounts/clients/base/layouts/record/record.php file so we duplicated the core ./clients/base/layouts/record/record.php toÂ ./custom/modules/Accounts/clients/base/layouts/record/record.php. Since we are copying from the ./clients/ core directory, modify:
$viewdefs['base']['layout']['record'] = array(
    ...
);
To:
$viewdefs['Accounts']['base']['layout']['record'] = array(
    ...
);
After this has been completed, append the new custom view to the components:
array(
    'view' => 'validate-account',
),
The resulting file is shown below:
./custom/modules/Accounts/clients/base/layouts/record/record.php
<?php
$viewdefs['Accounts']['base']['layout']['record'] = array(
    'components' => array(
        array(
            'layout' => array(
                'components' => array(
                    array(
                        'layout' => array(
                            'components' => array(
                                array(
                                    'view' => 'record',
                                    'primary' => true,
                                ),
                                array(
                                    'view' => 'validate-account',


),
                                array(
                                    'view' => 'validate-account',
                                ),
                                array(
                                    'layout' => 'extra-info',
                                ),
                                array(
                                    'layout' => array(
                                        'name' => 'filterpanel',
                                        'span' => 12,
                                        'last_state' => array(
                                            'id' => 'record-filterpanel',
                                            'defaults' => array(
                                                'toggle-view' => 'subpanels',
                                            ),
                                        ),
                                        'availableToggles' => array(
                                            array(
                                                'name' => 'subpanels',
                                                'icon' => 'icon-table',
                                                'label' => 'LBL_DATA_VIEW',
                                            ),
                                            array(
                                                'name' => 'list',
                                                'icon' => 'icon-table',
                                                'label' => 'LBL_LISTVIEW',
                                            ),
                                            array(
                                                'name' => 'activitystream',
                                                'icon' => 'icon-th-list',
                                                'label' => 'LBL_ACTIVITY_STREAM',
                                            ),
                                        ),
                                        'components' => array(
                                            array(
                                                'layout' => 'filter',
                                                'targetEl' => '.filter',
                                                'position' => 'prepend'
                                            ),
                                            array(
                                                'view' => 'filter-rows',
                                                "targetEl" => '.filter-options'
                                            ),
                                            array(
                                                'view' => 'filter-actions',
                                                "targetEl" => '.filter-options'
                                            ),
                                            array(
                                                'layout' => 'activitystream',
                                                'context' =>
                                                array(
                                                    'module' => 'Activities',
                                                ),
                                            ),
                                            array(
                                                'layout' => 'subpanels',
                                            ),
                                        ),


array(
                                                'layout' => 'subpanels',
                                            ),
                                        ),
                                    ),
                                ),
                            ),
                            'type' => 'simple',
                            'name' => 'main-pane',
                            'span' => 8,
                        ),
                    ),
                    array(
                        'layout' => array(
                            'components' => array(
                                array(
                                    'layout' => 'sidebar',
                                ),
                            ),
                            'type' => 'simple',
                            'name' => 'side-pane',
                            'span' => 4,
                        ),
                    ),
                    array(
                        'layout' => array(
                            'components' => array(
                                array(
                                    'layout' => array(
                                        'type' => 'dashboard',
                                        'last_state' => array(
                                            'id' => 'last-visit',
                                        )
                                    ),
                                    'context' => array(
                                        'forceNew' => true,
                                        'module' => 'Home',
                                    ),
                                ),
                            ),
                            'type' => 'simple',
                            'name' => 'dashboard-pane',
                            'span' => 4,
                        ),
                    ),
                    array(
                        'layout' => array(
                            'components' => array(
                                array(
                                    'layout' => 'preview',
                                ),
                            ),
                            'type' => 'simple',
                            'name' => 'preview-pane',
                            'span' => 8,
                        ),
                    ),
                ),
                'type' => 'default',
                'name' => 'sidebar',
                'span' => 12,
            ),
        ),
    ),
    'type' => 'simple',


),
        ),
    ),
    'type' => 'simple',
    'name' => 'base',
    'span' => 12,
);
Once the files are in place, navigate to Admin > Repair > Quick Repair and Rebuild.Â
  	Last modified: 2023-02-03 21:04:03


Disabling RLI Alerts on Opportunities
Overview
How to disable Revenue Line Item (RLI) alerts on Opportunities using a custom JavaScript controller.
Overriding the Record View
First, we must overrideÂ the stock opportunities record view. This will handle the RLI alerts when navigating to an existing record. This can be done by creating:
./custom/modules/Opportunities/clients/base/views/record/record.js
({
    extendsFrom: 'OpportunitiesRecordView',
    initialize: function (options) {
        this._super('initialize', [options]);
    },
    /**
     * Hide the warning message about missing RLIs
     * @param string module     The module that we are currently on.
     */
    showRLIWarningMessage: function(module) {
        //here we create an empty override function
    },
    /**
     * @inheritdoc
     */
    _dispose: function() {
        this._super('_dispose', []);
    }
})
As you can see, this file extends from 'OpportunitiesRecordView' which points our code to extend the stock ./modules/Opportunities/clients/base/views/record/record.js file.
  	Last modified: 2023-02-03 21:04:03


Refreshing Subpanels on the RecordView
Overview
How to refresh specific subpanels on the Record View.
Refreshing Subpanels
When Working with the Record View, it is sometimes necessary to force the refresh of a subpanel. The following example will demonstrate how to add buttons to force refresh a specific subpanel or all subpanels on the Accounts RecordView.
Adding the Button Metadata
For our example, we will first create a metadata extension file to append our custom refresh buttons to the Accounts RecordView action menu.
./custom/Extension/modules/Accounts/Ext/clients/base/views/record/refreshButtons.php
<?php
//module name
$module = 'Accounts';
//buttons to append
$addButtons = array(
    array(
        'type' => 'divider',
    ),
    array (
        'type' => 'rowaction',
        'event' => 'button:refresh_specific_subpanel:click',
        'name' => 'refresh_specific_subpanel',
        'label' => 'LBL_REFRESH_SPECIFIC_SUBPANEL',
        'acl_action' => 'view',
    ),
    array (
        'type' => 'rowaction',
        'event' => 'button:refresh_all_subpanels:click',
        'name' => 'refresh_all_subpanels',
        'label' => 'LBL_REFRESH_ALL_SUBPANELS',
        'acl_action' => 'view',
    )
);
//if the buttons are missing in our base modules metadata, include core buttons
if (!isset($viewdefs[$module]['base']['view']['record']['buttons']))
{
    require('clients/base/views/record/record.php');
    $viewdefs[$module]['base']['view']['record']['buttons'] = $viewdefs['base']['view']['record']['buttons'];
    unset($viewdefs['base']);
}


unset($viewdefs['base']);
}
foreach($viewdefs[$module]['base']['view']['record']['buttons'] as $outerKey => $outerButton)
{
    if (
        isset($outerButton['type'])
        && $outerButton['type'] == 'actiondropdown'
        && isset($outerButton['name'])
        && $outerButton['name'] == 'main_dropdown'
        && isset($outerButton['buttons'])
    )
    {
        /*
        //removing buttons by name
        foreach($viewdefs[$module]['base']['view']['record']['buttons'][$outerKey]['buttons'] as $innerKey => $innerButton)
        {
            if (
                isset($innerButton['name'])
                && $innerButton['name'] == 'button_name'
            )
            {
                unset($viewdefs[$module]['base']['view']['record']['buttons'][$outerKey]['buttons'][$innerKey]);
            }
        }
        */
        //appending buttons
        foreach ($addButtons as $addButton)
        {
            $viewdefs[$module]['base']['view']['record']['buttons'][$outerKey]['buttons'][]=$addButton;
        }
    }
}
Next, we will create our language labels for theÂ buttons.
./custom/Extension/modules/Accounts/Ext/Language/en_us.refreshButtons.php
<?php
$mod_strings['LBL_REFRESH_SPECIFIC_SUBPANEL'] = 'Refresh Specific Subpanel';
$mod_strings['LBL_REFRESH_ALL_SUBPANELS'] = 'Refresh All Subpanels';
Our next step is to extend the Accounts controller file. This is where we will add our code to refresh the subpanels when the buttons are clicked.
./custom/modules/Accounts/clients/base/views/record/record.js
({


./custom/modules/Accounts/clients/base/views/record/record.js
({
    extendsFrom: 'RecordView',
    initialize: function (options) {
       this._super('initialize', [options]);
        //add listeners for custom buttons
        this.context.on('button:refresh_specific_subpanel:click', this.refresh_specific_subpanel, this);
        this.context.on('button:refresh_all_subpanels:click', this.refresh_all_subpanels, this);
    },
    /**
     * Refreshes a specific subpanel given a link
     */
    refresh_specific_subpanel: function() {
        var linkName = 'contacts';
        var subpanelCollection = this.model.getRelatedCollection(linkName);
        subpanelCollection.fetch({relate: true});
    },
    /**
     * Refreshes all subpanels
     */
    refresh_all_subpanels: function() {
        _.each(this.model._relatedCollections, function(collection){
            collection.fetch({relate: true});
        });
    }
})
Note:Â To refresh a specific subpanel, you will need to pass the linkname of the relationship to theÂ this.model.getRelatedCollection() method.
Finally, we will then need to navigate to Admin > Repair > Quick Repair and Rebuild. This will rebuild our extensions and make the refresh buttons available on our RecordView.
  	Last modified: 2023-02-03 21:04:03


This page applies to the legacy "BWC" calendar offering. Sugar has released a Sidecar-based calendar in version 11.2.0; this content does not apply to the Sidecar calendar.
Modifying Calendar Item Colors
Overview
Each calendar event type (Meetings, Calls, and Tasks) has its own distinct color scheme. This article will review how to modify these colors, as well as demonstrate how to set the event color based on the event status.
The color scheme for each calendar activity type is defined in the stock file ./modules/Calendar/CalendarDisplay.phpÂ in the $activity_colors array as shown below:
public $activity_colors = array(
	'Meetings' => array(
		'border' => '#1C5FBD',
		'body' => '#D2E5FC',
	),
	'Calls' => array(
		'border' => '#DE4040',
		'body' => '#FCDCDC',
	),
	'Tasks' => array(
		'border' => '#015900',
		'body' => '#B1F5AE',
	),
);
This file can not be overridden or extended in an upgrade-safe way, but this array can still be overridden later in the rendering process allowing for an upgrade safe customization.
Modifying the Calendar Activity Colors
The CalendarDisplay class passes the $activity_colors array to javascript by way of the ./modules/Calendar/tpls/main.tpl Smarty template.
In this template file, you will see the following code where the CAL.activity_colorsÂ object is setup:
CAL.activity_colors = [];
{foreach name=colors from=$activity_colors key=module item=v}
	CAL.activity_colors['{$module}'] = [];
	CAL.activity_colors['{$module}']['border'] = '{$v.border}';


CAL.activity_colors['{$module}']['border'] = '{$v.border}';
	CAL.activity_colors['{$module}']['body'] = '{$v.body}'
{/foreach}
This is where we can update the $activity_colors array in an upgrade safe manner. To do this, copy the template file from./modules/Calendar/tpls/main.tpl to ./custom/modules/Calendar/tpls/main.tpl.
If we wanted to change Meetings to have a purple color scheme, we might choose:
border: #580059body : #F2AEF5
To implement this, we'll update the activity colors array in the custom template file as follows:
./custom/modules/Calendar/tpls/main.tpl
CAL.activity_colors = [];
{foreach name=colors from=$activity_colors key=module item=v}
	CAL.activity_colors['{$module}'] = [];
	CAL.activity_colors['{$module}']['border'] = '{$v.border}';
	CAL.activity_colors['{$module}']['body'] = '{$v.body}'
{/foreach}
{literal}
CAL.activity_colors["Meetings"] = {
	"border": "#580059",
	"body": "#F2AEF5"
};
{/literal}
In the above code, the activity_colors entry for "Meetings" has specifically been altered to set the custom colors just for Meetings, leaving the other activity types as they were. Alternatively, the entire activity_colors array could be updated, replacing the original coming from CalendarDisplay. For example:
./custom/modules/Calendar/tpls/main.tpl
CAL.activity_colors = [];
{foreach name=colors from=$activity_colors key=module item=v}
	CAL.activity_colors['{$module}'] = [];
	CAL.activity_colors['{$module}']['border'] = '{$v.border}';


CAL.activity_colors['{$module}']['border'] = '{$v.border}';
	CAL.activity_colors['{$module}']['body'] = '{$v.body}'
{/foreach}
{literal}
CAL.activity_colors = {
		"Meetings": {
			"border": "#580059",
			"body": "#F2AEF5"
		},
		"Calls": {
			"border": "#DE4040",
			"body": "#FCDCDC"
		},
		"Tasks": {
			"border": "#015900",
			"body": "#B1F5AE"
		}
	};
{/literal}
While Calls and Tasks have not been modified from their original colors, having the entire array defined here may make it easier to make further color customizations later on, since the modified activity_colors will be completely defined in the custom template file.
After making the above changes, run a Quick Repair and Rebuild and reload the Calendar module in your browser to see the changes take effect.
Customizing colors based on event status
If you wanted to go a step further, you might want to customize the colors based on the status of the activity. For example, perhaps for Meetings, the color should reflect if the Meeting status is Scheduled, Held, or Canceled. To achieve this, we will need to customize the javascript that actually renders the activities onto the calendar view, so we will copyÂ ./modules/Calendar/Cal.js to ./custom/modules/Calendar/Cal.js.
Additionally, in order for our custom template to load the custom Cal.js, it will need to be updated to point to the custom Cal.js by changing the line:
{sugar_getscript file="modules/Calendar/Cal.js"}
to


{sugar_getscript file="modules/Calendar/Cal.js"}
to
{sugar_getscript file="custom/modules/Calendar/Cal.js"}
The activity colors from the CAL.activity_colors object get applied in the Cal.js script at:
el.style.backgroundColor = CAL.activity_colors[item.module_name]['body'];
el.style.borderColor = CAL.activity_colors[item.module_name]['border'];
The activity type (Meetings, Calls, Tasks) comes from item.module_name, as shown above. The activity's status can be found in the item.status variable. Using these, we can modify the color-setting logic to apply a status-specific color scheme to the Meeting items in the calendar, for example:
if (item.module_name =="Meetings") {
    el.style.backgroundColor = CAL.activity_colors[item.module_name][item.status]['body'];
    el.style.borderColor     = CAL.activity_colors[item.module_name][item.status]['border'];
}
else {
    el.style.backgroundColor = CAL.activity_colors[item.module_name]['body'];
    el.style.borderColor     = CAL.activity_colors[item.module_name]['border'];
}
This assumes that the 'Meetings' part of CAL.activity_colors has status-specific colors set. Therefore we will need to update our custom template file so that this is the case:
./custom/modules/Calendar/tpls/main.tpl
{literal}
CAL.activity_colors = {
    "Meetings": {
        "Planned": {
            "border": "#FF6666",
            "body": "#FF6666"
        },
        "Held": {
            "border": "#FF9999",
            "body": "#FF9999"
        },
        "Not Held": {
            "border": "#6C8CD5",
            "body": "#6C8CD5"
        }
    },
    "Calls": {


}
    },
    "Calls": {
        "border": "#DE4040",
        "body": "#FCDCDC"
    },
    "Tasks": {
        "border": "#015900",
        "body": "#B1F5AE"
    },
    "Planned": {
        "border": "#ff6666",
        "body": "#ff6666"
    },
    "Held": {
        "border": "#FF9999",
        "body": "#FF9999"
    },
    "Not Held": {
        "border": "#6C8CD5",
        "body": "#6C8CD5"
    }
};
{/literal}
After making the above changes, run Rebuild JS Grouping Files followed by a Quick Repair and Rebuild, then reload the Calendar module in your browser to see the changes take effect.
  	Last modified: 2023-02-03 21:04:03


Prepopulating the Compose Email View
Overview
When composing an email in Sugar, it may be useful to modify the compose view to better suit your common business practices. In this article, we will use JavaScript to create a code-level customization which causes the To, CC, and Subject fields of the email to prepopulate with data from a related Opportunity.
Steps to Complete
Create a Custom Compose Email View
To modify the default compose email functionality, we will need to create our own compose-email view that extends from the base compose-email view. To accomplish this, we will need to create the following file:
./custom/modules/Emails/clients/base/views/compose-email/compose-email.js
({
    extendsFrom: 'EmailsComposeEmailView',
    initialize: function(options){
        this._super('initialize',[options]);
        this.configureDefaults();
    },
    /**
     * Configure the default data
     */
    configureDefaults: function(){
        var model = this.context.parent.get('model');
        var module = this.context.parent.get('module');
        if (module == 'Opportunities'){
            var contacts;
            var oppName = model.get('name');
            if (this.model.get('to_collection').length === 0) {
                var email = this.model;
                //Get the related contacts to the Opportunity
                contacts = model.getRelatedCollection('contacts');
                contacts.fetch({
                    relate: true,
                    success: function (data) {
                        var ccRecords = [],
                            toRecords = [];
                        data.forEach(function (record) {
                            var parentName = app.utils.getRecordName(record);
                            if (record.attributes.opportunity_role == 'Primary Decision Maker') {
                                //Primary decisions makers are added to the TO Recipients
                                toRecords.push(app.data.createBean('EmailParticipants', {


toRecords.push(app.data.createBean('EmailParticipants', {
                                    _link: 'to',
                                    parent: _.extend({type: record.module}, record.attributes),
                                    parent_type: record.module,
                                    parent_id: record.get('id'),
                                    parent_name: parentName
                                }));
                            } else {
                                //All other contacts are added to the CC Recipients
                                ccRecords.push(app.data.createBean('EmailParticipants', {
                                    _link: 'cc',
                                    parent: _.extend({type: record.module}, record.attributes),
                                    parent_type: record.module,
                                    parent_id: record.get('id'),
                                    parent_name: parentName
                                }));
                            }
                        });
                        email.get('to_collection').add(toRecords);
                        email.get('cc_collection').add(ccRecords);
                    },
                    fields: ['id', 'full_name', 'email', 'opportunity_role']
                });
            }
            //Default the Subject of Email to the Opportunity Name
            email.set('name',oppName);
        }
    }
})
Some key things to note about this customization:
The custom view should extend from EmailsComposeEmailView
When adding recipients to an Email Bean Model, you should create an EmailParticipants Bean Model and specify the _link property that will be used.
This view will be used when clicking on the "Create" button in the Emails subpanel, or when clicking on Email Address on a record when the user is configured to use the Sugar Email Client
Once you have added the custom code, execute a Quick Repair and rebuild via Admin > Repair > Quick Repair and Rebuild, and your changes should now be reflected in your instance.
  	Last modified: 2023-02-03 21:04:03


Customizing the Start Speed of List View Search
Overview
When searching a Sidecar module's list view, SugarÂ® begins returning results automatically once aÂ predefined number of milliseconds have passed. This article covers how to customize the start speed of the list view search for Sidecar modules in Sugar.
Prerequisites
This change requires code-level customizations, and you will need direct access to the server in order to make the necessary changes in the file system.Â If you already have a relationship with a Sugar partner, you can work with them to make this customization. If not, please refer to the Partner Page to find a reselling partner to help with your development needs.Â
Note: If yourÂ instance is hosted in Sugar's cloud environment, you can create a package that can be installed within Admin > Module Loader. For more information, please refer to the Creating an Installable Package That Copies Files article.
Note: Sugar Sell Essentials customers do not have the ability to upload custom file packages to Sugar using Module Loader.
Use Case
By default, the list view search process in Sugar begins to run 400 milliseconds after a user stops typing or pasting values into one of the search fields.Â For some users or situations, the system-defined start speed for list view search may be considered too fast. We will walk through increasing the length of time it takes before the search starts to run, using 750 milliseconds as an example.
Steps to Complete
The function that controls the list view search speed in Sugar can be found in the following file: ./clients/base/views/filter-quicksearch/filter-quicksearch.jsÂ In order to customize this function, please use the following steps to create an extension in the ./custom/ directory:
Navigate to the ./custom/ directory in the Sugar file system and create the following directory structure if it does not already exist: ./clients/base/views/filter-quicksearch/.


In a text editor application, create a new file.
Copy the code provided below into this new file:({
  extendsFrom: 'FilterQuicksearchView',
  /**
   * @override
   * @param {Object} opts
   */
  initialize: function(opts) {
      this._super('initialize', [opts]);
  },
  /**
   * Fire quick search
   * @param {Event} e
   */
    throttledSearch: _.debounce(function(e) {
      var newSearch = this.$el.val();
      if(this.currentSearch !== newSearch) {
          this.currentSearch = newSearch;
          this.layout.trigger('filter:apply', newSearch);
      }
    }, 750),
})
For our example, the value of "750" at the end of the code represents the new start speed of list view search. Feel free to replace it withÂ your desired value in milliseconds.
Save the new file as./custom/clients/base/views/filter-quicksearch/filter-quicksearch.js.
Update the Ownership and Permissions of the directories and file that were created.
For more information on Linux based stacks, please refer to the article RequiredÂ FileÂ SystemÂ PermissionsÂ on Linux.
For more information on Windows-based stacks, please refer to the article Required File System Permissions on Windows With IIS.
Finally, log into Sugar and navigate to Admin > Repair then perform a "Quick Repair and Rebuild".
After completing the steps in this article, Sugar will wait 750 milliseconds before returning search results onceÂ the user has stopped entering search criteria.
  	Last modified: 2023-02-03 21:04:03


Disabling Tooltips
Overview
This article will demonstrate how to disable the tooltips in Sugar.
Steps to Complete
Creating a Custom JavaScript File
First, we will need to create a custom JavaScript file. This file can technically exist anywhere within the root of yourÂ Sugar instance.
custom/JavaScript/disable_tooltips.js
(function(app){
     app.events.on('app:init', function(){
          // Clear existing ones
          app.tooltip.clear();
          // Disable all
          app.tooltip._disable();
     });
})(SUGAR.App);
Appending to JSGroupings
Second, we need to add our disable_tooltips.js file to sugar_grp7.min.js in ourÂ JSGroupings.
custom/Extension/application/Ext/JSGroupings/disable_tooltips.php
<?php
foreach ($js_groupings as $key => $groupings) {
    foreach ($groupings as $file => $target) {
        if ($target == 'include/javascript/sugar_grp7.min.js') {
            $js_groupings[$key]['custom/JavaScript/disable_tooltips.js'] = 'include/javascript/sugar_grp7.min.js';
        }
        break;
    }
}
Note: More information about JSGroupings can be found here.
Quick Repair and Rebuild
After creating theÂ ./custom/JavaScript/disable_tooltips.js andÂ ./custom/Extension/application/Ext/JSGroupings/disable_tooltips.php files, navigate to Admin > Repairs andÂ perform a "Quick Repair and Rebuild". This will rebuild the cached files to fully implementÂ the changes.Â  Once "Quick Repair and Rebuild" finishes, the tooltips will be disabled through Sugar.
  	Last modified: 2023-02-03 21:04:03


Logic Hooks
These pages demonstrateÂ some common examples of working with logic hooks in Sugar.
TopicsComparing Bean Properties Between Logic HooksHow to compare the properties of a bean between the before_save and after_save logic hooksPreventing Infinite Loops with Logic HooksHow to add a check to a logic hook to eliminate perpetual looping
  	Last modified: 2023-02-03 21:04:03


Preventing Infinite Loops with Logic Hooks
Overview
Infinite loops often happen when a logic hook calls save on a bean in a scenario that triggers the same hook again. This example showsÂ how to add a check to a logic hook to eliminate perpetual looping.
Saving in an After Save Hook
Infinite loops can sometimes happen when you have a need to update a record after it has been run through the workflow process in the after_save hook. Here is an example of a looping hook:
./custom/modules/Accounts/logic_hooks.php
<?php
$hook_version = 1;
$hook_array = Array();
$hook_array['after_save'] = Array();
$hook_array['after_save'][] = Array(
    1,
    'Update Account Record',
    'custom/modules/Accounts/Accounts_Hook.php',
    'Accounts_Hook',
    'update_self'
);
./custom/modules/Accounts/Accounts_Hook.php
<?php
if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
class Accounts_Hook
{
    function update_self($bean, $event, $arguments)
    {
        //generic condition
        if ($bean->account_type == 'Analyst')
        {
            //update
            $bean->industry = 'Banking';
            $bean->save();
        }
    }
}


$bean->save();
        }
    }
}
In the example above, there is a condition that, when met, will trigger the update of a field on the record. This will cause an infinite loop because calling the save() method will trigger once during the before_save hook and then again during the after_save hook. The solution to this problem is to add a new property on the bean to ignore any unneeded saves. For this example, we will name the property "ignore_update_c" and add a check to the logic hook to eliminate the perpetual loop. An example of this is shown below:
./custom/modules/Accounts/Accounts_Hook.php
<?php
if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
class Accounts_Hook
{
    function update_self($bean, $event, $arguments)
    {
        //loop prevention check
        if (!isset($bean->ignore_update_c) || $bean->ignore_update_c === false)
        {
            //generic condition
            if ($bean->account_type == 'Analyst')
            {
                //update
                $bean->ignore_update_c = true;
                $bean->industry = 'Banking';
                $bean->save();
            }
        }
    }
}
Related Record Save Loops
Sometimes logic hooks on two separate but related modules can cause an infinite loop. The problem arises when the two modules have logic hooks that update one another. This is often done when wanting to keep a field in sync between two modules. The example below will demonstrate this behavior on the accounts:contacts relationship:
./custom/modules/Accounts/logic_hooks.php
<?php
$hook_version = 1;
$hook_array = Array();
$hook_array['before_save'] = Array();


$hook_array = Array();
$hook_array['before_save'] = Array();
$hook_array['before_save'][] = Array(
    1,
    'Handling associated Contacts records',
    'custom/modules/Accounts/Accounts_Hook.php',
    'Accounts_Hook',
    'update_contacts'
);
./custom/modules/Accounts/Accounts_Hook.php
<?php
if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
class Accounts_Hook
{
    function update_contacts($bean, $event, $arguments)
    {
        //if relationship is loaded
        if ($bean->load_relationship('contacts'))
        {
            //fetch related beans
            $relatedContacts = $bean->contacts->getBeans();
            foreach ($relatedContacts as $relatedContact)
            {
                //perform any other contact logic
                $relatedContact->save();
            }
        }
    }
}
The above example will loop through all contacts related to the account and save them. At this point, the save will then trigger our contacts logic hook shown below:
./custom/modules/Contacts/logic_hooks.php
<?php
$hook_version = 1;
$hook_array = Array();
$hook_array['before_save'] = Array();
$hook_array['before_save'][] = Array(
    1,
    'Handling associated Accounts records',
    'custom/modules/Contacts/Contacts_Hook.php',
    'Contacts_Hook',
    'update_account'
);
./custom/modules/Contacts/Contacts_Hook.php
<?php
if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
class Contacts_Hook
{
    function update_account($bean, $event, $arguments)
    {


{
    function update_account($bean, $event, $arguments)
    {
        //if relationship is loaded
        if ($bean->load_relationship('accounts'))
        {
            //fetch related beans
            $relatedAccounts = $bean->accounts->getBeans();
            $parentAccount = false;
            if (!empty($relatedAccounts))
            {
                //order the results
                reset($relatedAccounts);
                //first record in the list is the parent
                $parentAccount = current($relatedAccounts);
            }
            if ($parentAccount !== false && is_object($parentAccount))
            {
                //perform any other account logic
                $parentAccount->save();
            }
        }
    }
}
These two logic hooks will continuously trigger one another in this scenario until you run into a max_execution or memory_limit error. The solution to this problem is to add a new property on the bean to ignore any unneeded saves. In our example, we will name this property "ignore_update_c" and add a check to our logic hook to eliminate the perpetual loop. The following code snippets are copies of the two classes with the ignore_update_c property added.
./custom/modules/Accounts/Accounts_Hook.php
<?php
if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
class Accounts_Hook
{
    function update_contacts($bean, $event, $arguments)
    {
        //if relationship is loaded
        if ($bean->load_relationship('contacts'))
        {
            //fetch related beans
            $relatedContacts = $bean->contacts->getBeans();
            foreach ($relatedContacts as $relatedContact)
            {
                //check if the bean's ignore_update_c attribute is not set


{
                //check if the bean's ignore_update_c attribute is not set
                if (!isset($bean->ignore_update_c) || $bean->ignore_update_c === false)
                {
                    //set the ignore update attribute
                    $relatedContact->ignore_update_c = true;
                    //perform any other contact logic
                    $relatedContact->save();
                }
            }
        }
    }
}
./custom/modules/Contacts/Contacts_Hook.php
<?php
if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
class Contacts_Hook
{
    function update_account($bean, $event, $arguments)
    {
        //if relationship is loaded
        if ($bean->load_relationship('accounts'))
        {
            //fetch related beans
            $relatedAccounts = $bean->accounts->getBeans();
            $parentAccount = false;
            if (!empty($relatedAccounts))
            {
                //order the results
                reset($relatedAccounts);
                //first record in the list is the parent
                $parentAccount = current($relatedAccounts);
            }
            if ($parentAccount !== false && is_object($parentAccount))
            {
                //check if the bean's ignore_update_c element is set
                if (!isset($bean->ignore_update_c) || $bean->ignore_update_c === false)
                {
                    //set the ignore update attribute
                    $parentAccount->ignore_update_c = true;
                    //perform any other account logic
                    $parentAccount->save();
                }
            }
        }
    }
}
  	Last modified: 2023-02-03 21:04:03


Comparing Bean Properties Between Logic Hooks
Overview
How to compare the properties of a bean between the before_save and after_save logic hooks
Storing and Comparing Values
When working with a bean in the after_save logic hook, you may notice that the after_save fetched rows no longer match the before_save fetched rows. If your after_save logic needs to be able to compare values that were in the before_save, you can use the following example to help you store and use the values.
./custom/modules/<module>/logic_hooks.php
<?php
$hook_version = 1;
$hook_array = Array();
$hook_array['before_save'] = Array();
$hook_array['before_save'][] = Array(
    1,
    'Store values',
    'custom/modules/<module>/My_Logic_Hooks.php',
    'My_Logic_Hooks',
    'before_save_method'
);
$hook_array['after_save'] = Array();
$hook_array['after_save'][] = Array(
    1,
    'Retrieve and compare values',
    'custom/modules/<module>/My_Logic_Hooks.php',
    'My_Logic_Hooks',
    'after_save_method'
);
?>
./custom/modules/<module>/My_Logic_Hooks.php
<?php
if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
class My_Logic_Hooks
{
    function before_save_method($bean, $event, $arguments)
    {
        //store as a new bean property
        $bean->stored_fetched_row_c = $bean->fetched_row;
    }
    function after_save_method($bean, $event, $arguments)
    {
        //check if a fields value has changed
        if (