),
            ),
        ),
    ),
);
If adding custom fields from the Product Bundles module to the view, you will also need to add that field to the related_fields array as outlined in the Record View documentation below. Once the files are in place, navigate to Admin > Repair > Quick Repair and Rebuild. Your changes will now be reflected in the system.
Grand Totals Footer
The Grand Total Footer contains the calculated totals for the quote. It mimics the information found in the Grand Total Header.
Modifying Fields in the Grand Totals Footer
To modify the Grand Totals Footer fields, you can copy ./modules/Quotes/clients/base/views/quote-data-grand-totals-footer/quote-data-grand-totals-footer.php to ./custom/modules/Quotes/clients/base/views/quote-data-grand-totals-footer/quote-data-grand-totals-footer.php or you may create a metadata extension in ./custom/Extension/modules/Quotes/clients/base/views/quote-data-grand-totals-footer/. Next, modify the $viewdefs['Quotes']['base']['view']['quote-data-grand-totals-footer']['panels'][0]['fields'] index to add or remove your preferred fields.
Example
The example below will remove the "Shipping" field (quotes.shipping) field from the layout.
./custom/modules/Quotes/clients/base/views/quote-data-grand-totals-footer/quote-data-grand-totals-footer.php
<?php
$viewdefs['Quotes']['base']['view']['quote-data-grand-totals-footer'] = array(
    'panels' => array(
        array(
            'name' => 'panel_quote_data_grand_totals_footer',
            'label' => 'LBL_QUOTE_DATA_GRAND_TOTALS_FOOTER',
            'fields' => array(
                'quote_num',
                array(
                    'name' => 'new_sub',


'quote_num',
                array(
                    'name' => 'new_sub',
                    'type' => 'currency',
                ),
                array(
                    'name' => 'tax',
                    'type' => 'currency',
                    'related_fields' => array(
                        'taxrate_value',
                    ),
                ),
                array(
                    'name' => 'total',
                    'label' => 'LBL_LIST_GRAND_TOTAL',
                    'type' => 'currency',
                    'css_class' => 'grand-total',
                ),
            ),
        ),
    ),
);
If adding custom fields from the QuotesÂ module to the view, you will also need to add that field to the related_fields array as outlined in the Record View documentation below. Once the files are in place, navigate to Admin > Repair > Quick Repair and Rebuild. Your changes will now be reflected in the system.
Record View
The record view for Quotes is updated and used like the standard Record View for all modules. The one major difference to note is that the JavaScript models used by the previous views above, are derived from the Model defined in the record view metadata.
Adding Custom Related Fields to Model
In the Record View metadata, the firstÂ panel panel_header containing the picture and name fields for the Quote record, contains a related_fields property in the name definition that defines the related Product Bundles and Products data that is pulled into the JavaScript model. When custom fields are added to the above mentioned views you will need to add them to the related_fields property in their respective related module so that they are properly loaded during the page load.
The following example adds the custom_product_bundle_field_c field from the Product Bundles module, and the custom_product_field_c from the Products module into the retrieved Model.
./custom/modules/Quotes/clients/base/views/record/record.php


./custom/modules/Quotes/clients/base/views/record/record.php
<?php
$viewdefs['Quotes']['base']['view']['record'] = array(
    ...
    'panels' => array(
        array(
            'name' => 'panel_header',
            'label' => 'LBL_PANEL_HEADER',
            'header' => true,
            'fields' => array(
                array(
                    'name' => 'picture',
                    'type' => 'avatar',
                    'size' => 'large',
                    'dismiss_label' => true,
                    'readonly' => true,
                ),
                array(
                   'name' => 'name',
                   'events' => array(
                       'keyup' => 'update:quote',
                   ),
                   'related_fields' => array(
                       array(
                           'name' => 'bundles',
                           'fields' => array(
                              'id',
                              'bundle_stage',
                              'currency_id',
                              'base_rate',
                              'currencies',
                              'name',
                              'deal_tot',
                              'deal_tot_usdollar',
                              'deal_tot_discount_percentage',
                              'new_sub',
                              'new_sub_usdollar',
                              'position',
                              'related_records',
                              'shipping',
                              'shipping_usdollar',
                              'subtotal',
                              'subtotal_usdollar',
                              'tax',
                              'tax_usdollar',
                              'taxrate_id',
                              'team_count',
                              'team_count_link',
                              'team_name',
                              'taxable_subtotal',
                              'total',
                              'total_usdollar',
                              'default_group',
                              'custom_product_bundle_field_c',
                              array(
                                  'name' => 'product_bundle_items',


array(
                                  'name' => 'product_bundle_items',
                                  'fields' => array(
                                      'name',
                                      'quote_id',
                                      'description',
                                      'quantity',
                                      'product_template_name',
                                      'product_template_id',
                                      'deal_calc',
                                      'mft_part_num',
                                      'discount_price',
                                      'discount_amount',
                                      'tax',
                                      'tax_class',
                                      'subtotal',
                                      'position',
                                      'currency_id',
                                      'base_rate',
                                      'discount_select',
                                      'custom_product_field_c',
                                  ),
                                  'max_num' => -1,
                              ),
                          ),
                          'max_num' => -1,
                          'order_by' => 'position:asc',
                      ),
                  ),
              ),
          ),
      ),
      ...
  ),
);
Quote PDFs
Ungrouped Quoted Line Items and Notes
As of Sugar 7.9, the quotes module allows users to add quoted line items and notes without first adding a group. Adding at least one group to your quote was mandatory in earlier versions. This document will cover how to update your custom PDF templates to support ungrouped QLIs and notes. Ungrouped items are technically added to a default group. The goal is to exclude this group when no items exist in it.
PDF Manager Templates
In your PDF Manager templates, you may have code similar to what is shown below to iterate over the groups in a quote:
{foreach from=$product_bundles item="bundle"}
    ....
{/foreach}


{foreach from=$product_bundles item="bundle"}
    ....
{/foreach}
To correct this for 7.9, we will need to add an if statement to check to see if the group is empty. If it is empty, it will be ignored in your PDF. The benefit is that it will also exclude any other empty groups in your quote and clean the generated PDF.
{foreach from=$product_bundles item="bundle"}
    {if $bundle.products|@count}
        ....
    {/if}
{/foreach}
Smarty Templates
In Smarty templates, you may have code similar to what is shown below to iterate over the groups in a quote:
{literal}{foreach from=$product_bundles item="bundle"}{/literal}
    ...
{literal}{/foreach}{/literal}
To correct this for 7.9, we will need to add an if statement to check to see if the group is empty. If it is empty, it will be ignored in your PDF. The benefit is that it will also exclude any other empty groups in your quote and clean the generated PDF.
{literal}{foreach from=$product_bundles item="bundle"}{/literal}
    {literal}{if $bundle.products|@count}{/literal}
        ...
    {literal}{/if}{/literal}
{literal}{/foreach}{/literal}
Creating Custom PDF Templates
With Sugar, there are generally two routes that can be taken to create custom PDF templates. The first and most recommended route is to use the PDF Manager found in the Admin > PDF Manager. The second route is to extend the Sugarpdf class and write your own template using PHP and the TCPDF library. This method should only be used if you are unable to accomplish your business needs through the PDF Manager.
Creating the TCPDF Template


Creating the TCPDF Template
The first step is to create your custom TCPDF template in ./custom/modules/Quotes/sugarpdf/. For our example, we will extend QuotesSugarpdfStandard, found at ./modules/Quotes/sugarpdf/sugarpdf.standard.php, to a new file in ./custom/modules/Quotes/sugarpdf/ to create a custom invoice. The QuotesSugarpdfStandard class sets up our general quote requirements and extends the Sugarpdf class. Technical documentation on templating can be found in the Sugar PDF documentation. Our class will be named QuotesSugarpdfCustomInvoice as the naming must be in the format of <module>Sugarpdf<pdf view>.
./custom/modules/Quotes/sugarpdf/sugarpdf.custominvoice.php
<?php
if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
require_once('modules/Quotes/sugarpdf/sugarpdf.standard.php');
class QuotesSugarpdfCustomInvoice extends QuotesSugarpdfStandard
{
    function preDisplay()
    {
        global $mod_strings, $timedate;
        parent::preDisplay();
        $quote[0]['TITLE'] = $mod_strings['LBL_PDF_INVOICE_NUMBER'];
        $quote[1]['TITLE'] = $mod_strings['LBL_PDF_QUOTE_DATE'];
        $quote[2]['TITLE'] = $mod_strings['LBL_PURCHASE_ORDER_NUM'];
        $quote[3]['TITLE'] = $mod_strings['LBL_PAYMENT_TERMS'];
        $quote[0]['VALUE']['value'] = format_number_display($this->bean->quote_num, $this->bean->system_id);
        $quote[1]['VALUE']['value'] = $timedate->nowDate();


$quote[1]['VALUE']['value'] = $timedate->nowDate();
        $quote[2]['VALUE']['value'] = $this->bean->purchase_order_num;
        $quote[3]['VALUE']['value'] = $this->bean->payment_terms;
        // these options override the params of the $options array.
        $quote[0]['VALUE']['options'] = array();
        $quote[1]['VALUE']['options'] = array();
        $quote[2]['VALUE']['options'] = array();
        $quote[3]['VALUE']['options'] = array();
        $html = $this->writeHTMLTable($quote, true, $this->headerOptions);
        $this->SetHeaderData(PDF_HEADER_LOGO, PDF_HEADER_LOGO_WIDTH, $mod_strings['LBL_PDF_INVOICE_TITLE'], $html);
    }
    /**
     * This method build the name of the PDF file to output.
     */
    function buildFileName()
    {
        global $mod_strings;
        $fileName = preg_replace("#[^A-Z0-9\-_\.]#i", "_", $this->bean->shipping_account_name);
        if (!empty($this->bean->quote_num)) {
            $fileName .= "_{$this->bean->quote_num}";
        }
        $fileName = $mod_strings['LBL_INVOICE'] . "_{$fileName}.pdf";
        if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/MSIE/", $_SERVER['HTTP_USER_AGENT'])) {
            //$fileName = $locale->translateCharset($fileName, $locale->getExportCharset());
            $fileName = urlencode($fileName);
        }
        $this->fileName = $fileName;
    }
}


}
        $this->fileName = $fileName;
    }
}
Next, register the layout. This is a two step process. The first step is to create ./custom/modules/Quotes/Layouts.php which will map our view action to the physical PHP file.
./custom/modules/Quotes/Layouts.php
<?php
$layouts['CustomInvoice'] = 'custom/modules/Quotes/sugarpdf/sugarpdf.custominvoice.php';
The second step is to update the layouts_dom dropdown list by navigating to Admin > Dropdown Editor. Once there, create a new entry in the list with an Item Name of "CustomInvoice" and a Display Label of your choice. It's also recommended to remove the Quote and Invoice Templates if they are not being used to avoid confusion. This will create a ./custom/Extension/application/Ext/Language/en_us.sugar_layouts_dom.php file that should look similar to:
./custom/Extension/application/Ext/Language/en_us.sugar_layouts_dom.php
<?php
$app_list_strings['layouts_dom']=array (
  'CustomInvoice' => 'Custom Invoice',
);
Once the layout is registered, we need to extendÂ the pdfaction field for the Quotes module to render our custom pdf templates in the record view. An example of this is shown below:
./custom/modules/Quotes/clients/base/fields/pdfaction/pdfaction.js
/**
 * @class View.Fields.Base.Quotes.PdfactionField
 * @alias SUGAR.App.view.fields.BaseQuotesPdfactionField
 * @extends View.Fields.Base.PdfactionField
 */
({
    extendsFrom: 'PdfactionField',
    /**
     * @inheritdoc
     * Create PDF Template collection in order to get available template list.
     */
    initialize: function(options) {
        this._super('initialize', [options]);
    },
    /**


this._super('initialize', [options]);
    },
    /**
     * Define proper filter for PDF template list.
     * Fetch the collection to get available template list.
     * @private
     */
    _fetchTemplate: function() {
        this.fetchCalled = true;
        var collection = this.templateCollection;
        collection.filterDef = {'$and': [{
            'base_module': this.module
        }, {
            'published': 'yes'
        }]};
        collection.fetch({
            success: function(){
                var models = [];
                _.each(app.lang.getAppListStrings('layouts_dom'), function(template, id){
                    var model = new Backbone.Model({
                        id: id,
                        name: template
                    });
                    models.push(model);
                });
                collection.add(models);
            }
        });
    },
    /**
     * Build download link url.
     *
     * @param {String} templateId PDF Template id.
     * @return {string} Link url.
     * @private
     */
    _buildDownloadLink: function(templateId) {
        var sugarpdf = this._isUUID(templateId)? 'pdfmanager' : templateId;
        var urlParams = $.param({
            'action': 'sugarpdf',
            'module': this.module,
            'sugarpdf': sugarpdf,
            'record': this.model.id,
            'pdf_template_id': templateId
        });
        return '?' + urlParams;
    },
    /**
     * Build email pdf link url.
     *
     * @param {String} templateId PDF Template id.
     * @return {string} Email pdf url.
     * @private
     */


* @return {string} Email pdf url.
     * @private
     */
    _buildEmailLink: function(templateId) {
        var sugarpdf = this._isUUID(templateId)? 'pdfmanager' : templateId;
        return '#' + app.bwc.buildRoute(this.module, null, 'sugarpdf', {
                'sugarpdf': sugarpdf,
                'record': this.model.id,
                'pdf_template_id': templateId,
                'to_email': '1'
            });
    },
    /**
     * tests to see if a templateId is a uuid or template name.
     *
     * @param {String} templateId PDF Template id
     * @return {boolean} true if uuid, false if not
     * @private
     */
    _isUUID: function(templateId) {
        var regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
        return regex.test(templateId);
    }
})
Once the files are in place, navigate to Admin > Repair > Quick Repair and Rebuild. Your changes will now be reflected in the system and navigating to a url in the format of index.php?module=Quotes&record=<record id>&action=sugarpdf&sugarpdf=CustomInvoice will generate the pdf document.
Hiding PDF Buttons


Hiding PDF Buttons
In some circumstances, users may not have a need to generate PDFs from Quotes. If this should occur, a developer can copy ./modules/Quotes/clients/base/views/record/record.phpÂ to ./custom/modules/Quotes/clients/base/views/record/record.phpÂ if it doesn't already exist. Next, find the array with a name of main_dropwdown inÂ the $viewdefs['Quotes']['base']['view']['record']['buttons']Â index. Once found, you will then need to locate the buttons index within thatÂ array. You will then need to remove the following arrays from that index:
array(
    'type' => 'pdfaction',
    'name' => 'download-pdf',
    'label' => 'LBL_PDF_VIEW',
    'action' => 'download',
    'acl_action' => 'view',
),
array(
    'type' => 'pdfaction',
    'name' => 'email-pdf',
    'label' => 'LBL_PDF_EMAIL',
    'action' => 'email',
    'acl_action' => 'view',
),
Your file should look similar to what is shown below:
custom/modules/Quotes/clients/base/views/record/record.php
<?php
$viewdefs['Quotes']['base']['view']['record'] = array(
    'buttons' => array(
        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',
            ),
        ),
        array(


),
        ),
        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',
        ),
        array(
            'type' => 'actiondropdown',
            'name' => 'main_dropdown',
            'primary' => true,
            'showOn' => 'view',
            'buttons' => array(
                array(
                    'type' => 'rowaction',
                    'event' => 'button:edit_button:click',
                    'name' => 'edit_button',
                    'label' => 'LBL_EDIT_BUTTON_LABEL',
                    'acl_action' => 'edit',
                ),
                array(
                    'type' => 'shareaction',
                    'name' => 'share',
                    'label' => 'LBL_RECORD_SHARE_BUTTON',
                    'acl_action' => 'view',
                ),
                array(
                    'type' => 'divider',
                ),
                array(
                    'type' => 'convert-to-opportunity',
                    'event' => 'button:convert_to_opportunity:click',
                    'name' => 'convert_to_opportunity_button',
                    'label' => 'LBL_QUOTE_TO_OPPORTUNITY_LABEL',
                    'acl_module' => 'Opportunities',
                    'acl_action' => 'create',
                ),
                array(
                    'type' => 'divider',
                ),
                array(
                    'type' => 'rowaction',
                    'event' => 'button:historical_summary_button:click',


'event' => 'button:historical_summary_button:click',
                    'name' => 'historical_summary_button',
                    'label' => 'LBL_HISTORICAL_SUMMARY',
                    'acl_action' => 'view',
                ),
                array(
                    'type' => 'rowaction',
                    'event' => 'button:audit_button:click',
                    'name' => 'audit_button',
                    'label' => 'LNK_VIEW_CHANGE_LOG',
                    'acl_action' => 'view',
                ),
                array(
                    'type' => 'rowaction',
                    'event' => 'button:find_duplicates_button:click',
                    'name' => 'find_duplicates_button',
                    'label' => 'LBL_DUP_MERGE',
                    'acl_action' => 'edit',
                ),
                array(
                    'type' => 'divider',
                ),
                array(
                    'type' => 'rowaction',
                    'event' => 'button:delete_button:click',
                    'name' => 'delete_button',
                    'label' => 'LBL_DELETE_BUTTON_LABEL',
                    'acl_action' => 'delete',
                ),
            ),
        ),
        array(
            'name' => 'sidebar_toggle',
            'type' => 'sidebartoggle',
        ),
    ),
    ...
);
Once the files are in place, navigate to Admin > Repair > Quick Repair and Rebuild. Your changes will now be reflected in the system.
  	Last modified: 2023-02-03 21:04:03


Module Builder
Overview
The Module Builder tool allows programmers to create custom modules without writing code and to create relationships between new and existing CRM modules. To illustrate how to use Module Builder, this article will show how to create and deploy a custom module.
ForÂ this example, a custom module to track media inquiries will be created to track public relations requests within a CRM system. This use case is an often requested enhancement for CRM systems that apply across industries.
Creating New Modules
Module Builder functionality is managed within the 'Developer Tools' section of Sugar's administration console.
Upon selecting 'Module Builder', the user has the option of creating a "New Package". Packages are a collection of custom modules, objects, and fields that can be published within the application instance or shared across instances of Sugar. Once the user selects "New Package", the usernames and describes the type of Custom Module to be created. A package key, usually based on the organization or name of the package creator is required to prevent conflicts between two packages with the same name from different authors. In this case, the package will be named "MediaTracking" to explain its purpose, and a key based on the author name will be used.
Once the new package is created and saved, the user is presented with a screen to create a Custom Module. Upon selecting the "New Module" icon, a screen appears showing six different object templates.
Understanding Object Templates


Understanding Object Templates
Five of the six object templates contain pre-built CRM functionality for key CRM use cases. These objects are:"basic", "company", "file", "issue", "person", and "sale". The "basic" template provides fields such as Name, Assigned to, Team, Date Created, and Description. As their title denotes, the rest of these templates contain fields and application logic to describe entities similar to "Accounts", "Documents, "Cases", "Contacts", and "Opportunities", respectively. Thus, to create a Custom Module to track a type of account, you would select the "Company" template. Similarly, to track human interactions, you would select "People".
For the media tracking use case, the user will use the object template "Issue" because inbound media requests have similarities to incoming support cases. In both examples, there is an inquiry, a recipient of the issue, assignment of the issue and resolution. The final object template is named "Basic" which is the default base object type. This allows the administrator to create their own custom fields to define the object.
Upon naming and selecting the Custom Module template named "Issue", the user can further customize the module by changing the fields and layout of the application and creating relationships between this new module and existing standard or custom modules. This Edit functionality allows a user to construct a module that meets the specific data requirements of the Custom Module.
Editing Module Fields
Fields can be edited and created using the field editor. Fields inherited from the custom module's templates can be relabeled while new fields are fully editable. New fields are added using the Add Field button. This displays a tab where you can select the type of field to add as well as any properties that field-type requires.
Editing Module Layouts


Editing Module Layouts
The layout editor can be used to change the appearance of the screens within the new module, including the EditView, DetailView and ListView screens. When editing the Edit View or the Detail View, new panels and rows can be dragged from the toolbox on the left side to the layout area on the right. Fields can then be dragged between the layout area and the toolbox. Fields are removed from the layout by dragging them from the layout area to the recycling icon. Fields can be expanded or collapsed to take up one or two columns on the layout using the plus and minus icons. TheÂ List, Search, Dashlet, and Subpanel views can be edited by dragging fields between hidden/visible/available columns.
Building Relationships
Once the fields and layout of the Custom Module have been defined, the user then defines relationships between this new module and existing CRM data by clicking "View Relationships". The "Add Relationship" button allows the user to associate the new module to an existing or new custom module in the same package. In the case of the Media Tracker, the user can associate the Custom Module with the existing, standard 'Contacts' module that is available in every Sugar installation using a many-to-many relationship. By creating this relationship, end-users will see the Contacts associated with each Media Inquiry. We will also add a relationship to the activities module so that a Media Inquiry can be related to calls, meetings, tasks, and emails.
Publishing and Uploading Packages


Publishing and Uploading Packages
After the user has created the appropriate fields, layouts, and relationships for the custom modules, this new CRM functionality can be deployed. Click the "Deploy" button to deploy the package to the current instance. This is the recommended way to test your package while developing. If you wish to make further changes to your package or custom modules, you should make those changes in Module Builder, and click the Deploy button again. Clicking the Publish button generates a zip file with the Custom Module definitions. This is the mechanism for moving the package to a test environment and then ultimately to the production environment. The Export button will produce a module loadable zip file, similar to the Publish functionality, except that when the zip file is installed, it will load the custom package into Module Builder for further editing. This is a good method for storing the custom package in case you would like to make changes to it in the future on another Sugar instance. Once your module has been deployed in a production environment, we highly recommend that you do not redeploy the module in Module Builder but modify the module using Studio as outlined in our Best Practices When Building Custom Modules.
After the new package has been published, the administrator must commit the package to the Sugar system through the Module Loader. The administrator uploads the files and commits the new functionality to the live application instance.
Note: Sugar Sell Essentials customers do not have the ability to upload custom file packages to Sugar using Module Loader.
Adding Custom Logic Using Code
While the key benefit of the Module Builder is that the Administrator user is able to create entirely new modules without the need to write code, there are still some tasks that require writing PHP code. For instance, adding custom logic or making a call to an external system through a Web Service. This can be done in one of two methods.
Logic Hooks


Logic Hooks
One way is by writing PHP code that leverages the event handlers, or "logic hooks", available in Sugar. In order to accomplish this, the developer must create the custom code and then add it to the manifest file for the "Media Inquiry" package. More information on creating logic hooks can be found in the Logic Hooks section. Information on adding a hook to your installer package can be found in theÂ Creating an Installable Package for a Logic Hook example.
Custom Bean files
Another method is to add code directly to the custom bean. This is a more complicated approach because it requires understanding the SugarBean class. However, it is a far more flexible and powerful approach.
First, you must "build" your module. This can be done by either deploying your module or clicking the Publish button. Module Builder will then generate a folder for your package in ./custom/modulebuilder/builds/. Inside that folder is where Sugar will have placed the bean files for your new module(s). In this case, we want ./custom/modulebuilder/builds/MediaTracking/SugarModules/modules/jsche_mediarequest/
Inside you will find two files of interest. The first one is {module_name}sugar.php. This file is generated by Module Builder and may be erased by further changes in module builder or upgrades to the Sugar application. You should not make any changes to this file. The second is {module_name}.php. This is the file where you make any changes you would like to the logic of your module. To add our timestamp, we would add the following code to jsche_mediarequest.php
function save($check_notify = FALSE)
{
     global $current_user;
     $this->description .= "Saved on " . date("Y-m-d g:i a"). " by ". $current_user->user_name;
     parent::save($check_notify);
}


parent::save($check_notify);
}
The call to the parent::save function is critical as this will call on the out of box SugarBean to handle the regular Save functionality. To finish, re-deploy or re-publish your package from Module Builder.
You can now upload this module, extended with custom code logic, into your Sugar application using the Module Loader as described earlier.
Using the New Module
After you upload the new module, the new custom module appears in the Sugar instance. In this example, the new module, named "Media" uses the object template "Issue" to track incoming media inquiries. This new module is associated with the standard "Contacts" modules to show which journalist has expressed interest. In this example, the journalist has requested a product briefing. On one page, users can see the nature of the inquiry, the journalist who requested the briefing, who the inquiry was assigned to, the status, and the description.
TopicsBest PracticesSugar provides two tools for building and maintaining custom module configurations: Module Builder and Studio. As an administrator of Sugar, it is important to understand the strengths of both tools so that you have a sound development process as you look to build on Sugar's framework.
  	Last modified: 2023-02-03 21:04:03


Best Practices
Overview
Sugar provides two tools for building and maintaining custom module configurations: Module Builder and Studio. As an administrator of Sugar, it is important to understand the strengths of both tools so that you have a sound development process as you look to build on Sugar's framework.
Goal
This guide will provide you with all the necessary steps from initial definition of your module to the configuration and customization of the module functionality. Follow these tips to ensure your development process will be sound:
Build Your Module in Module Builder
Build the initial framework for your module in Module Builder. Add all the fields you feel will be necessary for the module and construct the layouts with those fields. If possible, it is even better to create your custom modules in a development environment that mirrors your production environment.
Never Redeploy a Package
Once a module has been promoted to a production environment, we recommend only making additional changes in Studio.Â Redeploying a package will remove all customizations related to your module in the following directories:
./modules/
./custom/modules/
./custom/Extension/modules/
This includes workflows, code customizations, changes through Studio, and much more. It is imperative that this directive is followed to ensure any desired configurations remain intact. When working in Studio, you can make the following types of changes:
adding a new field
updating the properties of a field that has been deployed with the module
changing field layouts
creating, modifying and removing relationships
Every Module in Module Builder Gets Its Very Own Package
While it is possible to create multiple modules in a package, this can also cause design headaches down the road. If you end up wanting to uninstall a module and it is part of a larger package, all modules in that package would need to be uninstalled. Keeping modules isolated to their own packages allows greater flexibility in the future if a module is no longer needed.
Create Relationships in Studio After the Module Is Deployed


Create Relationships in Studio After the Module Is Deployed
This part is critical for success as relationships created in Module Builder cannot be removed after the module is deployed unless the package is updated and redeployed from Module Builder. Redeploying from Module Builder is what we are trying to avoid as mentioned above. If you deploy the module and then create the relationships in Studio, you can update or remove the relationships via Studio at any future point in time.
Delete the Package from Module Builder Once It Is Deployed
Once the package is deployed, delete it from Module Builder so that it will not accidentally be redeployed. The only exception to this rule is in a development environment as you may want to continue working and testing until you are ready to move the module to your production environment. If you ever want to uninstall the module at a later date, you can do so under Admin > Module Loader.
  	Last modified: 2023-02-03 21:04:03


Filters
Overview
Filters are a way to predefine searchesÂ on views that render a list of recordsÂ such as list views, pop-up searches, and lookups.Â This pageÂ explains howÂ to implement the various types of filters for record list views.Â
Filters contain the following properties:
Property
Type
Description
id
String
A unique identifier for the filter
name
StringÂ
The label key for the display labelÂ of the filter
filter_definition
Array
The filter definition to apply to the results
editable
BooleanÂ
Determines whether the user can edit the filter. Note:Â If you are creating a predefined filter for a user, this should be set toÂ false
is_template
Boolean
Used with initial pop-up filters to determine if theÂ filter is available as a template
Note: If a filter contains custom fields, those fields must be search-enabled in Studio > {Module Name} > Layouts > Search.
Operators
Operators, defined in ./clients/base/filters/operators/operators.php, are expressions used by aÂ filter to represent queryÂ operators. They are constructed in theÂ filter_definition to help generate the appropriate query syntax that is sent to the database for a specific field type. Operators can be defined on a global or module level. The accepted paths are listed below:
./clients/base/filters/operators/operators.php
./custom/clients/base/filters/operators/operators.php
./modules/<module>/clients/base/filters/operators.php
./custom/modules/<module>/clients/base/filters/operators.php
The list of stock operators is shown below:
Operator
Label / Key
Description
$contains
is any of / LBL_OPERATOR_CONTAINSUsed by multienum
Matches anything that contains the value.
$empty
is empty / LBL_OPERATOR_EMPTYUsed by enum and tag
Matches on empty values.


is empty / LBL_OPERATOR_EMPTYUsed by enum and tag
Matches on empty values.
$not_contains
is not any of / LBL_OPERATOR_NOT_CONTAINSUsed by multienum
Matches anything that does not contain the specified value.
$not_empty
is not empty / LBL_OPERATOR_NOT_EMPTYUsed by enum and tag
Matches on non-empty values.
$in
is any of / LBL_OPERATOR_CONTAINSUsed by enum, int, relate, teamset, and tag
Finds anything where field matches one of the values as specified as an array.
$not_in
is not any of / LBL_OPERATOR_NOT_CONTAINSUsed by enum, relate, teamset, and tag
Finds anything where the field does not match any of the values in the specified array of values.
$equals
exactly matches / LBL_OPERATOR_MATCHESUsed by varchar, name, email, text, and textarea
is equal to / LBL_OPERATOR_EQUALSUsed by currency, int, double, float, decimal, and date
is / LBL_OPERATOR_ISUsed by bool, phone, radioenum, and parent
Performs an exact match on that field.
$starts
starts with / LBL_OPERATOR_STARTS_WITHUsed by varchar, name, email, text, textarea, and phone
is equal to / LBL_OPERATOR_EQUALSUsed by datetime and datetimecombo
Matches on anything that starts with the value.
$not_equals
is not equal to / LBL_OPERATOR_NOT_EQUALSUsed by currency, int, double, float, and decimal
is not / LBL_OPERATOR_IS_NOTUsed by radioenum
Matches on non-matching values.
$gt
is greater than / LBL_OPERATOR_GREATER_THANUsed by currency, int, double, float, and decimal
after / LBL_OPERATOR_AFTERUsed by date
Matches when the field is greater than the value.
$lt


Matches when the field is greater than the value.
$lt
is less than / LBL_OPERATOR_LESS_THANUsed by currency, int, double, float, and decimal
before / LBL_OPERATOR_BEFOREUsed by date
Matches when the field is less than the value.
$gte
is greater than or equal to / LBL_OPERATOR_GREATER_THAN_OR_EQUALSUsed by currency, int, double, float, and decimal
after / LBL_OPERATOR_AFTERUsed by datetime and datetimecombo
Matches when the field is greater than or equal to the value
$lte
is less than or equal to / LBL_OPERATOR_LESS_THAN_OR_EQUALSUsed by currency, int, double, float, and decimal
before / LBL_OPERATOR_BEFOREUsed by datetime and datetimecombo
Matches when the field is less than or equal to the value.
$between
is between / LBL_OPERATOR_BETWEENUsed by currency, int, double, float, and decimal
Matches when a numerical value is between two other numerical values.
last_7_days
last 7 days / LBL_OPERATOR_LAST_7_DAYSUsed by date, datetime, and datetimecombo
Matches date in the last 7 days relative to the current date.
next_7_days
next 7 days / LBL_OPERATOR_NEXT_7_DAYSUsed by date, datetime, and datetimecombo
Matches dates in the next 7 days relative to the current date.
last_30_days
last 30 days / LBL_OPERATOR_LAST_30_DAYSUsed by date, datetime, and datetimecombo
Matches dates in the last 30 days relative to the current date.
next_30_days
next 30 days / LBL_OPERATOR_NEXT_30_DAYSUsed by date, datetime, and datetimecombo
Matches dates in the next 30 days relative to the current date.
last_month
last month / LBL_OPERATOR_LAST_MONTHUsed by date, datetime, and datetimecombo


last month / LBL_OPERATOR_LAST_MONTHUsed by date, datetime, and datetimecombo
Matches dates in the previous month relative to the current month.
this_month
this month / LBL_OPERATOR_THIS_MONTHUsed by date, datetime, and datetimecombo
Matches dates in the current month.
next_month
next month / LBL_OPERATOR_NEXT_MONTHUsed by date, datetime, and datetimecombo
Matches dates in the next month relative to the current month.
last_year
last year / LBL_OPERATOR_LAST_YEARUsed by date, datetime, and datetimecombo
Matches dates in the last year relative to the current year.
this_year
this year / LBL_OPERATOR_THIS_YEARUsed by date, datetime, and datetimecombo
Matches dates in the current year.
next_year
next year / LBL_OPERATOR_NEXT_YEARUsed by date, datetime, and datetimecombo
Matches dates in the next year relative to the current year.
$dateBetween
is between / LBL_OPERATOR_BETWEENUsed by date, datetime, and datetimecombo
Matches dates between two given dates.
yesterday
yesterday / LBL_OPERATOR_YESTERDAYUsed by date, datetime, and datetimecombo
Matches dates on yesterday relative to the current date.
today
today / LBL_OPERATOR_TODAYUsed by date, datetime, and datetimecombo
Matches dates in the current date.
tomorrow
tomorrow / LBL_OPERATOR_TOMORROWUsed by date, datetime, and datetimecombo
Matches dates on tomorrow relative to the current date.
Example
The example below defines a filter where the type field must contain the value Customer and the nameÂ field must start with the letterÂ A.
$filters = array(
    array(
        'type' => array(
            '$in' => array(
                'Customer',
            ),
        ),
    ),
    array(


'Customer',
            ),
        ),
    ),
    array(
        'name' => array(
            '$starts' => 'A',
        ),
    ),
);
Sub-Expressions
Sub-expressions group filter expressions into groupings. By default, all expressions are bound by an $andÂ expression grouping.Â
Sub-Expression
Description
$and
Joins the filters in an "and" expression
$or
Joins the filters in an "or" expression
Note: Sub-Expressions are only applicable to predefined filters and cannot be used for initial filters.Â
The example below defines a filter where the name field must begin with the lettersÂ A or C.
$filters = array(
    '$or' => array (
        array(
            'name' => array(
                '$starts' => 'A',
            ),
        ),
        array(
            'name' => array(
                '$starts' => 'C',
            ),
        ),
    )
);
Module Expressions
Module expressionsÂ operate on modules instead of specific fields. The current module can be specified by either using the module name _this or by leaving the module name as a blank string.
Module Expression
Description
$favorite
Filters the records by the current users favorited items.
$owner
Filters the records by the assigned user.
The example below defines a filter where records must be favorited items.
$filters = array(
    array(
        '$favorite' => '_this'
    ),
);
Filter Examples
Adding Predefined Filters to the List View Filter List
To add a predefined filter to the module's list view, create a new filter definition extension, which will append the filter to the module's viewdefs.


The following example will demonstrate how to add a predefined filter on the Accounts module to return all records with an account type of "Customer" and industry of "Other".
To create a predefined filter, create a display label extension in Â ./custom/Extension/modules/<module>/Ext/Language/. For this example, we will create:
./custom/Extension/modules/Accounts/Ext/Language/en_us.filterAccountByTypeAndIndustry.php
<?php
$mod_strings['LBL_FILTER_ACCOUNT_BY_TYPE_AND_INDUSTRY'] = 'Customer/Other Accounts';
Next, create aÂ custom filter extension in Â ./custom/Extension/modules/<module>/Ext/clients/base/filters/basic/.
For this example,Â we will create:
./custom/Extension/modules/Accounts/Ext/clients/base/filters/basic/filterAccountByTypeAndIndustry.php
<?php
$viewdefs['Accounts']['base']['filter']['basic']['filters'][] = array(
    'id' => 'filterAccountByTypeAndIndustry',
    'name' => 'LBL_FILTER_ACCOUNT_BY_TYPE_AND_INDUSTRY',
    'filter_definition' => array(
        array(
            'account_type' => array(
                '$in' => array(
                    'Customer',
                ),
            ),
        ),
        array(
            'industry' => array(
                '$in' => array(
                    'Other',
                ),
            ),
        ),
    ),
    'editable' => false,
    'is_template' => false,
);
You should notice that the editable and is_template options have been set to "false". If editable is not set to "false", the filter will not be displayed in the list view filter's list.


Finally, navigate to Admin > Repair and click "Quick Repair and Rebuild" to rebuild the extensions and make the predefined filter available for users.
Adding Initial Filters to Lookup Searches
To add initial filters to record lookups and type-ahead searches, define a filter template. This will allow youÂ to filter results for users when looking up a parent related record.Â The following example will demonstrate how to add an initial filter for the Account lookup on the Contacts module. This initial filter will limit recordsÂ to having anÂ account type of "Customer" and a dynamically assigned user value determined by the contact's assigned user.
To add an initial filter to the Contacts record view, create a display label for the filter in ./custom/Extension/modules/<module>/Ext/Language/.Â For this example , we will create:
./custom/Extension/modules/Accounts/Ext/Language/en_us.filterAccountTemplate.php
<?php
$mod_strings['LBL_FILTER_ACCOUNT_TEMPLATE'] = 'Customer Accounts By A Dynamic User';
Next, create aÂ custom template filter extension in Â ./custom/Extension/modules/<module>/Ext/clients/base/filters/basic/. For this example, create:
./custom/Extension/modules/Accounts/Ext/clients/base/filters/basic/filterAccountTemplate.php
<?php
$viewdefs['Accounts']['base']['filter']['basic']['filters'][] = array(
    'id' => 'filterAccountTemplate',
    'name' => 'LBL_FILTER_ACCOUNT_TEMPLATE',
    'filter_definition' => array(
        array(
            'account_type' => array(
                '$in' => array(),
            ),
        ),
        array(
            'assigned_user_id' => ''
        )
    ),
    'editable' => true,
    'is_template' => true,
);


'editable' => true,
    'is_template' => true,
);
As you can see, the filter_definition contains arrays for account_type and assigned_user_id. These filter definitions will receive their values from the contact record view's metadata. You should also note that this filter hasÂ is_template and editable set to "true". This is required for initial filters.
Once the filter template is in place, modify the contact record view's metadata. To accomplish this, edit ./custom/modules/Contacts/clients/base/views/record/record.php to adjust the account_name field. If this file does not exist in your local Sugar installation, navigate to Admin > Studio > Contacts > Layouts > Record View and click "Save & Deploy" to generate it. In this file, identifyÂ the panel_body array as shown below:
1 =>
array (
    'name' => 'panel_body',
    'label' => 'LBL_RECORD_BODY',
    'columns' => 2,
    'labelsOnTop' => true,
    'placeholders' => true,
    'newTab' => false,
    'panelDefault' => 'expanded',
    'fields' =>
    array (
        0 => 'title',
        1 => 'phone_mobile',
        2 => 'department',
        3 => 'do_not_call',
        4 => 'account_name',
        5 => 'email',
    ),
),
Next, modify the account_name field to contain theÂ initial filter parameters.Â
1 =>
array (
    'name' => 'panel_body',
    'label' => 'LBL_RECORD_BODY',
    'columns' => 2,
    'labelsOnTop' => true,
    'placeholders' => true,
    'newTab' => false,


'placeholders' => true,
    'newTab' => false,
    'panelDefault' => 'expanded',
    'fields' =>
    array (
        0 => 'title',
        1 => 'phone_mobile',
        2 => 'department',
        3 => 'do_not_call',
        4 => array (
            //field name
            'name' => 'account_name',
            //the name of the filter template
            'initial_filter' => 'filterAccountTemplate',
            //the display label for users
            'initial_filter_label' => 'LBL_FILTER_ACCOUNT_TEMPLATE',
            //the hardcoded filters to pass to the templates filter definition
            'filter_populate' => array(
                'account_type' => array('Customer')
            ),
            //the dynamic filters to pass to the templates filter definition
            //please note the index of the array will be for the field the data is being pulled from
            'filter_relate' => array(
                //'field_to_pull_data_from' => 'field_to_populate_data_to'
                'assigned_user_id' => 'assigned_user_id',
            )
        ),
        5 => 'email',
    ),
),
Finally, navigate to Admin > Repair and clickÂ "Quick Repair and Rebuild". This will rebuild the extensions and make the initialÂ filter available for users when selecting a parent account for a contact.
Adding Initial Filters to Drawers from a Controller
When creating your own views, you may need to filter a drawer called from within your custom controller. Using an initial filter, as described in the Adding Initial Filters to Lookup SearchesÂ section, we can filter a drawer with predefined values by creating a filter object andÂ populating the config.filter_populateÂ property as shown below:
//create filter


//create filter
var filterOptions = new app.utils.FilterOptions()
    .config({
        'initial_filter': 'filterAccountTemplate',
        'initial_filter_label': 'LBL_FILTER_ACCOUNT_TEMPLATE',
        'filter_populate': {
            'account_type': ['Customer'],
            'assigned_user_id': 'seed_sally_id'
        }
    })
    .format();
//open drawer
app.drawer.open({
    layout: 'selection-list',
    context: {
        module: 'Accounts',
        filterOptions: filterOptions,
        parent: this.context
    }
});
To create a filtered drawer with dynamic values, create a filter object and populate the config.filter_relate property using the populateRelate method as shown below:
//record to filter related fields by
var contact = app.data.createBean('Contacts', {
    'first_name': 'John',
    'last_name': 'Smith',
    'assigned_user_id': 'seed_sally_id'
});
//create filter
var filterOptions = new app.utils.FilterOptions()
    .config({
        'initial_filter': 'filterAccountTemplate',
        'initial_filter_label': 'LBL_FILTER_ACCOUNT_TEMPLATE',
        'filter_populate': {
            'account_type': ['Customer'],
        },
        'filter_relate': {
            'assigned_user_id': 'assigned_user_id'
        }
    })
    .populateRelate(contact)
    .format();
//open drawer
app.drawer.open({
    layout: 'selection-list',
    context: {
        module: 'Accounts',
        filterOptions: filterOptions,
        parent: this.context
    }
});
  	Last modified: 2023-02-03 21:04:03


Logging
Overview
There are two logging systems implemented in the Sugar application: SugarLogger and PSR-3.Â PSR-3Â is Sugar's preferred logger solution and should be used going forward.
PSR-3
PSR-3 compliant logging solution has been implemented based on PHP Monolog.Â
Log Levels
Log Level
Description
Debug
Logs events that help in debugging the application
Info
Logs informational messages and database queries
Warning
Logs potentially harmful events
Notice
Logs messages for deprecated methods that are still in use.
Error
Logs error events in the application
Alert
Logs severe error events that may cause the application to abort. This is the default and recommended level.
Critical
Logs events that may compromise the security of the application
Off
Turns off all logging
When you specify a logging level, the system will record messages for the specified level as well as all higher levels. For example, if you specify "Error", the system records all Error, Fatal, and Security messages. More information on logging levels can be found in theÂ logger levelÂ configuration documentation.
Considerations
When you are not troubleshooting Sugar, the log level should be setÂ toÂ FatalÂ inÂ Admin > System Settings > Logger Settings toÂ ensure that your environment is not wasting unnecessaryÂ resources to write to the Sugar log.
Logging Messages
The PSR-3 implementation in Sugar can also be used to log messages to the Sugar Log file. You can utilize the implementation to log to the Sugar log file using the default channel or you can specify your own custom channel if you want further control over when your custom logs should be displayed.Â
use \Sugarcrm\Sugarcrm\Logger\Factory;
//Get the default Logger
$Logger = Factory::getLogger('default');
$Logger->debug('Debug level message');


$Logger->debug('Debug level message');
$Logger->info('Info level message');
$Logger->notice('Notice level message');
$Logger->warning('Warning level message');
$Logger->error('Error level message');
$Logger->critical('Critical level message');
$Logger->alert('Alert level message');
$Logger->emergency('Emergency level message');
//Get a custom Log Channel
$Logger = Factory::getLogger('my_logger');
Note: For more information on using custom channels, adding custom log handlers and processors see the PSR-3 LoggerÂ documentation.
Â
SugarLogger
The SugarLogger class, located in ./include/SugarLogger/SugarLogger.php, allows for developers and system administrators to log system events to a log file. Sugar then determines which events to write to the log based on the system's Log Level. This can be set in Admin > System Settings.
Log Levels
Log Level
Description
Debug
Logs events that help in debugging the application
Info
Logs informational messages and database queries
Warn
Logs potentially harmful events
Deprecated
Logs messages for deprecated methods that are still in use.
Error
Logs error events in the application
Fatal
Logs severe error events that may cause the application to abort. This is the default and recommended level.
Security
Logs events that may compromise the security of the application
Off
Logging is turned off
When you specify a logging level, the system will record messages for the specified level as well as all higher levels. For example, if you specify "Error", the system records all Error, Fatal, and Security messages. More information on logging levels can be found in the logger level documentation.
Considerations


Considerations
When you are not troubleshooting Sugar, the log level should be set to FatalÂ in Admin > System Settings > Logger Settings toÂ ensure that your environment is not wasting unnecessaryÂ resources to write to the Sugar log.
Logging Messages
Using $GLOBALS['log']
How to log messages using $GLOBALS['log'] in the system.
$GLOBALS['log']->debug('Debug level message');
$GLOBALS['log']->info('Info level message');
$GLOBALS['log']->warn('Warn level message');
$GLOBALS['log']->deprecated('Deprecated level message');
$GLOBALS['log']->error('Error level message');
$GLOBALS['log']->fatal('Fatal level message');
$GLOBALS['log']->security('Security level message');
For more information on the implementation, please refer toÂ theÂ SugarLogger documentation.Â
Using LoggerManager
How to log messages using the LoggerManager.Â
$Logger = \LoggerManager::getLogger();
$Logger->debug('Debug level message');
$Logger->info('Info level message');
$Logger->warn('Warn level message');
$Logger->deprecated('Deprecated level message');
$Logger->error('Error level message');
$Logger->fatal('Fatal level message');
$Logger->security('Security level message');
For more information on the implementation, please refer toÂ theÂ SugarLoggerÂ documentation.Â
Log Rotation


Log Rotation
TheÂ SugarLoggerÂ willÂ automatically rotate the logs when theÂ logger.file.maxSizeÂ configuration setting has been met or exceeded. When this happens, the Sugar log will be renamed with an integer. For example, if the Sugar log wasÂ named "sugarcrm.log, it will then be renamed "sugarcrm_1.log". The next log rotation after that would create "sugarcrm_2.log". This will occur until theÂ logger.file.maxLogsÂ configuration setting has been met. Once met, theÂ log rollover will start over.
Debugging Messages with _ppl()
When developing, it may be beneficial for a developer to use _ppl()Â method to log a message to the Sugar log. The _ppl() method handles converting Objects, so you can quickly dump an entire object to the log while testing during development.Â
_ppl('Debugging message');
This will write a message to the Sugar log that defines the message and file location. An example is shown below:
------------------------------ _ppLogger() output start -----------------------------
Debugging message
------------------------------ _ppLogger() output end -----------------------------
------------------------------ _ppLogger() file: myFile.php line#: 5-----------------------------
Â Note: It is important that you remove _ppl() from your code for production use as it will affect system performance.
TopicsCreating Custom LoggersCustom loggers, defined in ./custom/include/SugarLogger/, can be used to write log entries to a centralized application management tool or to write messages to a developer tool such as FirePHP.PSR-3 LoggerMonolog\Handler\HandlerInterfaceSugarLoggerThe SugarLogger is used for log reporting by the application. The following article outlines the LoggerTemplate interface as well as the LoggerManager object and explains how to programmatically use the SugarLogger.


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


SugarLogger
Overview
The SugarLoggerÂ is used for log reporting by the application. The following article outlines theÂ LoggerTemplateÂ interfaceÂ as well as theÂ LoggerManager object and explains how to programmatically use the SugarLogger.
LoggerTemplate
The LoggerManager manages those objects that implement the LoggerTemplate interface found in ./include/SugarLogger/LoggerTemplate.php.Â
Methods
log($method, $message)
The LoggerTemplate has a single method that should be implemented, which is the log() method.
Arguments
Name
Type
Description
$method
String
The method or level which the Logger should handle the provided message
$message
String
The logged message
Implementations
Sugar comes with two stock LoggerTemplate implementations that can be utilized for different scenarios depending on your needs. You can extend from these implementations or create your own class that implements the template as outlined in Creating Custom LoggersÂ section.
SugarLogger
The SugarLoggerÂ class, found in ./include/SugarLogger/SugarLogger.php, is the default system logging class utilized throughout the majority of Sugar.Â
SugarPsrLogger
The SugarPsrLogger was added in Sugar 7.9 to accommodate the PSR-3Â compliant logging implementation. This logger works just like the SugarLogger object, however it uses the Monolog implementation of Logger.
To configure the SugarPsrLogger as the default system logger, you can add the following to your configuration:
$sugar_config['logger']['default'] = 'SugarPsrLogger';
Log Level Mappings


Log Level Mappings
PSR-3 defines the set of log levels that should be implemented for all PHP Applications, however, theseÂ are different from the log levels defined by theÂ SugarLogger. Below is the list ofÂ SugarLoggerÂ log levels and theirÂ SugarPsrLoggerÂ compatible mapping. All calls to the SugarLogger log levels are mapped according to the table below. For example, when using theÂ SugarPsrLoggerÂ class, all calls to theÂ Â fatal()Â method will map to theÂ AlertÂ log level.
SugarLogger Level
SugarPsrLogger LevelÂ (PSR-3)
Debug
Debug
Info
Info
Warn
Warning
Deprecated
Notice
Error
Error
Fatal
Alert
Security
Critical
LoggerManager
The LoggerManager Object acts as a singleton factory that sets up the configured logging object for use throughout the system. This is the object stored in $GLOBALS['log'] in the majority of use cases in the system.
Methods
getLogger()
This method is used to get the currently configured Logger class.
setLevel($level)
You may find that you want to define the log level while testing your code without modifying the configuration. This can be done by using the setLevel() method.
Arguments
Name
Type
Description
$level
String
The method or level which the Logger should handle the provided message
Example
\LoggerManager::getLogger()->setLevel('debug');
Note: The use of setLevel should be removed from your code for production and it is important to note that it is restricted by package scanner as defined by theÂ Module Loader Restrictions.
assert($message, $condition)


assert($message, $condition)
In your custom code you may want to submit a debug level log, should a condition not meet your expectations. You could do this with an if statement, otherwise you could just use the assert() method as it allows you to pass the debug message, and the condition to check, and if that condition is FALSE a debug level message is logged.
Arguments
Name
Type
Description
$message
String
The log message
$condition
Boolean
The condition to check
Example
$x = 1;
\LoggerManager::getLogger()->assert('X was not equal to 0!', $x==0)
wouldLog($level)
If in your customization you find that extra debugging is needed for particular area of code, and that extra work might have performance impacts on standard log levels, you can use the wouldLog() method to check the current log level before doing the extra work.
Arguments
Name
Type
Description
$level
String
The levelÂ to check againstÂ
Example
if (\LoggerManager::getLogger()->wouldLog('debug')) {
   //Do extra debugging
}
setLogger($level, $logger)
This method allows you to setup a second logger for a specific log level, rather than just using the same logger for all levels. Passing defaultÂ as the level, will set the default Logger used by all levels in the system.
Arguments
Name
Type
Description
$level
String
The levelÂ to check againstÂ
$logger
String
The class name of an installed Logger
Example
//Set the debug level to a custom Logger
\LoggerManager::getLogger()->setLogger('debug', 'CustomLogger');
//Set all other levels to SugarPsrLogger
\LoggerManager::getLogger()->setLogger('default', 'SugarPsrLogger');
getAvailableLoggers()


getAvailableLoggers()
Returns a list of the names of Loggers found/installed in the system.
Arguments
None
Example
$loggers = \LoggerManager::getLogger()->getAvailableLoggers();
getLoggerLevels()
Returns a list of the names of Loggers found/installed in the system.
Arguments
None
Example
$levels = \LoggerManager::getLogger()->getLoggerLevels();
Adding a Custom SugarLogger
Custom loggers are defined in ./custom/include/SugarLogger/, and can be used to write log entries to a centralized application management tool, to a developer tool such as FirePHP or even to a secondary log file inside the Sugar application.
The following is an example of how to create a FirePHP logger.
./custom/include/SugarLogger/FirePHPLogger.php.
<?php
// change the path below to the path to your FirePHP install
require_once('/path/to/fb.php');
class FirePHPLogger implements LoggerTemplate
{
    /** Constructor */
    public function __construct()
    {
        if (
            isset($GLOBALS['sugar_config']['logger']['default'])
            && $GLOBALS['sugar_config']['logger']['default'] == 'FirePHP'
        )
        {
            LoggerManager::setLogger('default','FirePHPLogger');
        }
    }
    /** see LoggerTemplate::log() */
    public function log($level, $message)
    {
        // change to a string if there is just one entry
        if ( is_array($message) && count($message) == 1 ) {
            $message = array_shift($message);
        }
        switch ($level) {
            case 'debug':
                FB::log($message);
                break;
            case 'info':
                FB::info($message);
                break;


case 'info':
                FB::info($message);
                break;
            case 'deprecated':
            case 'warn':
                FB::warn($message);
                break;
            case 'error':
            case 'fatal':
            case 'security':
                FB::error($message);
                break;
        }
    }
}
You will then specify your default logger as 'FirePHP' in your ./config_override.php file.
$sugar_config['logger']['default'] = 'FirePHP';
  	Last modified: 2023-02-03 21:04:03


PSR-3 Logger
Monolog\Handler\HandlerInterface
Overview
Sugar'sÂ PSR-3Â compliant logging solution has been implemented based on PHP Monolog. Accessing the PSR-3 Logger Objects can be done by via the \Sugarcrm\Sugarcrm\Logger namespace. Currently, this logging implementation is only used in a few areas of the system to allow for more in-depth logging in those areas, see the Usage section for more details.
Architecture
The PSR-3 Logging solution is found in ./src/Logger directory, which is mapped to the \Sugarcrm\Sugarcrm\Logger namespace in the application. The following outlines the architecture and current implementations of those core objects.
Factory
The Logger Factory Object, namespaced asÂ \Sugarcrm\Sugarcrm\Logger\Factory, is a singleton factory that creates and manages all Loggers currently being used by the system. The Logger Factory uses 'channels' to allow for multiple Loggers to be utilized and configured independently of default log level set for the system.Â It also allows for each channel to use different handlers, formatters, and processors. Check out the Usage section below for further details on configuration and usage.
Methods
getLogger($channel)
Returns the requested channels \Psr\Log\LoggerInterface implementation
Arguments
Name
Type
Description
$channel
String
The channel for which you are logging against
Example
use \Sugarcrm\Sugarcrm\Logger\Factory;
$Logger = Factory::getLogger('default');
Handlers


$Logger = Factory::getLogger('default');
Handlers
Handlers are the primary object used to manage the logs. They control how logs are submitted and where the logs will go. Since the PSR-3 Logging solution is based on Monolog, there are a number of default Handlers included in Sugar, however, they are not set up in the Sugar architecture to be utilized right off the bat. Sugar utilizes a Factory implementation for the handlers so that theÂ LoggerÂ Factory Object can build out specific Handlers for each channel based on the Sugar Config.
Factory Interface
The Factory Interface for Handlers is used to implement a Logging Handler, so that the Logger Factory can build out the configured handler for a channel, without a lot of work from external developers. The Handler Factory Interface is located in ./src/Logger/Handlers/Factory.php or in code atÂ theÂ \Sugarcrm\Sugarcrm\Logger\Handler\Factory namespace.
Methods
There is only one method to implement forÂ aÂ Handler Factory, which is the create() method. This will contain the necessary Logic to set up the Logger Handler
create($level, $config)
Arguments
Name
Type
Description
$level
String
The log level the Logger Handler will operate atÂ
$config
Array
The config parameters set for the handler in the Sugar config file
Returns
The Monolog\Handler\HandlerInterface implementation
Implementations
By default, Sugar only comes with a single Handler implementation, which is the File Handler implementation. This is used to log directly to the ./sugarcrm.log file to mirror the functionality of the previous logging framework. For information on adding custom handlers, or implementing the built-in Monolog handlers inside of Sugar PSR-3 Logging framework, see the Customization section below.
Configuration
To configure the default logging handler for Sugar the following configuration setting is used:


Configuration
To configure the default logging handler for Sugar the following configuration setting is used:
$sugar_config['logger']['handler'] = '<handler>';
You can also configure a different Logging Handler or Handlers for a specific channel:
//Single Handler
$sugar_config['logger']['channels']['test_channel']['handlers'] = '<handler>';
//Multiple Channels
$sugar_config['logger']['channels']['test_channel']['handlers'] = array('<handler1>','<handler2>');
To pass configuration properties to a Handler your configuration will need to look as follows:
//For system handler
$sugar_config['logger']['handlers']['<handler>']['host'] = '127.0.0.1';
$sugar_config['logger']['handlers']['<handler>']['port'] = 12201;
//For channel handlers
$sugar_config['logger']['channels']['test_channel']['handlers']['<handler>']['host'] = '127.0.0.1';
$sugar_config['logger']['channels']['test_channel']['handlers']['<handler>']['port'] = 12201;
$sugar_config['logger']['channels']['test_channel']['handlers']['<handler>']['level'] = 'debug';
Note: For more information, please refer to the logger.channels.channel.handlers documentation.
Formatters
Formatters are a component of the Handler Object. By default Sugar only comes with a single Formatter, which is the BackwardCompatibleFormatter used by the File Handler, which simply assures that Log messages are consistent with the legacy Logging functionality. Formatters are used and built out in accordance with the Monolog framework, and under the majority of circumstances, building out a custom formatter is not necessary. For more information on Formatters, you can review the Monolog repository.
Processors


Processors
Processors provide a way to add more information to Log messages, without having to hard code this information inside the Handler, so that it can be used only when necessary. For example, Sugar's PSR-3 Logging implementation provides two default Processors that can be enabled for a given channel or handler via configuration. These Processors provide functionality such as adding a stack trace or the web request informationÂ to the log message to provide further debugging context.
Factory Interface
The Factory Interface for processorsÂ is used to implement a Logging Processor, so that the Logger Factory can build out the configured handler for a channel, without a lot of work from external developers. TheÂ ProcessorÂ Factory Interface is located in ./src/Logger/Processor/Factory.phpÂ or in code atÂ \Sugarcrm\Sugarcrm\Logger\Handler\FactoryÂ namespace.
Methods
There is only one method to implement forÂ aÂ Processor Factory, which is theÂ create()Â method. This method will contain the necessary Logic to set up the Processor object.Â
create($config)
Arguments
Name
Type
Description
$config
Array
The config parameters set for theÂ processorÂ in the Sugar config file
Returns
Callable - See Customization section for an example of implementation, otherwise review the included Processor Factory implementations in code in ./src/Logger/Processor/Factory/.
Implementations
By default, Sugar comes with twoÂ ProcessorÂ implementations, \Sugarcrm\Sugarcrm\Logger\Processor\BacktraceProcessor andÂ \Sugarcrm\Sugarcrm\Logger\Processor\RequestProcessor.Â
BacktraceProcessor


BacktraceProcessor
As the name implies, the BacktraceProcessor appends a backtrace or stack trace to the log message output, to easily trace where and how the log message came from. The Factory implementation for this Processor lives inÂ ./src/Logger/Processor/Factory/Backtrace.php, and is referenced in the sugar config as backtrace.
The following shows an example of the output that occurs when utilizing this processor:
Fri Mar 23 09:24:19 2018 [90627][1][FATAL] <log message>; Call Trace: \n0: /var/www/Ent/71110/custom/SugarQueryLogger.php:43 - Sugarcrm\Sugarcrm\Logger\BackwardCompatibleAdapter::fatal()\n2: /var/www/Ent/71110/include/utils/LogicHook.php:270\n3: /var/www/Ent/71110/include/utils/LogicHook.php:160 - LogicHook::process_hooks()\n4: /var/www/Ent/71110/data/SugarBean.php:6684 - LogicHook::call_custom_logic()\n5: /var/www/Ent/71110/data/SugarBean.php:3317 - SugarBean::call_custom_logic()\n6: /var/www/Ent/71110/clients/base/api/FilterApi.php:632 - SugarBean::fetchFromQuery()\n7: /var/www/Ent/71110/clients/base/api/FilterApi.php:397 - FilterApi::runQuery()\n8: /var/www/Ent/71110/include/api/RestService.php:257 - FilterApi::filterList()\n9: /var/www/Ent/71110/api/rest.php:23 - RestService::execute()
Note: when viewing complex logs, you can use the following to print log entries in a more human readable format:
cat sugarcrm.log | sed s/\\n/\n/g


cat sugarcrm.log | sed s/\\n/\n/g
RequestProcessor
The RequestProcessor appendsÂ Session properties to the Log message that would help identify the request that caused the log message. The Factory implementation for this Processor lives in ./src/Logger/Processor/Factory/Request.php, and is referenced in the sugar config as request.
The following list of session properties are appended to the log:
User ID
Client ID (OAuth2 Client)
Platform
The following shows an example of the output that occurs when utilizing this processor:
Mon Mar 26 09:48:58 2018 [5930][1][FATAL] <log message>; User ID=1; Client ID=sugar; Platform=base
Configuration
To configure the default Logging Handler for Sugar to utilize the built-in processors:
//Configure a single Processor
$sugar_config['logger']['channels']['default']['processors'] = 'backtrace';
//Configure multiple Processors
$sugar_config['logger']['channels']['default']['processors'] = array('backtrace','request');
You can also configure different channels to utilize the processors:
//Single Processors
$sugar_config['logger']['channels']['<channel>']['processors'] = 'backtrace';
//Multiple Channels
$sugar_config['logger']['channels']['<channel>']['processors'] = array('backtrace','request');
To pass configuration properties to aÂ ProcessorÂ your configuration will need to look as follows:
$sugar_config['logger']['channels']['<channel>']['processors']['<processor>']['config_key'] = 'test_value';
Note: For more information, please refer to the logger.channels.channel.processors documentation.
Usage


Note: For more information, please refer to the logger.channels.channel.processors documentation.
Usage
As previously mentioned the Logger Factory allows for multiple channels to be configured independently of each other. The following examples will showcase how to use the Logger Factory to get a channel's Logger and use it in code, as well as how to configure the system to use multiple channels with different configurations.
Basic Usage
use \Sugarcrm\Sugarcrm\Logger\Factory;
//Retrieve the default Logger
$DefaultLogger = Factory::getLogger('default');
$DefaultLogger->alert('This is a log message');
Configuring Channels
The following is an example of the ./config_override.php file that would configure two different channels at different log levels, using the logger.channel.channel.level configuration setting. These two channels would allow for portions of the code to Log messages at Debug (and higher) levels, and other portions to only log Info (and higher) levels.
$config['logger']['channels']['default'] = array(
    'level' => 'alert'
);
$config['logger']['channels']['channel1'] = array(
    'level' => 'debug'
);
The following code example shows how toÂ retrieve the above-configured channels and use the Logger for each channel.Â
use \Sugarcrm\Sugarcrm\Logger\Factory;
//Retrieve the default Logger
$DefaultLogger = Factory::getLogger('default');
$DefaultLogger->info("This message will not display");
//Channel1 Logger
$Channel1Logger = Factory::getLogger('channel1');
$Channel1Logger->info("This message will display");


$Channel1Logger->info("This message will display");
In the example above, assuming a stock system with the previously mentioned config values set in config_override.php, the default channel logger would be set to Alert level logging, and therefore would not add the info level log to the Log file, however, the channel11 Logger would add the log message to the Sugar log file, since it is configured at the info log level.
Default Channels
By default, Sugar has a few areas of the system that utilize a different channel than the default. The usage of these channels means that you can configure the log level and processors differently for those areas, without inundating the log file with lower level logs from other areas of the system.
Channel
Description
authentication
This logger channel is utilized by the AuthenticationController, and can show useful information about failed login attempts.Â
input_validation
This logger channel is utilized by the Input Validation framework and can display information regarding failed validations.
metadata
This logger channel is utilized by the MetadataManager and mainly provides information on when rebuilds occur.
rest
This channel is utilized by theÂ RestService object.
db
This channel is utilized by the databse for query logging.
Customization
You can use custom channels where ever you would like in your customizations, and configure them as outlined above, but if you need to send logs somewhere other than the Sugar log file, you will need to build out your own Handler. The following will walk through adding a custom Logging Handler that utilizes Monologs built-in Chrome Logger.
Adding a Custom Handler
Create the following file in ./custom/src/Logger/Handler/Factory/ folder.
./custom/src/Logger/Handler/Factory/Chrome.php
<?php
namespace Sugarcrm\Sugarcrm\custom\Logger\Handler\Factory;
use Monolog\Handler\ChromePHPHandler;


use Monolog\Handler\ChromePHPHandler;
use Sugarcrm\Sugarcrm\Logger\Handler\Factory;
class Chrome implements Factory
{
    public function create($level, array $config)
    {
        return new ChromePHPHandler($level);
    }
}
Once you have the new class in place, you will need to run a Quick Repair and Rebuild so that the new class is auto-loaded correctly. Then you can configure the handler for use in the Sugar config:
$sugar_config['logger']['channels']['<channel>']['handlers'] = 'Chrome';
Â Note: For more information, please refer to theÂ logger.channels.channel.handlersÂ documentation.
  	Last modified: 2023-02-03 21:04:03


Creating Custom Loggers
Custom Loggers
Custom loggers,Â defined in ./custom/include/SugarLogger/, can be used to write log entries to a centralized application management tool or to write messages to a developer tool such as FirePHP.
To do this, you can create a new instance class that implements the LoggerTemplate interface. The following is an example of how to create a FirePHP logger.
./custom/include/SugarLogger/FirePHPLogger.php.
<?php
// change the path below to the path to your FirePHP install
require_once('/path/to/fb.php');
class FirePHPLogger implements LoggerTemplate
{
    /** Constructor */
    public function __construct()
    {
        if (
            isset($GLOBALS['sugar_config']['logger']['default'])
            && $GLOBALS['sugar_config']['logger']['default'] == 'FirePHP'
        )
        {
            LoggerManager::setLogger('default','FirePHPLogger');
        }
    }
    /** see LoggerTemplate::log() */
    public function log($level, $message)
    {
        // change to a string if there is just one entry
        if ( is_array($message) && count($message) == 1 )
        {
            $message = array_shift($message);
        }
        switch ($level)
        {
            case 'debug':
                FB::log($message);
                break;
            case 'info':
                FB::info($message);
                break;
            case 'deprecated':
            case 'warn':
                FB::warn($message);
                break;
            case 'error':
            case 'fatal':
            case 'security':
                FB::error($message);
                break;
        }
    }
}


FB::error($message);
                break;
        }
    }
}
The only method that needs to be implemented by default is the log() method, which writes the log message to the backend. You can specify which log levels this backend can use in the constructor by calling the LoggerManager::setLogger() method and specifying the level to use for this logger in the first parameter; passing 'default' makes it the logger for all logging levels.
You will then specify your default logger as 'FirePHP' in your ./config_override.php file.
$sugar_config['logger']['default'] = 'FirePHP';
  	Last modified: 2023-02-03 21:04:03


Email
Overview
OutlinesÂ the relationshipsÂ between emails, email addresses, and bean records.
Email Tables
Table Name
Description
email_addresses
Each record in this table represents an email address in the system. Note that the invalid_email column and opt_out column are stored on this table, meaning that they are treated as properties of the email address itself, not a relationship attribute between a given email address and related record. This means if a Lead opts-out of a campaign, this email address will be considered opted-out in all contexts, not just with further correspondence with that specific Lead.
email_addr_bean_rel
The email_addr_bean_rel table maintains the relationship between the email address and its parent module record (Contacts, Accounts, Leads, Users, etc) to determine which record of the given module the email address belongs to. Note that this relationship table also has the primary_address and reply_to_address columns to indicate whether a given email address for a given record is the primary email address, the reply to email address, or some other address for the contact. A contactÂ can have one address flagged as primary, and one flagged as "Reply To". This can be the same email address or two different email addresses.
emails_email_addr_rel
The emails_email_addr_rel table maintains the relationships between the email address and email records. Note that this relationship table also has the address_type column to indicate if the email address is related to the email as a "To", "From", "CC", or "BCC" address. The valid values at the database level for this column are: from, to, cc, bcc.
emails
Each record in this table represents an email in the system. Note that emails fall outside the scope of this document, but are mentioned here for clarity, as the two modules are closely related.
emails_beans


emails_beans
Similar to the email_addr_bean_rel table, the emails_beans table maintains the relationship between an email record and any related records (Contacts, Accounts, Cases, etc.).
<module>
This is used to showÂ how an email address record relates to a recordÂ in anyÂ module is set on bean_module. If bean_module is set to Contacts, <module>Â would be the contacts table.
The following diagram illustrates table relationships between email addresses and other modules, email addresses and email records, and email records and other modules.
Helper Queries
Retrieve the Primary Email Address of a Contact
The following query will fetch the email address given a specific contacts id:
SELECT
  email_address
FROM email_addresses
JOIN email_addr_bean_rel eabr
  ON eabr.email_address_id = email_addresses.id
WHERE eabr.bean_module = "Contacts"
AND eabr.bean_id = "<contact id>"
AND email_addresses.invalid_email = 0
AND eabr.deleted = 0
AND eabr.primary_address = 1;
Retrieve All Records Related to an Email Address
The following query will fetch the id and module name of all records related to the specified email address:
SELECT
  bean_module,
  bean_id
FROM email_addr_bean_rel eabr
JOIN email_addresses
  ON eabr.email_address_id = email_addresses.id
WHERE email_addresses.email_address = "<email address>"
AND eabr.deleted = 0;
Retrieve All Emails Sent From An Email Address
The following query will fetchÂ all emails sent from aÂ specified email address:
SELECT
  emails.name,
  emails.date_sent
FROM emails
JOIN emails_email_addr_rel eear
  ON eear.email_id = emails.id
JOIN email_addresses
  ON eear.email_address_id = email_addresses.id
WHERE email_addresses.email_address = "<email address>"


WHERE email_addresses.email_address = "<email address>"
AND eear.address_type = "from"
AND eear.deleted = 0
Cleanup Duplicate Email Addresses
The following queries will remove any duplicateÂ email addresses.
Â First, create aÂ temporary tableÂ with distinct records from theÂ email_addr_bean_rel table:
create table email_addr_bean_rel_tmp_ful
SELECT
  *
FROM email_addr_bean_rel
WHERE deleted = '0'
GROUP BY email_address_id,
         bean_module,
         bean_id
ORDER BY primary_address DESC;
Next, clear out the email_addr_bean_rel table:
truncate email_addr_bean_rel;
Move the records from the temporary table back toÂ email_addr_bean_rel:
INSERT INTO email_addr_bean_rel
 SELECT
 *
 FROM email_addr_bean_rel_tmp;
Validate that all of the duplicates have been removed:
SELECT
 COUNT(*) AS repetitions,
 date_modified,
 bean_id,
 bean_module
FROM email_addr_bean_rel
WHERE deleted = '0'
GROUP BY bean_id,
 bean_module,
 email_address_id
HAVING repetitions > 1;
Finally, remove the temporary table:
drop table email_addr_bean_rel_tmp;
Email Address Validation
Sugar validates emails addresses according to theÂ RFC 5321Â andÂ RFC 5322Â standards. The following sections will detail how a developer can validate email addresses both server and client side.
Server Side
To validate an email address in a server-side context, the EmailAddress::isValidEmail() static method should be used. For example:
$emailAddress = "test@example.com";
$isValid = EmailAddress::isValidEmail($emailAddress);


$isValid = EmailAddress::isValidEmail($emailAddress);
TheÂ EmailAddress::isValidEmail method leverages the PHPMailer library bundled with Sugar, specifically the PHPMailer::validateAddress method, which validates the addressÂ according to theÂ RFC 5321Â and RFC 5322 standards.
Client Side
To validate an email address client-side context, theÂ app.utils.isValidEmailAddress()Â function can be used.
var emailAddress = "test@example.com";
var isValid = app.utils.isValidEmailAddress(emailAddress);
Note: This function is more permissive and does not conform exactly to the RFC standards used on the server. As such, theÂ email addressÂ will be validated again on theÂ serverÂ when theÂ record is saved, which could still fail validation.
TopicsMailer FactoryThe Mailer Factory, located in ./modules/Mailer/MailerFactory.php, helps developers generate outbound mailers for the system account as well as individual user accounts. The Mailer Factory is a replacement for SugarPHPMailer which is now deprecated.
  	Last modified: 2023-02-03 21:04:03


Mailer Factory
Overview
The Mailer Factory, located in ./modules/Mailer/MailerFactory.php, helps developers generate outbound mailersÂ for the system account as well as individual user accounts. The Mailer Factory is a replacement for SugarPHPMailer which is now deprecated.
Mailers
There areÂ two types of outbound mailers:Â System andÂ User. The follow sections will outline how to use each.
System Mailer
TheÂ system outbound mailer can be setÂ using theÂ getSystemDefaultMailerÂ method. This will set the mailer to use the system outbound email account.Â
Example
$mailer = MailerFactory::getSystemDefaultMailer();
User Mailer
The user outbound mailer can be setÂ using the getMailerForUserÂ method. This will set the mailer to use the outbound email account for a specific user.Â
Example
$user = BeanFactory::getBean("Users", 1);
$mailer = MailerFactory::getMailerForUser($user);
Populating the Mailer
Setting the Subject
To set the email subject, use theÂ setSubject method. It accepts a plain text string.
Example
$mailer->setSubject("Test Mail Subject");
Setting the Body
Depending on your email type, you can use the setTextBodyÂ and/or setHtmlBody methods respectively to populate the content of the email body.
Example
// Text Body
$mailer->setTextBody("This is a text body message");
// HTML Body
$mailer->setHtmlBody("This is an HTML body message.
 You can use html tags.");
Note:Â The email HTML body is not necessary if you have populated the text body.
Adding Recipients


Adding Recipients
To add recipients to your email, you can use theÂ addRecipientsTo, addRecipientsCc, or addRecipientsBcc methods . These methods require an EmailIdentity object as a parameter.
Example
$mailer->addRecipientsTo(new EmailIdentity('user1@yourcompany.crm', 'User 1'));
$mailer->addRecipientsCc(new EmailIdentity('user2@yourcompany.crm', 'User 2'));
$mailer->addRecipientsBcc(new EmailIdentity('user3@yourcompany.crm', 'User 3'));
Clearing Recipients
You can clear the current recipients specified in the mailer by using the clearRecipients method.
Example
$to = true;
$cc = true;
$bcc = true;
$mailer->clearRecipients($to, $cc, $bcc);
Adding Attachments
To add attachments,Â use theÂ addAttachment method.
Example
$path = "/path/to/your/document";
$mailer->addAttachment(new Attachment($path));
Sending Emails
Once your email is populated, you can send it using theÂ send method. TheÂ send method will returnÂ the content of the mail. If the Mailer Factory experiences an error, it will throw an exception. It is highly recommended to use a try and catch when sending emails.
Example
$mailSubject = "Test Mail Subject";
$mailHTML = "
SugarCRM

 Test body message";
$mailTo = array(
    0 => array(
        'name' => 'Test User',
        'email' => 'test@yourcompany.crm',
    ),
    1 => array(
        'name' => 'Other Recipient',
        'email' => 'email@addres'
    )
);


'email' => 'email@addres'
    )
);
$mailAttachment = "/path/to/pdf/files/document.pdf";
try {
    $mailer = MailerFactory::getSystemDefaultMailer();
    $mailTransmissionProtocol = $mailer->getMailTransmissionProtocol();
    $mailer->setSubject($mailSubject);
    $body = trim($mailHTML);
    $textOnly = EmailFormatter::isTextOnly($body);
    if ($textOnly) {
        $mailer->setTextBody($body);
    } else {
        $textBody = strip_tags(br2nl($body)); // need to create the plain-text part
        $mailer->setTextBody($textBody);
        $mailer->setHtmlBody($body);
    }
    $mailer->clearRecipients();
    foreach ($mailTo as $mailTo) {
        $mailer->addRecipientsTo(new \EmailIdentity($mailTo['email'], $mailTo['name']));
    }
    $mailer->addAttachment(new \Attachment($mailAttachment));
    $result = $mailer->send();
    if ($result) {
        // $result will be the body of the sent email
    } else {
        // an exception will have been thrown
    }
} catch (MailerException $me) {
    $message = $me->getMessage();
    switch ($me->getCode()) {
        case \MailerException::FailedToConnectToRemoteServer:
            $GLOBALS["log"]->fatal("BeanUpdatesMailer :: error sending email, system smtp server is not set");
            break;
        default:
            $GLOBALS["log"]->fatal("BeanUpdatesMailer :: error sending e-mail (method: {$mailTransmissionProtocol}), (error: {$message})");
            break;
    }
}


break;
    }
}
  	Last modified: 2023-02-03 21:04:03


Duplicate Check
Overview
The duplicate-check framework provides the capability to alter how the system searches for duplicate records in the database when creating records. For information on duplicate checking during imports, please refer to the index documentation.
Default Strategy
The default duplicate-check strategy, located in ./data/duplicatecheck/FilterDuplicateCheck.php, is referred to as FilterDuplicateCheck. This strategy utilizes the Filter APIÂ and a defined filter in the vardefs of the module to search for duplicates and rank them based on matching data.
Custom Filter
To alter a defined filter for a stockÂ a module, you only need to update the Vardefs using the Extensions framework. If you are working with a custom module, you can modify the vardefs inÂ ./modules/<module>/vardefs.php directly. The FilterDuplicateCheck Strategy accepts two properties in its metadata:
Name
Type
Description
filter_template
array
An array containing the Filter Definition for which fields to search for duplicates on. Please consult the Filter API for further information on the filter syntax
ranking_fields
array
A list of arrays with the following properties. The order in which you list the fields for ranking will determine the ranking score. The first ranks higher, than those after it.
in_field_name: Name of field in vardefs
dupe_field_name: Name of field returned by filter
Example
The following example will demonstrate how to manipulate the stock Accounts duplicate check to not filterÂ on the shipping address city. The default AccountÂ duplicate_check defintion, located in ./modules/Accounts/vardefs.php, is shown below.
...
'duplicate_check' => array(
    'enabled' => true,
    'FilterDuplicateCheck' => array(
        'filter_template' => array(
            array(
                '$or' => array(
                    array('name' => array('$equals' => '$name')),


array('name' => array('$equals' => '$name')),
                    array('duns_num' => array('$equals' => '$duns_num')),
                    array(
                        '$and' => array(
                            array('name' => array('$starts' => '$name')),
                            array(
                                '$or' => array(
                                    array('billing_address_city' => array('$starts' => '$billing_address_city')),
                                    array('shipping_address_city' => array('$starts' => '$shipping_address_city')),
                                )
                            ),
                        )
                    ),
                )
            ),
        ),
        'ranking_fields' => array(
            array('in_field_name' => 'name', 'dupe_field_name' => 'name'),
            array('in_field_name' => 'billing_address_city', 'dupe_field_name' => 'billing_address_city'),
            array('in_field_name' => 'shipping_address_city', 'dupe_field_name' => 'shipping_address_city'),
        )
    )
),
...
Â To add or remove fields from the check, you will need to manipulateÂ $dictionary['<module>']['duplicate_check']['FilterDuplicateCheck']['filter_template']Â and Â Â $dictionary['<module>']['duplicate_check']['FilterDuplicateCheck']['ranking_fields']Â . For our example, we will simply remove the references toÂ shipping_address_city. Â It is important to familiarize yourself with filter operators before making any changes.
./custom/Extension/modules/Accounts/Ext/Vardefs/newFilterDuplicateCheck.php
<?php
$dictionary['Account']['duplicate_check']['FilterDuplicateCheck'] = array(
    'filter_template' => array(
        array(
            '$or' => array(
                array('name' => array('$equals' => '$name')),


array('name' => array('$equals' => '$name')),
                array('duns_num' => array('$equals' => '$duns_num')),
                array(
                    '$and' => array(
                        array('name' => array('$starts' => '$name')),
                        array(
                            '$or' => array(
                                array('billing_address_city' => array('$starts' => '$billing_address_city')),
                            )
                        ),
                    )
                ),
            )
        ),
    ),
    'ranking_fields' => array(
        array('in_field_name' => 'name', 'dupe_field_name' => 'name'),
        array('in_field_name' => 'billing_address_city', 'dupe_field_name' => 'billing_address_city'),
    )
);
Finally, navigate to Admin > Repair > Quick Repair and Rebuild. The system will then enable the custom duplicate-check class.
If you want to disable the duplicate check entirely, you can setÂ $dictionary['<module>']['duplicate_check']['enabled'] to false in your vardefs and run a Quick Repair and Rebuild.
$dictionary['Account']['duplicate_check']['enabled'] = false;
Custom Strategies
Custom duplicate-check class files are stored under ./custom/data/duplicatecheck/. The files in this directory can be enabled on a module's duplicate_check property located inÂ ./custom/Extension/modules/<module>/Ext/Vardefs/. Only one duplicate-check class can be enabled on a module at a given time.
Duplicate Check Class
To create a custom duplicate-check strategy, you need to create a custom duplicate-check class that extends the base DuplicateCheckStrategy, ./data/duplicatecheck/DuplicateCheckStrategy.php. To work, this custom class requires the implementation of two methods:
Method Name
Description
setMetadata


Method Name
Description
setMetadata
Sets up properties for duplicate-check logic based on the passed-in metadata
findDuplicates
Finds possible duplicate records for a given set of field data
Example
The following example will create a new duplicate-check class called "OneFieldDuplicateCheck" that will query the database based on the configured field for records that contain data similar to that one field:
./custom/data/duplicatecheck/OneFieldDuplicateCheck.php
<?php
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
class OneFieldDuplicateCheck extends DuplicateCheckStrategy
{
    protected $field;
    public function setMetadata($metadata)
    {
        if (isset($metadata['field'])) {
            $this->field = $metadata['field'];
        }
    }
    public function findDuplicates()
    {
        if (empty($this->field)){
            return null;
        }
        $Query = new SugarQuery();
        $Query->from($this->bean);
        $Query->where()->ends($this->field,$this->bean->{$this->field});
        $Query->limit(10);
        //Filter out the same Bean during Edits
        if (!empty($this->bean->id)) {
            $Query->where()->notEquals('id',$this->bean->id);
        }
        $results = $Query->execute();
        return array(
            'records' => $results
        );
    }
}
Vardef Settings
The duplicate-check vardef settings are configured on each module and can be altered using the Extension framework.
Name
Type
Description
enabled
boolean
Whether or not duplicate-check framework is enabled on the module
<class_name>
array


boolean
Whether or not duplicate-check framework is enabled on the module
<class_name>
array
The class name that will provide duplicate checking, set to an array of metadata that gets passed to the duplicate-check class
Â If you want to enable the OneFieldDuplicateCheck strategy (shown above) for the Accounts module, you must create the following file:
./custom/Extension/modules/Accounts/Ext/Vardefs/newDuplicateCheck.php
<?php
$dictionary['Account']['duplicate_check'] = array(
	'enabled' => true,
	'OneFieldDuplicateCheck' => array(
            'field' => 'name'
        )
);
Finally, navigate to Admin > Repair > Quick Repair and Rebuild. The system will then enable the custom duplicate-check class.
Programmatic Usage
You can also use the duplicate-check framework in code such as in a logic hook or a scheduler. The following example shows how to use the module's defined duplicate-check strategy when utilizing a bean object by simply calling the findDuplicates method on the bean object:
$account = BeanFactory::newBean('Accounts');
$account->name = 'Test';
$duplicates = $account->findDuplicates();
if (count($duplicates['records'])>0){
    $GLOBALS['log']->fatal("Duplicate records found for Account");
}
  	Last modified: 2023-02-03 21:04:03