Global Search
Overview
How to customize the global search results.
Configuring Search Results
The Search-List view, located in ./clients/base/views/search-list/,Â is responsible for theÂ global search results page. By default, it contains the rowactionsÂ that are available for each result.Â The following sections will outline how to configure the primary and secondary fields.
Primary and Secondary Fields
Each row returned from the global searchÂ isÂ split into two lines. The first line contains the primary fields and avatar. A primary field is a field considered to be important to the identification of a record and is usually composed of the records name value and a link to the record.Â The avatar is an icon specific to the module the results are coming from.


The second lineÂ containsÂ the highlighted matches and anyÂ secondary fields specified. A highlight is a hit returned by elastic search. Highlights are driven from elastic and no definition is needed in the metadata to define them. Both primary and secondary fields can be highlighted. A secondary field is a field that has regularly accessed data. It is important to noteÂ that the initial type-ahead drop-down will not show specified secondary fields unless the user hits enter and is taken to the search results page. Different modules have different secondary fields. For example, an account's secondary fields are email and phone number, while cases' secondary fields are case ID number and related account.
To leverage the metadata manager, the global search view reuses the same metadata format as other views.Â You can add any indexed field in elastic searchÂ to the search list view. An example of the stock metadata is shown below.


./modules/{module}/clients/base/views/search-list/search-list.php
<?php
$viewdefs[$moduleName]['base']['view']['search-list'] = array(
    'panels' => array(
        array(
            'name' => 'primary', // This is mandatory and the word `primary` can not be changed.
            'fields' => array(
                /*
                 * Put here the list of primary fields.
                 * You can either define a field as a string and the metadata
                 * manager will load the field vardefs, or you can define as an
                 * array if you want to add properties or override vardefs
                 * properties just for the search results view.
                 *
                 * You most likely want to show the module icon on the left of the


*
                 * You most likely want to show the module icon on the left of the
                 * view. For this, you need to add the following picture field as
                 * a primary field.
                 */
                array(
                    'name' => 'picture',
                    'type' => 'avatar',
                    'size' => 'medium',
                    'css_class' => 'pull-left',
                ),
                array(
                    'name' => 'name',
                    'type' => 'name',
                    'link' => true,
                    'label' => 'LBL_SUBJECT',
                ),
            ),
        ),
        /*
         * If you don't want secondary fields, you can remove the following array.
         */
        array(
            'name' => 'secondary', // This is mandatory and the word `secondary` can not be changed.


'fields' => array(
                /*
                 * Put here the list of secondary fields
                 */
                'status', // This field is defined as a string for example.
                array(
                    'name' => 'description',
                    'label' => 'LBL_SEARCH_DESCRIPTION',
                ),
            ),
        ),
    ),
);
Note: You can technically override the row actions for the results of a module, however,Â the view currently only supports the preview action so itÂ is not recommended to customizes these actions.
Examples
The following examplesÂ demonstrate how to modify the primary and secondary rows of the global search.
Adding Fields to the Primary Row
This example will demonstrate how to add "Mobile" into the search-list view's primaryÂ row for Contact module results.


To accomplish this, you can duplicate ./modules/Contacts/clients/base/views/search-list/search-list.php to Â Â ./custom/modules/Contacts/clients/base/views/search-list/search-list.phpÂ if it doesn't exist. Once in place, add your phone_mobileÂ definition to theÂ $viewdefs['Contacts']['base']['view']['search-list']['panels'] array definition with the name attribute equal to "primary'. An example is shown below:
./custom/modules/Contacts/clients/base/views/search-list/search-list.php
<?php
$viewdefs['Contacts']['base']['view']['search-list'] = array(
    'panels' => array(
        array(


'panels' => array(
        array(
            'name' => 'primary',
            'fields' => array(
                array(
                    'name' => 'picture',
                    'type' => 'avatar',
                    'size' => 'medium',
                    'readonly' => true,
                    'css_class' => 'pull-left',
                ),
                array(
                    'name' => 'name',
                    'type' => 'name',
                    'link' => true,
                    'label' => 'LBL_SUBJECT',
                ),
                // Adding the mobile into first row of the results
                'phone_mobile',
            ),
        ),
        array(
            'name' => 'secondary',
            'fields' => array(
                array(
                    'name' => 'email',


array(
                    'name' => 'email',
                    'label' => 'LBL_PRIMARY_EMAIL',
                ),
            ),
        ),
    ),
);
Once in place, navigate to Admin > Repair > Quick Repair and Rebuild. Your changes will then be reflected in the system.
Adding Fields to the Secondary Row
This example will demonstrate how to add "Mobile" into the search-list view's secondary row for Contact module results.Â It is important to noteÂ that the initial type-ahead drop-down will not show specified secondary fields unless the user hits enter and is taken to the search results page.
In this example, we are going to add Portal Name into search-list for Contact module results.


To accomplish this, you can duplicate ./modules/Contacts/clients/base/views/search-list/search-list.php to Â Â ./custom/modules/Contacts/clients/base/views/search-list/search-list.phpÂ if it doesn't exist. Once in place, add your phone_mobile definition to theÂ $viewdefs['Contacts']['base']['view']['search-list']['panels'] array definition with the name attribute equal to "secondary'. An example is shown below:
<?php
$viewdefs['Contacts']['base']['view']['search-list'] = array(
    'panels' => array(
        array(
            'name' => 'primary',
            'fields' => array(
                array(
                    'name' => 'picture',


array(
                    'name' => 'picture',
                    'type' => 'avatar',
                    'size' => 'medium',
                    'readonly' => true,
                    'css_class' => 'pull-left',
                ),
                array(
                    'name' => 'name',
                    'type' => 'name',
                    'link' => true,
                    'label' => 'LBL_SUBJECT',
                ),
            ),
        ),
        array(
            'name' => 'secondary',
            'fields' => array(
                array(
                    'name' => 'email',
                    'label' => 'LBL_PRIMARY_EMAIL',
                ),
                // Adding mobile to second row for search results in globalsearch
                'phone_mobile'
            ),
        ),
    ),
);


'phone_mobile'
            ),
        ),
    ),
);
Once in place, navigate to Admin > Repair > Quick Repair and Rebuild. Your changes will then be reflected in the system.
  	Last modified: 2023-02-03 21:04:03


Validation Constraints
Overview
This article will cover how to add validation constraints toÂ your customÂ code.
Constraints
Validation constraintsÂ allow developers to validate user input
Symfony Validation Constraints
TheÂ Symfony's Validation library, located in ./vendor/symfony/validator/,Â contains many open source constraints. You can find the full list of constraints documented in Symphony'sÂ Validation Constraints. They are listed below for your reference.
Basic Constraints
NotBlank
Blank
NotNull
IsNull
IsTrue
IsFalse
Type
String Constraints
Email
Length
Url
Regex
Ip
Uuid
Number Constraints
Range
Comparison Constraints
EqualTo
NotEqualTo
IdenticalTo
NotIdenticalTo
LessThan


NotEqualTo
IdenticalTo
NotIdenticalTo
LessThan
LessThanOrEqual
GreaterThan
GreaterThanOrEqual
Date Constraints
Date
DateTime
Time
Collection Constraints
Choice
Collection
Count
UniqueEntity
Language
Locale
Country
File Constraints
File
Image
Financial and other Number Constraints
Bic
CardScheme
Currency
Luhn
Iban
Isbn
Issn
Other Constraints
Callback
Expression
All
UserPassword
Valid
Sugar Constraints
Sugar containsÂ its ownÂ set ofÂ validation constraints. These constraints extendÂ from Symfony's validation constraintsÂ and are located in ./src/Security/Validator/Constraints ofÂ your Sugar installation.
Bean/ModuleName


Bean/ModuleName
Bean/ModuleNameValidator
ComponentName
ComponentNameValidator
Delimited
DelimitedValidator
DropdownList
DropdownListValidator
File
FileValidator
Guid
GuidValidator
InputParameters
InputParametersValidator
JSON
JSONValidator
Language
LanguageValidator
LegacyCleanString
LegacyCleanStringValidator
Mvc/ModuleName
Mvc/ModuleNameValidator
PhpSerialized
PhpSerializedValidator
Sql/OrderBy
Sql/OrderByValidator
Sql/OrderDirection
Sql/OrderDirectionValidator
SugarLogic/FunctionName
SugarLogic/FunctionNameValidator
Custom Constraints


SugarLogic/FunctionNameValidator
Custom Constraints
If you find that the existing constraints do not meet your needs,Â you can also create your own. These constraints exist underÂ ./custom/src/Security/Validator/Constraints/.Â The following section will outline the details.Â The example below will demonstrate how to create a phone number validation constraint.Â
The constraint file will contain your validations error message.
./custom/src/Security/Validator/Constraints/Phone.php
<?php
namespace Sugarcrm\Sugarcrm\custom\Security\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
 *
 * @see PhoneValidator
 *
 */
class Phone extends Constraint
{
    public $message = 'Phone number violation: %msg%';
}


{
    public $message = 'Phone number violation: %msg%';
}
The validator file will contain the logic to determine whether the value meets the requirements.
./custom/src/Security/Validator/Constraints/PhoneValidator.php
<?php
namespace Sugarcrm\Sugarcrm\custom\Security\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
 *
 * Phone validator
 *
 */
class PhoneValidator extends ConstraintValidator
{
    /*
     * Phone Pattern Examples;
     * +1 12 3456789
     * +1.12.3456789
     */


* +1.12.3456789
     */
    const PHONE_PATTERN = '/^([+]?\d+(?:[ \.]\d+)*)$/';
    /**
     * {@inheritdoc}
     */
    public function validate($value, Constraint $constraint)
    {
        if (!$constraint instanceof Phone) {
            throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\Phone');
        }
        if (null === $value || '' === $value) {
            return;
        }
        if (!is_scalar($value) && !(is_object($value) && method_exists($value, '__toString'))) {
            throw new UnexpectedTypeException($value, 'string');
        }


throw new UnexpectedTypeException($value, 'string');
        }
        $value = (string) $value;
        // check for allowed characters
        if (!preg_match(self::PHONE_PATTERN, $value)) {
            $this->context->buildViolation($constraint->message)
                ->setParameter('%msg%', 'invalid format')
                ->setInvalidValue($value)
                ->addViolation();
            return;
        }
    }
}
Once the files are in place,Â navigate to Admin > Repairs and run a Quick Repair and Rebuild. Once completed, your constraint will be ready for use.
Using Constraints in API End Points


Using Constraints in API End Points
In this section, we will create a custom endpoint and triggerÂ the custom phone constraint we created above.Â TheÂ code below will create aÂ RESTÂ API endpoint path ofÂ /phoneCheck:
./custom/clients/base/api/MyEndpointsApi.php
<?php
if (!defined('sugarEntry') || !sugarEntry) {
    die('Not A Valid Entry Point');
}
use Sugarcrm\Sugarcrm\Security\Validator\ConstraintBuilder;
use Sugarcrm\Sugarcrm\Security\Validator\Validator;
class MyEndpointsApi extends SugarApi
{
    public function registerApiRest()
    {
        return array(
            //POST
            'MyEndpointsApi' => array(
                //request type


'MyEndpointsApi' => array(
                //request type
                'reqType' => 'POST',
                //endpoint path
                'path' => array('phoneCheck'),
                //endpoint variables
                'pathVars' => array(''),
                //method to call
                'method' => 'phoneCheck',                //minimum api version                'minVersion' => 10,
                //short help string to be displayed in the help documentation
                'shortHelp' => 'An example of a POST endpoint to validate phone numbers',
                //long help to be displayed in the help documentation
                'longHelp' => 'custom/clients/base/api/help/MyEndPoint_phoneCheck_help.html',
            ),
        );
    }
    /**
     * Method to be used for my MyEndpointsApi/phoneCheck endpoint
     */


*/
    public function phoneCheck($api, $args)
    {
        $validator = Validator::getService();
        /**
         * Validating Phone Number
         */
        $phoneContraintBuilder = new ConstraintBuilder();
        $phoneConstraints = $phoneContraintBuilder->build(
            array(
                'Assert\Phone',
            )
        );
        $errors = $validator->validate($args['phone'], $phoneConstraints);
        if (count($errors) > 0) {
            /*
             * Uses a __toString method on the $errors variable which is a
             * ConstraintViolationList object. This gives us a nice string
             * for debugging.
             */
            $errorsString = (string) $errors;
            // include/api/SugarApiException.php


// include/api/SugarApiException.php
            throw new SugarApiExceptionInvalidParameter($errorsString);
        }
        //custom logic
        return $args;
    }
}
After creating the endpoint, navigate to Admin > Repairs and run a Quick Repair and Rebuild. The API will then be ready to use.
Valid Payload - POST to /phoneCheck
The example below demonstrates a valid phone number post the /phoneCheckÂ endpoint.
{    phone: "+1.23.456.789"}
Result
{    phone: "+1.23.456.789"}
Invalid Payload - POST to /phoneCheck
The example below demonstrates an invalid phone number post theÂ /phoneCheckÂ endpoint.
{    phone: "Invalid+1.23.456.789"}
Result


{    phone: "Invalid+1.23.456.789"}
Result
{    "error":"invalid_parameter",    "error_message":"Invalid+1.23.456.789:\n    Phone number violation: invalid format\n"}
  	Last modified: 2023-02-03 21:04:03


Email Addresses
Overview
Recommended approaches when working with email addresses in Sugar.
Client Side
Recommended approaches when accessing email addresses in Sugar from a client.
Sidecar
Sidecar is the JavaScript UI framework that users interact within their browsers.
Fetching Email Addresses in Sidecar
In Sidecar, the email field will return an array of email addresses and their properties for the record. Given the model, you can fetch it using:
var emailAddresses = model.get('email');
Note:Â In the past, developers could useÂ model.get("email1") to fetch the primary email address. While this currently does work,Â these legacy email fields areÂ deprecatedÂ and may be subject to removal in an upcoming Sugar release.
Fetching a Primary Email Address in Sidecar
To fetch the primary email address for a bean, you can use app.utils.getPrimaryEmailAddress():


var primaryEmailAddress = app.utils.getPrimaryEmailAddress(model);
Fetching an Email Address by Properties in Sidecar
To fetch an email address based on properties such as invalid_email, you can use app.utils.getEmailAddress(). This function will return the first email address that matches the options or an empty string if not found. An example is shown below:
var emailAddress = app.utils.getEmailAddress(model, {invalid_email: true});
If you have complex filtering rules, you can use _.find() to fetch an email address:
var emailAddress = _.find(model.get('email'), function(emailAddress){
    if (emailAddress.invalid_email == true) {
        return emailAddress;
    }
});
Validating Email Addresses in Sidecar
To validate an email address, you can use app.utils.isValidEmailAddress():


To validate an email address, you can use app.utils.isValidEmailAddress():
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.Â More information can be found in the email address validation section.
Iterating Email Address in Sidecar
To iterate through email addresses on a model, you can use _.each():
_.each(model.get('email'), function(emailAddress) {
    console.log(emailAddress.email_address);
});
Updating Email Addresses in Sidecar
This section covers how to manipulate the email addresses for a model.
Adding Email Addresses in Sidecar


Adding Email Addresses in Sidecar
In Sidecar, you can add email addresses to a model using the custom function below:
function addAddress(model, email) {
    var existingAddresses = model.get('email') ? app.utils.deepCopy(model.get('email')) : [],
        dupeAddress = _.find(existingAddresses, function(address){
            return (address.email_address === email);
        }),
        success = false;
    if (_.isUndefined(dupeAddress)) {
        existingAddresses.push({
            email_address: email,
            primary_address: (existingAddresses.length === 0),
            opt_out: app.config.newEmailAddressesOptedOut || false
        });
        model.set('email', existingAddresses);
        success = true;
    }


success = true;
    }
    return success;
}
Removing Email Addresses in Sidecar
In Sidecar, you can remove email addresses from a model using the custom function below:
function removeAddress(model, email) {
    var existingAddresses = app.utils.deepCopy(model.get('email'));
    var index = false;
    _.find(existingAddresses, function(emailAddress, idx){
        if (emailAddress.email_address === email) {
            index = idx;
            return true;
        }
    });
    var primaryAddressRemoved = false;
    if (index !== false) {
        primaryAddressRemoved = !!existingAddresses[index]['primary_address'];
    }
    //Reject this index from existing addresses


}
    //Reject this index from existing addresses
    existingAddresses = _.reject(existingAddresses, function (emailInfo, i) { return i == index; });
    // If a removed address was the primary email, we still need at least one address to be set as the primary email
    if (primaryAddressRemoved) {
        //Let's pick the first one
        var address = _.first(existingAddresses);
        if (address) {
            address.primary_address = true;
        }
    }
    model.set('email', existingAddresses);
    return primaryAddressRemoved;
}
Server Side
Recommended approaches when accessing email addresses in Sugar from the server.
SugarBean
The SugarBean is Sugars PHP core object model.
Fetching Email Addresses Using the SugarBean


Fetching Email Addresses Using the SugarBean
Using the SugarBean, the $bean->emailAddress->addressesÂ property will return an array of email addresses and its properties. TheÂ $bean->emailAddressÂ property makes use of the EmailAddress class which is located inÂ ./modules/EmailAddresses/EmailAddress.php. An example is shown below:
$emailAddresses = $bean->emailAddress->addresses;
Fetching a Primary Email Address Using the SugarBean
To fetch the primary email address for a bean, you can use $bean->emailAddress->getPrimaryAddress():
$primaryEmailAddress = $bean->emailAddress->getPrimaryAddress($bean);
Another alternative is to use theÂ email_addresses_primary relationship:
$primaryEmailAddress = false;
if ($this->load_relationship('email_addresses_primary')) {


if ($this->load_relationship('email_addresses_primary')) {
    $relatedBeans = $this->email_addresses_primary->getBeans();
    if (!empty($relatedBeans)) {
        $primaryEmailAddress = current($relatedBeans);
    }
}
You may also choose to iterate the email address list with a foreach(). An example function is shown below:
function getPrimaryEmailAddress($bean)
{
    foreach ($bean->emailAddress->addresses as $emailAddress) {
        if ($emailAddress['primary_address'] == true) {
            return $emailAddress;
        }
    }
    return false;
}
Fetching an Email Address by Properties Using the SugarBean
To fetch an email address based on properties such as invalid_email, you can use a foreach():
$result = false;


$result = false;
foreach ($bean->emailAddress->addresses as $emailAddress) {
    if ($emailAddress['invalid_email']) {
        $result = $emailAddress;
        break;
    }
}
Validating Email Addresses Using the SugarBean
To validate an email address, you can use $bean->emailAddress->isValidEmail():
$isValid = $bean->emailAddress->isValidEmail($emailAddress);
Note: 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. More information can be found in the email address validation section.
Iterating Email Addresses Using the SugarBean


Iterating Email Addresses Using the SugarBean
To iterate through email addresses on a bean, you can use foreach():
foreach ($bean->emailAddress->addresses as $emailAddress) {
    $GLOBALS['log']->info($emailAddress['email_address']);
}
Fetching Beans by Email Address Using the SugarBean
To fetch all beans related to an email address you can useÂ getBeansByEmailAddress():
$beans = $bean->emailAddress->getBeansByEmailAddress($emailAddress);
If you don't have a bean available, you may choose to create a new EmailAddress object:
$sea = BeanFactory::newBean('EmailAddresses');
$sea->getBeansByEmailAddress($emailAddress);
Updating Email Addresses Using the SugarBean
This section covers how to manipulate the email addresses for a bean.


This section covers how to manipulate the email addresses for a bean.
Adding Email Addresses Using the SugarBean
To add an email address to the bean, you can useÂ $bean->emailAddress->addAddress():
$bean->emailAddress->addAddress('address@sugar.crm');
Note: The addAddress() function has additional parameters that are defaulted for determining if the email address is a primary, reply to, invalid, or opted out email address. You can also specify an id for the email address and whether or not the email address should be validated.
function addAddress($addr, $primary=false, $replyTo=false, $invalid=false, $optOut=false, $email_id = null, $validate = true)
Removing Email Addresses Using the SugarBean
To remove an email address you can use $bean->emailAddress->removeAddress():


To remove an email address you can use $bean->emailAddress->removeAddress():
$bean->emailAddress->removeAddress('address@sugar.crm');
PDF Templates
Using the Sugar PDF templates, you can reference the primary email address of the bean using:
{$fields.email_addresses_primary.email_address}
REST API
Sugar comes out of the box with an API that can be called from custom applications utilizing the REST interface. The API can be used to mass create and update records in Sugar with external data. For more information on the REST API in Sugar, please refer to the Web Services documentation.
Creating Email Addresses Using the REST API


Creating Email Addresses Using the REST API
When creating records in Sugar through the API, modules with relationships to email addresses can utilize the email link field to specify email addresses for a record. Using the email link field, you can specify multiple email addresses to assign to the record. You may specify the following additional information regarding each email address being added:
Property
Description
invalid_email
Specify this email address as being invalid
opt_out
Specify this email address as being opted out. More information on opt-outs can be found in the Emails documentation.
primary_address
Specify this email address as the primary
Using the /<module>Â POST endpoint, you can send the following JSON payload to create a contact record with multiple email addresses using the email link field: POST URL: http://<site url>/rest/v<version>/Contacts
{
   "first_name":"Rob",


{
   "first_name":"Rob",
   "last_name":"Robertson",
   "email":[
      {
         "email_address":"rob.robertson@sugar.crm",
         "primary_address":"1",
         "invalid_email":"0",
         "opt_out":"0"
      },
      {
         "email_address":"rob@sugar.crm",
         "primary_address":"0",
         "invalid_email":"0",
         "opt_out":"1"
      }
   ]
}
For more information on the /<module>/:record POST endpoint, you can refer to your instance's help documentation found at:
http://<site url>/rest/v<version>/help
Or you can reference the <module> POST PHP example.


Or you can reference the <module> POST PHP example.
Updating Email Addresses Using the REST API
When updating existing records in Sugar through the API, modules with relationships to email addresses can use the email link field to specify email addresses for a record. Using the email link field, you can specify multiple email addresses to update the record with. You may specify the following additional information regarding each email address being added:
invalid_email : Specify this email address as being invalid
opt_out : Specify this email address as being opted out
primary_address : Specify this email address as the primary
Using the /<module>/:record PUT endpoint, you can send the following JSON payload to update a contact record with multiple email addresses: PUT URL: http://<site url>/rest/v<version>/Contacts/<record id>
{
   "email":[
      {


{
   "email":[
      {
         "email_address":"rob.robertson@sugar.crm",
         "primary_address":"1",
         "invalid_email":"0",
         "opt_out":"0"
      },
      {
         "email_address":"rob@sugar.crm",
         "primary_address":"0",
         "invalid_email":"0",
         "opt_out":"1"
      }
   ]
}
For more information on the /<module>/:record PUT endpoint, you can refer to your instance's help documentation found at:
http://<site url>/rest/v<version>/help
You want to reference the <module>/:record PUT PHP example.
Legacy Email Fields


Legacy Email Fields
The legacy email fields in SugarÂ areÂ deprecatedÂ and may be subject to removal in an upcoming Sugar release. When using the email1 field, the default functionality is to import the email address specified as the primary address.
Legacy Email Field
Description
email1
The text value of primary email address. Does not indicate if the email address is valid or opted-out.
email2
The text value of first non-primary email address. Does not indicate if the email address is valid or opted-out.
Note: For importing multiple email addresses with properties, you will need to use the email link field.
Creating Email Addresses Using Direct SQL
When importing records into Sugar directly via the database, it is important that you understand the data structure involved before loading data. Email addresses are not stored directly on the table for the module being imported in but are related via the email_addr_bean_rel table.


The table structure for email addresses can be seen from the database via the following SQL statement:
SELECT
 email_addr_bean_rel.bean_id,
 email_addr_bean_rel.bean_module,
 email_addresses.email_address
FROM email_addr_bean_rel
INNER JOIN email_addresses
 ON email_addresses.id = email_addr_bean_rel.email_address_id
 AND email_addr_bean_rel.deleted = 0
WHERE email_addresses.deleted = 0;
Checking for Duplicates


WHERE email_addresses.deleted = 0;
Checking for Duplicates
Email addresses can become duplicated in Sugar from a variety of sources including API calls, imports, and from data entry. There are a few ways to have the system check for duplicate contact records, but not many of those methods work for checking email addresses for duplicates. The following section will demonstrate how to find and clean up duplicate email addresses using SQL.
The following SQL query can be used to locate if any email addresses are being used against more than one bean record within Sugar:
SELECT
 email_addresses.email_address,
 COUNT(*) AS email_address_count
FROM email_addr_bean_rel
INNER JOIN email_addresses
 ON email_addresses.id = email_addr_bean_rel.email_address_id
 AND email_addr_bean_rel.deleted = 0


AND email_addr_bean_rel.deleted = 0
WHERE email_addresses.deleted = 0
GROUP BY email_addresses.email_address
HAVING COUNT(*) > 1;
Note: If you convert a Lead record to a Contact record, both the Lead and the Contact will be related to the same Email Address and will return as having duplicates in this query. You can add the following line to the WHERE clause to filter the duplicate check down to only one bean type:
AND email_addr_bean_rel.bean_module = 'Contacts'
Email addresses can not only beÂ duplicated in the system but can occasionally be missing critical data. Each bean record with an email address assigned to itÂ should have an email address designated the primary. The following query will locate any bean records that have at least one email address, where there is not an email address designated as the primary:
SELECT


SELECT
 email_addr_bean_rel.bean_module,
 email_addr_bean_rel.bean_id,
 COUNT(*) AS email_count,
 COUNT(primary_email_addr_bean_rel.id) AS primary_email_count
FROM email_addr_bean_rel
LEFT JOIN email_addr_bean_rel primary_email_addr_bean_rel
 ON primary_email_addr_bean_rel.bean_module = email_addr_bean_rel.bean_module
 AND primary_email_addr_bean_rel.bean_id = email_addr_bean_rel.bean_id
 AND primary_email_addr_bean_rel.primary_address = '1'
 AND primary_email_addr_bean_rel.deleted = '0'
WHERE email_addr_bean_rel.deleted = '0'


WHERE email_addr_bean_rel.deleted = '0'
GROUP BY email_addr_bean_rel.bean_module,
 email_addr_bean_rel.bean_id
HAVING primary_email_count < 1;
Note: If you are a SugarCloud customer, you can open up a case with Sugar Support to have this query run for you.
Removing Duplicates
If it is determined you have duplicate email addresses being used in your system, you can use the following query to clean up the records:
START TRANSACTION;
CREATE
 TABLE email_addr_bean_rel_tmp
SELECT
 *
FROM email_addr_bean_rel
WHERE deleted = '0'
GROUP BY email_address_id,
 bean_module,
 bean_id
ORDER BY primary_address DESC;
TRUNCATE TABLE email_addr_bean_rel;


TRUNCATE TABLE email_addr_bean_rel;
INSERT INTO email_addr_bean_rel
 SELECT
 *
 FROM email_addr_bean_rel_tmp;
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;
COMMIT;
Note: If you are a SugarCloud customer, you can open up a case with Sugar Support to have this query run for you.
  	Last modified: 2023-02-03 21:04:03


SugarBPM
Introduction
SugarBPMâ¢ automation suite is the next-generation workflow management tool for Sugar. Introduced in 7.6, SugarBPM is loosely based on BPMN process notation standards and provides a simple drag-and-drop user interface along with an intelligent flow designer to allow for the creation of easy yet powerful process definitions.
Note:Â SugarBPMâ¢ is not available for Sugar Professional.
SugarBPM enables administrators to streamline common business processes by managing approvals, sales processes, call triaging, and more. The easy-to-use workflow tool adds advanced BPM functionality to the core Sugar software stack.


A business process is a set of logically related tasks that are performed in order to achieve a specific organizational goal. It presents all of the tasks that must be completed in a simplified and streamlined format. SugarBPM empowers Sugar administrators, allowing for the automation of vital business processes for their organization. The SugarBPM automation suite features an extensive toolbox of modules that provide the ability to easily create digital forms and map out fully functioning workflows.
SugarBPM Modules
SugarBPM is broken down into four distinct modules, each with its own area of responsibility. Three of the four SugarBPM modules have access control settings that restrict its visibility to System Administrators, Module Administrators, and Module Developers.
Process Definitions


Process Definitions
A process definition defines the steps in an overall business process. Process definitions are created by either a Sugar administrator, a module Administrator or a module Developer. The process definition consists of a collection of activities and their relationships, criteria to indicate the start and end of the process, and information about the individual activities (e.g., participants) contained within the business process.
Business Rules


Business Rules
A business rule is a reusable set of conditions and outcomes that can be embedded in a process definition. The set of rules may enforce business policy, make a decision, or infer new data from existing data. For example, if Sally manages all business opportunities of $10,000 or more, and Chris manages all business opportunities under $10,000, a business rule can be created and used by all relevant process definitions based on the same target module to ensure that the assignment policy is respected. In the case of an eventual personnel change, only the business rule will need to be edited to affect all related processes.
Email Templates
A process email template is required in order to include a Send Message event in a process definition. The SugarCRM core product includes several places where email templates can be created for different purposes, but SugarBPM requires all sent messages to be created via the Process Email Templates module.
Processes


Processes
A process is a running instance of a process definition. A single process begins every time a process definition meeting certain criteria is executed. For example, a single process definition could be created to automate quote approvals, but because users may engage in several quote approvals per day, each approval will be represented by a separate process instance, all governed by the single process definition.
Database Tables
Information surrounding various portions of SugarBPM can be found in the following SugarBPM database tables:
Table name
Description
pmse_bpm_access_management
This table is not used.
pmse_bpm_activity_definition
Holds definition data for an activity. Maps directly to the pmse_bpmn_activity table and may, in the future, be merged with the pmse_bpmn_activity table to prevent forked data backends.
pmse_bpm_activity_step
This table is not used.


pmse_bpm_activity_step
This table is not used.
pmse_bpm_activity_user
This table is not used.
pmse_bpm_config
This table is not used.
pmse_bpm_dynamic_forms
Holds module specific form information - basically viewdefs for a module - for a process.
pmse_bpm_event_definition
Holds definition data for an event. Maps directly to the pmse_bpmn_eventÂ table and may, in the future, be merged with the pmse_bpmn_event table to prevent forked data backends.
pmse_bpm_flow
Holds information about triggered process flows as well as state of any process that is currently running.
pmse_bpm_form_action
Holds information about a process that has been acted upon.


Holds information about a process that has been acted upon.
pmse_bpm_gateway_definition
Holds definition data for a gateway. Maps directly to the pmse_bpmn_eventÂ table and may, in the future, be merged with the pmse_bpmn_gateway table to prevent forked data backends.
pmse_bpm_group
This table is not used.
pmse_bpm_group_user
This table is not used.
pmse_bpm_notes
Holds notes saved for a process record.
pmse_bpm_process_definition
Holds definition data for a process definition. Maps directly to the pmse_bpmn_process table and may, in the future, be merged with the pmse_bpmn_process table to prevent forked data backends.
pmse_bpm_related_dependency


pmse_bpm_related_dependency
Holds information about dependencies related to an event
pmse_bpm_thread
Holds information related to process threads for running processes.
pmse_bpmn_activity
Please see pmse_bpm_activity_definition
pmse_bpmn_artifact
Holds BPM artifact information, like Comments on a process diagram. At present, SugarBPM only supports the text annotation artifact.
pmse_bpmn_bound
Data related to element boundaries on the diagram.Â
pmse_bpmn_data
This table is not used.
pmse_bpmn_diagram
Data related to a process definition's diagram.Â
pmse_bpmn_documentation
This table is not used.
pmse_bpmn_event
Please see pmse_bpm_event_definition


Please see pmse_bpm_event_definition
pmse_bpmn_extension
This table is not used.
pmse_bpmn_flow
Holds information about the connecting flows of a diagram.
pmse_bpmn_gateway
Please see pmse_bpm_gateway_definition
pmse_bpmn_lane
This table is not used.
pmse_bpmn_laneset
This table is not used.
pmse_bpmn_participant
This table is not used.
pmse_bpmn_process
Please see pmse_bpm_process_definition
pmse_business_rules
Holds business rule record data.
pmse_emails_templates
Holds email template record data.
pmse_inbox
Holds information about processes that are currently running or that have completed.


Holds information about processes that are currently running or that have completed.
pmse_project
Holds process definition record information.
Relationships
The following Entity Relationship Diagram highlights the relationships between the various SugarBPM tables.
-Relationships" width="1654" recordid="e3710de4-b5b1-11e6-a3de-005056bc231a" style="width: nullpx; height: NaNpx;" />
Flows
When a process definition is created the following 5 tables get populated:
pmse_bpm_dynamic_forms contains dyn_view_defs which is theÂ JSONÂ encoded string of module-specific form information
pmse_bpm_process_definition contains the module associated with and the status of aÂ process definition
pmse_bpmn_diagram contains process definition name as well and a diagram uid


pmse_bpmn_process contains process definition name. The same Id is shared between pmse_bpm_process_definition and pmse_bpmn_process dia_id is a foreign key related to pmse_bpmn_diagram
pmse_project contains process definition name, project id, module, status. Other tables have aÂ foreign key of prj_id in them.
Upon adding a start/end/send message event:
pmse_bpmn_event contains event name
pmse_bpm_event_definition Shares id with pmse_bpmn_event
pmse_bpmn_bound
When Settings is changed to "New Records Only"
pmse_bpm_related_dependency
Upon adding an action or activity:
pmse_bpm_activity_definition contains the action name


pmse_bpm_activity_definition contains the action name
The act_fields column contains the JSONÂ encoded list of fields which will be changed
pmse_bpm_activity contains action name. Shares id with pmse_bpm_activity_definition. act_script_typeÂ contains the type of action e.g. CHANGE_FIELD
pmse_bpmn_bound
Upon adding a connector between start event and action:
pmse_bpmn_flow contains origin and destination info
Upon adding a Gateway:
pmse_bpm_gateway_definition
pmse_bpm_gateway shares id with pmse_bpm_gateway_definition
pmse_bpmn_flow flo_condition contains JSONÂ encoded string of conditions required to proceed ahead with an action or activity
pmse_bpmn_bound
When a process runs:


pmse_bpmn_bound
When a process runs:
pmse_inbox cas_status indicates theÂ status of aÂ process
pmse_bpm_thread
pmse_bpm_flow Stores the entire flow that the process elements went through (so if 3 elements and 2 connectors are in the process definition then 5 records will exist)
After Process has run and appears in Process Management:
pmse_bpm_case_data
pmse_bpm_form_action contains frm_action,Â Â cas_pre_dataÂ , and cas_data
Add a comment:
pmse_bpmn_artifact contains the comment
pmse_bpm_bound
Add a business rule to a process definition:
pmse_business_rules rst_source_definition contains the conditions of the business rule


pmse_business_rules rst_source_definition contains the conditions of the business rule
pmse_bpm_activity_definition act_fields contains the business rule id
Add an email template to a process definition:
pmse_email_templates
pmse_bpm_event_definition evn_criteria contains the email template id
Extension and Customization
In Sugar 7.8, Sugar introduced the Process Manager library to support extending SugarBPM in an upgrade-safe way. For more information on extending SugarBPM, or on using the Process Manager library, please read the Process Manager documentation.
Caveats


Caveats
In Sugar 7.8, the Process Manager library brought Registry Object to maintain the state of SugarBPM processes during a PHP Process. This change introduced a limitation in SugarBPM that prevents the same process definition from running on multiple records inside the same PHP process. For certain customizations in Sugar, if you are updating multiple records in a module using SugarBean, you may want them to trigger the defined process definitions for each record.
The following code can be added to your customization to allow for that functionality:
use Sugarcrm\Sugarcrm\ProcessManager\Registry;
//custom code
Registry\Registry::getInstance()->drop('triggered_starts');
$bean->save();


$bean->save();
The 'triggered_starts' registry contains the Start Event IDs that have been triggered previously in the PHP process, thus allowing the SugarBean::save() method to trigger SugarBPM and go through the same Start Event again.
TopicsExtending SugarBPM (Process Manager)From its earliest version, Sugar has been a tool that allows for customizing and extending in a way that allows developers to shape and mold it to a specific business need. Most components of Sugar are customizable or extensible through the use of custom classes (for custom functionality) or extensions (for custom metadata). However, a recent addition to the product, the SugarBPMâ¢ automation suite, did not fit this paradigm. In response, the Process Manager Library was introduced to give developers the ability to customize any portion of SugarBPM where object instantiation was previously handled using the 'new' operator.


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


Extending SugarBPM (Process Manager)
Introduction
From its earliest version, Sugar has been a tool that allows for customizing and extending in a way that allows developers to shape and mold it to a specific business need. Most components of Sugar are customizable or extensible through the use of custom classes (for custom functionality) or extensions (for custom metadata). However, a recent addition to the product, the SugarBPMâ¢ automation suite, did not fit this paradigm. In response, the Process Manager Library was introduced to give developers the ability to customize any portion of SugarBPM where object instantiation was previously handled using the 'new' operator.Â
Note:Â SugarBPMâ¢ is not available in Sugar Professional. For an introduction to the SugarBPM modules and architecture, please refer to the SugarBPM documentation in this guide.
Process Manager


Process Manager
By way of ProcessManager\Factory we can now develop custom versions of most any PMSE* class and have that class used in place of the core PMSE* class.
Limitations
The current implementation of the Process Manager Library only handles server-side customizations by way of the Factory class. These customizations can be applied to any instantiable pmse_* object, however, static classes and singleton classes, such as the following, are not affected by the library:
PMSE
PMSEEngineUtils
PMSELogger
While some effort has been made to allow for the creation of custom actions in the Process Definition designer, as of Sugar 7.8, that is the only client customization allowance that will be implemented.
Library Structure
The Process Manager Library is placed under the Sugarcrm\Sugarcrm\ProcessManager namespace and contains the following directories and classes:
Factory
Exception/
BaseException


Factory
Exception/
BaseException
DateTimeException
ExceptionInterface
ExecutionException
InvalidDataException
NotAuthorizedException
RuntimeException
Field/
Evaluator/
AbstractEvaluator
Base
Currency
Datetime
Decimal
EvaluatorInterface
Int
Multienum
Relate
Registry/
Registry
RegistryInterface
Examples
Getting a SugarBPM object
Almost all objects in SugarBPM can be instantiated through the Process Manager Factory. When loading a class, the Factory getPMSEObject method will look in the following directories as well as the custom versions of these directories for files that match the object name being loaded:
modules/pmse_Business_Rules/
modules/pmse_Business_Rules/clients/base/api/
modules/pmse_Emails_Templates/


modules/pmse_Emails_Templates/
modules/pmse_Emails_Templates/clients/base/api/
modules/pmse_Inbox/clients/base/api/
modules/pmse_Inbox/engine/
modules/pmse_Inbox/engine/parser/
modules/pmse_Inbox/engine/PMSEElements/
modules/pmse_Inbox/engine/PMSEHandlers/
modules/pmse_Inbox/engine/PMSEPreProcessor/
modules/pmse_Inbox/engine/wrappers/
modules/pmse_Project/clients/base/api/
modules/pmse_Project/clients/base/api/wrappers/
modules/pmse_Project/clients/base/api/wrappers/PMSEObservers/


What this means is that virtually any class that is found by name in these directories can be instantiated and returned via the getPMSEObject() method. This also means that customizations to any of these classes can be easily implemented by creating a Custom version of the class under the appropriate ./custom directory path.
For example, to create a custom version of PMSEProjectWrapper, you would create a CustomPMSEProjectWrapper class at Â ./custom/modules/pmse_Project/clients/base/api/wrappers/CustomPMSEProjectWrapper.php. This allows you to have full control of your instance of SugarBPM while giving you the flexibility to add any classes you want and consuming them through the factory.
./custom/modules/pmse_Project/clients/base/api/wrappers/CustomPMSEProjectWrapper.php
<?php


<?php
require_once 'modules/pmse_Project/clients/base/api/wrappers/PMSEProjectWrapper.php';
class CustomPMSEProjectWrapper extends PMSEProjectWrapper
{
}
This can now be consumed by simply calling the getPMSEObject() method:
<?php
// Set the process manager library namespace
use \Sugarcrm\Sugarcrm\ProcessManager\Factory;
// Get our wrapper
$wrapper = ProcessManager\Factory::getPMSEObject('PMSEProjectWrapper');
Getting a SugarBPM Element Object
Element objects in SugarBPM generally represent some part of the Process engine, and often time map to design elements. All Element objects can be found in the modules/pmse_Inbox/engine/PMSEElements/ directory.


Getting a SugarBPM Element object can be done easily by calling the getElement() method of the Process Manager Factory:
<?php
use \Sugarcrm\Sugarcrm\ProcessManager\Factory;
// Get a default PMSEElement object
$element = ProcessManager\Factory::getElement();
To get a specific object, you can pass the object name to the getElement() method:
$activity = ProcessManager\Factory::getElement('PMSEActivity');
Alternatively, you can remove the PMSE prefix and call the method with just the element name:
$gateway = ProcessManager\Factory::getElement('Gateway');
To create a custom SugarBPM Element, you can simply create a new file at ./custom/modules/pmse_Inbox/engine/PMSEElements/ where the file name is prefixed with Custom.


./custom/modules/pmse_Inbox/engine/PMSEElements/CustomPMSEScriptTask.php
<?php
require_once 'modules/pmse_Inbox/engine/PMSEElements/PMSEScriptTask.php';
use Sugarcrm\Sugarcrm\ProcessManager;
class CustomPMSEScriptTask extends PMSEScriptTask
{
}
Note: All SugarBPM Element classesÂ must implement the PMSERunnable interface. Failure to implement this interface will result in an exception when using the Factory to get an Element object.
To get aÂ custom ScriptTaskÂ object via the Factory, just call getElement():
$obj = ProcessManager\Factory::getElement('ScriptTask');
To make your own custom widget element, you can implement the PMSERunnable interface:


To make your own custom widget element, you can implement the PMSERunnable interface:
./custom/modules/pmse_Inbox/engine/PMSEElements/CustomPMSEWidget.php
<?php
require_once 'modules/pmse_Inbox/engine/PMSEElements/PMSERunnable.php';
use Sugarcrm\Sugarcrm\ProcessManager;
class CustomPMSEWidget implements PMSERunnable
{
}
To get a Widget object via the Factory, just call getElement():
$obj = ProcessManager\Factory::getElement('Widget');
Getting and Using a Process Manager Field Evaluator Object
Process Manager Field Evaluator objects determine if a field on a record has changed and if a field on a record is empty according to the field type. The purpose of these classesÂ can grow to include more functionality in the future.


Getting a field evaluator object is also doneÂ through the Factory:
<?php
use \Sugarcrm\Sugarcrm\ProcessManager\Factory;
// Get a Datetime Evaluator. Type can be date, time, datetime or datetimecombo
$eval = ProcessManager\Factory::getFieldEvaluator(['type'  => 'date']);
A more common way of getting a field evaluator object is to pass the field definitions for a field on a SugarBeanÂ into the Factory:
$eval = ProcessManager\Factory::getFieldEvaluator($bean ->field_defs[$field]);
$eval = ProcessManager\Factory::getFieldEvaluator($bean ->field_defs[$field]);
Once the evaluator object is fetched, you mayÂ initialize it with properties:
/*


/*
 This is commonly used for field change evaluations
 */
// $bean is a SugarBean object
// $field is the name of the field being evaluated
// $data is an array of data that is used in the evaluation
$eval ->init($bean, $field, $data);
// Has the field changed?
$changed  = $eval->hasChanged();
Â
Or you can set properties onto the evaluator:
/*
 This is commonly used for empty evaluations
 */
// Set the bean onto the evaluator
$eval->setBean($bean);
// And set the name onto the evaluator
$eval->setName($field);
// Are we empty?
$isEmpty  = $eval->isEmpty();
Creating and consuming a custom field evaluator is as easy as creating a custom class and extending the BaseÂ class:


./custom/src/ProcessManager/Field/Evaluator/CustomWidget.php
<?php
namespace Sugarcrm\Sugarcrm\ProcessManager\Field\Evaluator;
/**
 * Custom widget field evaluator
 *  @package ProcessManager
 */
class CustomWidget extends Base
{
}
Â
Alternatively, you can create a custom evaluator that extends the AbstractEvaluator parent class if you implement the EvaluatorInterface interface.
./custom/src/ProcessManager/Field/Evaluator/CustomWidget.php
<?php
namespace Sugarcrm\Sugarcrm\ProcessManager\Field\Evaluator;
/**
 * Custom widget field evaluator
 *  @package ProcessManager
 */
class CustomWidget extends AbstractEvaluator  implements EvaluatorInterface
{
}


class CustomWidget extends AbstractEvaluator  implements EvaluatorInterface
{
}
And at this point, you can use the Factory to get your widget object:
// Get a custom widget evaluator object
$eval  = ProcessManager\Factory::getFieldEvaluator(['type'  => 'widget']);
Getting an Exception Object with Logging
Getting a SugarBPM exception with logging is as easy as calling a single Factory method. Currently, the Process Manager library supports the following exception types:
DateTime
Execution
InvalidData
NotAuthorized
Runtime
As a fallback, the Base exception can be used. All exceptions log their message to the PMSE log when created, and all exceptions implement ExceptionInterface.
The most basic way to get a ProcessManager exception is to call the getException() method on the Factory with no arguments:
<?php


<?php
use \Sugarcrm\Sugarcrm\ProcessManager\Factory;
// Get a BaseException object, with a default message
// of 'An unknown Process Manager exception had occurred'
$exception = ProcessManager\Factory::getException();
Â
To get a particular type of exception, or to set your message, you can add the first two arguments:
if ($error) {
     throw ProcessManager\Factory::getException('InvalidData', $error);
}
It is possible to set an exception code, as well as a previous exception, when creating an Exception object:
// Assume $previous is an ExecutionException
throw ProcessManager\Factory ::getException('Runtime', 'Cannot carry out the action', 255, $previous);
Finally, to create your own custom exception classes, you must add a new custom file in the following path with the file named appropriately:


./custom/src/ProcessManager/Exception/CustomEvaluatorException.php
<?php
namespace Sugarcrm\Sugarcrm\ProcessManager\Exception;
/**
 * Custom Evaluator exception
 *  @package ProcessManager
 */
class CustomEvaluatorException  extends BaseException  implements ExceptionInterface
{
}
And to consume this custom exception, call the Factory:
$exception = ProcessManager\Factory::getException('Evaluator', 'Could not evaluate the expression');
Maintaining State Throughout a Process
The RegistryÂ classÂ implements the RegistryInterface which exposes the following public methods:
set($key, $value, $override = false)
get($key, $default = null)
has($key)
drop($key)
getChanges($key)
reset()


has($key)
drop($key)
getChanges($key)
reset()
To use the Registry class, simply use the namespace to get the singleton:
<?php
use \Sugarcrm\Sugarcrm\ProcessManager\Registry;
$registry = Registry\Registry::getInstance();
To set a value into the registry, simply add it along with a key:
$registry->set('reg_key', 'Hold on to this');
NOTE: To prevent named index collision, it is advisable to write indexes to contain a namespace indicator, such as a prefix.
$registry->set('mykeyindex:reg_key', 'Hold on to this');
Once a value is set in the registry, it is immutable unless theÂ set($key, $value, $override = false)Â method is called withÂ the override argument set to true:


// Sets the reg_key value
$registry->set('reg_key', 'Hold on to this');
// Since the registry key reg_key is already set, this will do nothing
$registry->set('reg_key', 'No wait!');
// To forcefully set it, add a true for the third argument
$registry->set('reg_key', 'No wait!', true);
When a key is changed on the registry, these changes are logged and can beÂ inspected using getChanges():
// Set and reset a value on the registry
$registry->set('key', 'Foo');
$registry->set('key', 'Bar', true);
$registry->set('key', 'Baz', true);
// Get the changes for this key
$changes  = $registry ->getChanges('key');
/*
$changes will be:
 array(


/*
$changes will be:
 array(
     0 => array(
         'from' => 'Foo',
         'to' => 'Bar',
     ),
     1 => array(
         'from' => 'Bar',
         'to' => 'Baz',
     ),
 */
To get the value of the key you just set, call the get() method:
$value = $registry->get('reg_key');
To get a value of a key with a default, you can pass the default value as the second argument to get():
// Will return either the value for attendee_count or 0
$count  = $registry->get('attendee_count', 0);
To see if the registry currently holds a value, call the has() method:
if ($registry->has('field_list')) {
    // Do something here
}


// Do something here
}
To remove a value from the registry, use the drop() method. This will add an entry toÂ the changelog, provided the key wasÂ already set.
$registry->drop('key');
Finally, to clear the entire registry of all values and changelog entries, use the reset() method.
$registry->reset();
Note: This is very destructive. Please use with caution because you can purge settings that you didn't own.
  	Last modified: 2023-02-03 21:04:03


Tags
Overview
The tagging feature allows a user to apply as many "tags" as they choose on any record they want to categorize. This allows the user to effectively group records together from any source within the application and relate them together. Please note that the tag field and Tags module do not support customization at this time.
The Tags Module
The Tags module is a very simple module based on the Basic SugarObject template. It is not studio editable, is part of the default module list, and is available to the application upon installation. By default, the Tags module is set up such that only an Administrator can perform any administrative tasks with the Tags module. In other words, a regular user will have a very restrictive set of permissions inside the Tags module, namely: create, export, view, and share. All other actions within the Tags modules must be carried out by an Administrative user.
The "tag" Field Type


The "tag" Field Type
The back end utilizes two new field types: the relatecollection field and the tag field. The tag field is a relatecollection type field with a few added enhancements specific to the tagging implementation:
Enforces uniqueness of a tag when created
Establishes the necessary relationship between the Tags module and the module record being tagged
Collects and formats a tag collection in a way the client understands
Format the tag field for consumption by and from the import process
Handles proper query setting for a search that is filtered by tags
The Taggable Implementation
The taggable implementation is applied to all SugarObject templates so that it is available on all custom modules created and is also applied to all sidecar enabled modules. Any module that implements a SugarObject templateÂ will be taggable by default. Any module that doesn't implement a SugarObject template can easily apply it using the uses property of the module vardefs:


$dictionary[$module] = array(
    ...
    'uses' => array(
        'taggable',
    ),
    ...
);
There may be instances where a module should not implement tagging yet implementsÂ the SugarObject template. To remove tagging from a module you can use the module vardefs ignore_templates property:
$dictionary[$module] = array(
    ...
    'ignore_templates' => array(
        'taggable',
    ),
    ...
);
The Tagging Relationship Structure
The tagging relationship schema is similar in nature to the email_addresses relationship schema in that it is used to represent a collection of 0-N Tags module records related to a module:
Column
Description
id
The GUID for the relationship between the tag record and the module record
tag_id
The id of the tag record used in this relationship


tag_id
The id of the tag record used in this relationship
bean_id
The id of the module record used in this relationship
bean_module
The module for the record being tagged
date_modified
The date the relationship was created/modified
deleted
A tinyint(1) boolean value that indicates whether this relationship is deleted
Tagging in Action
For adding tags from the UI, please refer to the Tags documentation.
Tagging Records from the API
Tagging a record via the APIÂ is done by sending aÂ PUT request to the /<module>/<record>Â API endpoint with a tag property set to an array of key/value pairs that include a tag id (optional) and a tag name:
{
    "name": "Record Name",
    "tag": [
        {"id": "Test Tag", "name": "Test Tag"},


{"id": "Test Tag", "name": "Test Tag"},
        {"name": "Test Tag 2"},
        {"id": "1234-56-7890", "name": "Test Tag 3"}
    ]
}
After the record is created/modified and the tag values are applied, the response will contain the returned collection of tags for that record complete with their ids:
{
    "name": "Record Name",
    "tag": [
        {"id": "9876-54-3210", "name": "Test Tag"},
        {"id": "4321-56-0987", "name": "Test Tag 2"},
        {"id": "1234-56-7890", "name": "Test Tag 3"}
    ]
}


]
}
You can visitÂ How to Manipulate Tags (CRUD)Â for a full example demonstratingÂ CRUD actions for tags.
Mass Updating Tags on Records Via the API
Mass updating records with tags are as simple as sending a MassUpdate PUT request to /<module>/MassUpdateÂ with a payload similar to:
{
    "massupdate_params": {
        "uid": ["12345-67890", "09876-54321"],
        "tag": [
            { "id": "23456-78901", "name": "MyTag1" },
            { "id": "34567-89012", "name": "MyTag2" }
        ]
    }
}


]
    }
}
This request will override all existing tags on the named records. To append tags to a record, send the "tag_type" request argument set to a value of 1:
{
    "massupdate_params": {
        "uid": ["12345-67890", "09876-54321"],
        "tag": [
            { "id": "23456-78901", "name": "MyTag1" },
            { "id": "34567-89012", "name": "MyTag2" }
        ],
        "tag_type": "1"
    }
}
More information on this API endpoint can be found in the /<module>/MassUpdate PUT documentation.
Fetching Tags on a Record


Fetching Tags on a Record
By default, the tag field is added to all Sidecar module record views. That means when a request is made for a single record through the API, that single record will return the "tag" field, which will be a collection of key:value pair objects.
For example, when a request is made to the /Accounts/:record GET endpoint, the tags associated with the AccountrecordÂ  will be returned in the tag field as an array of objects:
{
    "id": "<record>",
    "name": "Record Name",
    "tag": [
        { "id": "9876-54-3210", "name": "Test Tag" },
        { "id": "4321-56-0987", "name": "Test Tag 2" },


{ "id": "1234-56-7890", "name": "Test Tag 3" }
    ]
}
Filtering a Record List by Tags
Filtering a list of records by tags can be done simply by sending a filter request to the ModuleApi endpoint for a module. For example, to filter the Accounts list where the tag field has a tag by the name of "Tradeshow", you can send a request to the /Accounts GETÂ endpoint with the following request arguments:
{
    "view": "list",
    "filter": [
        {
            "tag": {
                "$in": [
                    {
                        "name": "Tradeshow"
                    }
                ]
            }
        }
    ]
}
Currently, the tag field filter definitions support the following filter operators:
Is any of ($in)


Is any of ($in)
Is not any of ($not_in)
Is empty ($empty)
Is not empty ($not_empty)
Fetching a list of Tags from the Tags module
Fetch a list of tag records from the Tags module is done the same way as fetch a list of records from any other module, namely by sending a GET request to the /Tags ModuleApi endpoint.Â More information can be found in the /<module> GET documentation.
Manipulating Tags Programmatically
Getting Tags Related to a Record
Here is an example that demonstrates how to get all the tags and its ids related to a contact record:
// Creating a Bean for Contacts
$bean = BeanFactory::getBean("Contacts");
// Creating a Bean for Tags
$tagBean = BeanFactory::newBean('Tags');


$tagBean = BeanFactory::newBean('Tags');
// Get all the tags related to Contacts Bean by givin Contact ID. You can provide more than one Record ID.
$tags = $tagBean->getRelatedModuleRecords($bean, ["<CONTACT_RECORD_ID>"]);
Creating a New Tag and Adding to a Record
Here is an example that demonstrates how to add create a tag and add to a contact record.
In order to add a new tag first, we will create the tag bean. Then using load_relationship function we will add the newly created tag id to the contactsÂ
// Creating new Tag Bean
$tagBean = BeanFactory::newBean("Tags");
// Setting its name
$tagBean->name = "New Tag";
$tagBean->save();
// Retrieving the Contacts Bean


$tagBean->save();
// Retrieving the Contacts Bean
$bean = BeanFactory::getBean("Contacts", "<RECORD_ID>");
// Getting tag field and its properties
$tagField = $bean->getTagField();
$tagFieldProperties = $bean->field_defs[$tagField];
// Identifying relation link
$link = $tagFieldProperties['link'];
// Loading relationship
if ($bean->load_relationship($link)) {
    // Adding newly created Tag Bean
    $bean->$link->add($tagBean->id);
}
Removing Tags from a Record
Here is an example that demonstrates how to remove a tag from a contact record:
// Getting the Contacts Bean
$bean = BeanFactory::getBean("Contacts", "<RECORD_ID>");
// Getting tag field and its properties


// Getting tag field and its properties
$tagField = $bean->getTagField();
$tagFieldProperties = $bean->field_defs[$tagField];
// Identifying relation link
$link = $tagFieldProperties['link'];
// Loading relationship
if($bean->load_relationship($link)){
    // Removing the Tag ID
    $bean->$link->delete($bean->id, "<TAG_RECORD_ID>");
}
Synchronizing Tags by Name With API Helpers
If you have multiple tags that you need to add and delete at the same time, then you can use SugarFieldTag->apiSave method. Here is an example that demonstrates how to sync tags by using SugarFieldTag ClassÂ forÂ a Contact record.


It is important to know that since this is a sync; you will need to keep the existing tags if you want them to still exist in the record.Â
// Getting the Contacts Bean
$bean = BeanFactory::getBean("Contacts", "<RECORD_ID>");
// Getting Tag Field ID
$tagField = $bean->getTagField();
// Getting Tag Field Properties
$tagFieldProperties = $bean->field_defs[$tagField];
// Preparing the latest Tags to be sync with the record
$tags = [
    // Note: Already attached tags will be automatically removed from the record
    // If you want to keep some of the existing tags then you will need to keep them in the array
    "tag" => [
        // Since this tag is already added, it will be preserved
        'already added tag' => 'Already Added Tag',


'already added tag' => 'Already Added Tag',
        // The new tags to add - All other tags that previously existed will be deleted
        'new tag' => 'New Tag',
        'new tag2' => 'New Tag2',
    ],
];
// Building SugarFieldTag instance
$SugarFieldTag = new SugarFieldTag();
// Passing the arguments to save the Tags
$SugarFieldTag->apiSave($bean, $tags, $tagField, $tagFieldProperties);
  	Last modified: 2023-02-03 21:04:03


Quotes
Overview
As of Sugar 7.9.0.0, the quotes module has been moved out of Backward Compatibility mode into Sidecar. Customizations to the Quotes interface will need to be rewritten for the new Sidecar framework as an automated migration of these customizations is not possible. This document will outline common customizations and implementation methods.
Quote Identifiers
Administrators can create custom quote identifiers that are more complex than the default auto-incrementing scheme found in an out of box installation. Utilizing this functionality, we can create numbering schemes that match any business needs.
Creating Custom Identifiers


Creating Custom Identifiers
To create a custom identifier, navigate to Admin > Studio > Quotes > Fields. Next, create a TextField type field named to your liking and check the "Calculated Value" checkbox and click "Edit Formula". Once you see the formula builder, you can choose an example below or create your own. Once created, You can add the new field to your Quote module's layouts as desired.
User Field with Pseudo-Random Values
This example creates a quote number in the format of <user last name>-##### that translates to Smith-87837. This identifier starts with the last name of the user creating it, followed by 5 pseudo-random numbers based on the creation time.
concat(related($created_by_link, "last_name"), "-", subStr(toString(timestamp($date_entered)), 5, 5))


Note: when using Sugar Logic, updates to related fields will update any corresponding Sugar Logic fields. The example being that an update to the Created By lastÂ name field will update all related records withÂ aÂ field using this formula.
Related Field Value and Auto-Incrementing Number
This example creates a quote number in the format of <billing account name>-#### that translates to Start Over Trust-0001. This number starts with the billing account name followed by a 4 digit auto-incrementing number.
concat(related($billing_accounts, "name"), "-", subStr(toString(add($quote_num, 10000)), 1, 4))
Note: when using Sugar Logic, updates to related fields will update any corresponding Sugar Logic fields. The example being that an update to the Billing Account name will update all related records withÂ aÂ field using this formula.
Record Layout


Record Layout
The following sections outline various changes that can be done to modify the record layout of the Quotes module, by outlining the extra views that can be updated.
Grand Totals Header
The Grand Total Header contains the calculated totals for the quote.
Modifying Fields in the Grand Totals Header


Modifying Fields in the Grand Totals Header
To modify the Grand Totals Header fields, you can copy ./modules/Quotes/clients/base/views/quote-data-grand-totals-header/quote-data-grand-totals-header.php to ./custom/modules/Quotes/clients/base/views/quote-data-grand-totals-header/quote-data-grand-totals-header.php or you may create a metadata extension in ./custom/Extension/modules/Quotes/clients/base/views/quote-data-grand-totals-header/. Next, modify the $viewdefs['Quotes']['base']['view']['quote-data-grand-totals-header']['panels'][0]['fields'] index to add or remove your preferred fields.
Example


Example
The example below will add the "Quote Number" field (quotes.quote_num) field to the layout and removes the "Total Discount" (quotes.deal_tot) from the layout.Â If adding a custom field 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.
./custom/modules/Quotes/clients/base/views/quote-data-grand-totals-header/quote-data-grand-totals-header.php
<?php
$viewdefs['Quotes']['base']['view']['quote-data-grand-totals-header'] = array(
    ...
    'panels' => array(
        array(
            'name' => 'panel_quote_data_grand_totals_header',


'label' => 'LBL_QUOTE_DATA_GRAND_TOTALS_HEADER',
            'fields' => array(
                array(
                    'name' => 'quote_num',
                    'label' => 'LBL_LIST_QUOTE_NUM',
                    'css_class' => 'quote-totals-row-item',
                ),
                array(
                    'name' => 'new_sub',
                    'css_class' => 'quote-totals-row-item',
                ),
                array(
                    'name' => 'tax',
                    'label' => 'LBL_TAX_TOTAL',
                    'css_class' => 'quote-totals-row-item',
                ),
                array(
                    'name' => 'shipping',


),
                array(
                    'name' => 'shipping',
                    'css_class' => 'quote-totals-row-item',
                ),
                array(
                    'name' => 'total',
                    'label' => 'LBL_LIST_GRAND_TOTAL',
                    'css_class' => 'quote-totals-row-item',
                ),
            ),
        ),
    ),
);
Modifying Buttons in the Grand Totals Header
The Quotes List Header contains row actions to create quoted line items, comments, and groupings. The actions are identified by the plus icon in the top left of the header. Editing the 'actions' will allow you to add or remove buttons. The following section will outline how these items can be modified.


To modify the Row Actions in the Grand Total Header, you can copy ./modules/Quotes/clients/base/views/quote-data-grand-totals-header/quote-data-grand-totals-header.php to ./custom/modules/Quotes/clients/base/views/quote-data-grand-totals-header/quote-data-grand-totals-header.php or you may create a metadata extension in ./custom/Extension/modules/Quotes/clients/base/views/quote-data-grand-totals-header/. Next, modify the $viewdefs['Quotes']['base']['view']['quote-data-grand-totals-header']['buttons'] index to add or remove your preferred row actions.
Example


Example
The example below will create a new view that extends the QuotesQuoteDataGrandTotalsHeader view and append a button to the Grand Total Header button list.
First, create your custom view type that will extend the QuotesQuoteDataGrandTotalsHeader view.
./custom/modules/Quotes/clients/base/views/quote-data-grand-totals-header/quote-data-grand-totals-header.js
({
    extendsFrom: 'QuotesQuoteDataGrandTotalsHeaderView',
    initialize: function(options) {
        this.events = _.extend({}, this.events, options.def.events, {
            'click [name="gth-custom-button"]': 'buttonClicked'
        });
        this._super('initialize', [options]);
    },
    /**
     * Click event
     */


},
    /**
     * Click event
     */
    buttonClicked: function() {
        app.alert.show('success_alert', {
            level: 'success',
            title: 'Grand Total Header Button was clicked!'
        });
    },
})
This code will append a click event to our button named gth-custom-button and trigger the buttonClicked method. Once completed, add your new buttonÂ array to the grand total header in the $viewdefs['Quotes']['base']['view']['quote-data-grand-totals-header']['buttons'] index.
./custom/modules/Quotes/clients/base/views/quote-data-grand-totals-header/quote-data-grand-totals-header.php
<?php


<?php
$viewdefs['Quotes']['base']['view']['quote-data-grand-totals-header'] = array(
    'buttons' => array(
        array(
            'type' => 'quote-data-actiondropdown',
            'name' => 'panel_dropdown',
            'no_default_action' => true,
            'buttons' => array(
                array(
                    'type' => 'button',
                    'icon' => 'fa-plus',
                    'name' => 'create_qli_button',
                    'label' => 'LBL_CREATE_QLI_BUTTON_LABEL',
                    'acl_action' => 'create',
                    'tooltip' => 'LBL_CREATE_QLI_BUTTON_TOOLTIP',
                ),
                array(


),
                array(
                    'type' => 'button',
                    'icon' => 'fa-plus',
                    'name' => 'create_comment_button',
                    'label' => 'LBL_CREATE_COMMENT_BUTTON_LABEL',
                    'acl_action' => 'create',
                    'tooltip' => 'LBL_CREATE_COMMENT_BUTTON_TOOLTIP',
                ),
                array(
                    'type' => 'button',
                    'icon' => 'fa-plus',
                    'name' => 'create_group_button',
                    'label' => 'LBL_CREATE_GROUP_BUTTON_LABEL',
                    'acl_action' => 'create',
                    'tooltip' => 'LBL_CREATE_GROUP_BUTTON_TOOLTIP',
                ),


),
                array(
                    'type' => 'button',
                    'icon' => 'fa-plus',
                    'name' => 'gth-custom-button',
                    'label' => 'LBL_GTH_CUSTOM_BUTTON',
                    'acl_action' => 'create',
                    'tooltip' => 'LBL_GTH_CUSTOM_BUTTON_TOOLTIP',
                ),
            ),
        ),
    ),
    ...
);
Finally, create labels under Quotes for the label and tooltip indexes. To accomplish this, create a language extension:
./custom/Extension/modules/Quotes/Ext/Language/en_us.custom-button.php
<?php
$mod_strings['LBL_GTH_CUSTOM_BUTTON'] = 'Custom Button';


$mod_strings['LBL_GTH_CUSTOM_BUTTON_TOOLTIP'] = 'Custom Button Tooltip';
Once the files are in place, navigate to Admin > Repair > Quick Repair and Rebuild. Your changes will now be reflected in the system.
List Header
The Quotes List Header is a complex view that pulls data from two different locations. The JavaScript controller is located in ./modules/Quotes/clients/base/views/quote-data-list-header/quote-data-list-header.js and pulls the metadata from ./modules/Products/clients/base/views/quote-data-group-list/quote-data-group-list.php. You should note that ProductBundleNotes combines its description field with Product fields in this list.
Modifying Fields in the List Header


Modifying Fields in the List Header
To modify the List Header fields, you can copy ./modules/Products/clients/base/views/quote-data-group-list/quote-data-group-list.php to ./custom/modules/Products/clients/base/views/quote-data-group-list/quote-data-group-list.php or you may create a metadata extension in ./custom/Extension/modules/Products/Ext/clients/base/views/quote-data-group-list/. Next, modify the $viewdefs['Products']['base']['view']['quote-data-group-list']['panels'][0]['fields'] index to add or remove your preferred fields.
Example


Example
The example below will remove the "Part Number" (products.mft_part_num) field and replace it with the "Weight" (products.weight) field.
./custom/modules/Products/clients/base/views/quote-data-group-list/quote-data-group-list.php
<?php
$viewdefs['Products']['base']['view']['quote-data-group-list'] = array(
    'panels' => array(
        array(
            'name' => 'products_quote_data_group_list',
            'label' => 'LBL_PRODUCTS_QUOTE_DATA_LIST',
            'fields' => array(
                array(
                    'name' => 'line_num',
                    'label' => null,
                    'widthClass' => 'cell-xsmall',


'widthClass' => 'cell-xsmall',
                    'css_class' => 'line_num tcenter',
                    'type' => 'line-num',
                    'readonly' => true,
                ),
                array(
                    'name' => 'quantity',
                    'label' => 'LBL_QUANTITY',
                    'widthClass' => 'cell-small',
                    'css_class' => 'quantity',
                    'type' => 'float',
                ),
                array(
                    'name' => 'product_template_name',
                    'label' => 'LBL_ITEM_NAME',
                    'widthClass' => 'cell-large',
                    'type' => 'quote-data-relate',
                    'required' => true,
                ),
                array(
                    'name' => 'weight',


),
                array(
                    'name' => 'weight',
                    'label' => 'LBL_WEIGHT',
                    'type' => 'float',
                ),
                array(
                    'name' => 'discount_price',
                    'label' => 'LBL_DISCOUNT_PRICE',
                    'type' => 'currency',
                    'convertToBase' => true,
                    'showTransactionalAmount' => true,
                    'related_fields' => array(
                        'discount_price',
                        'currency_id',
                        'base_rate',
                    ),
                ),
                array(
                    'name' => 'discount',
                    'type' => 'fieldset',
                    'css_class' => 'quote-discount-percent',


'css_class' => 'quote-discount-percent',
                    'label' => 'LBL_DISCOUNT_AMOUNT',
                    'fields' => array(
                        array(
                            'name' => 'discount_amount',
                            'label' => 'LBL_DISCOUNT_AMOUNT',
                            'type' => 'discount',
                            'convertToBase' => true,
                            'showTransactionalAmount' => true,
                        ),
                        array(
                            'type' => 'discount-select',
                            'name' => 'discount_select',
                            'no_default_action' => true,
                            'buttons' => array(
                                array(
                                    'type' => 'rowaction',


array(
                                    'type' => 'rowaction',
                                    'name' => 'select_discount_amount_button',
                                    'label' => 'LBL_DISCOUNT_AMOUNT',
                                    'event' => 'button:discount_select_change:click',
                                ),
                                array(
                                    'type' => 'rowaction',
                                    'name' => 'select_discount_percent_button',
                                    'label' => 'LBL_DISCOUNT_PERCENT',
                                    'event' => 'button:discount_select_change:click',
                                ),
                            ),
                        ),
                    ),
                ),
                array(
                    'name' => 'total_amount',
                    'label' => 'LBL_LINE_ITEM_TOTAL',


'label' => 'LBL_LINE_ITEM_TOTAL',
                    'type' => 'currency',
                    'widthClass' => 'cell-medium',
                    'showTransactionalAmount' => true,
                    'related_fields' => array(
                        'total_amount',
                        'currency_id',
                        'base_rate',
                    ),
                ),
            ),
        ),
    ),
);
Next, create the LBL_WEIGHT label under Quotes as this is not included in the stock installation. To accomplish this, we will need to create a language extension:
./custom/Extension/modules/Quotes/Ext/Language/en_us.weight.php
<?php
$mod_strings['LBL_WEIGHT'] = 'Weight';


<?php
$mod_strings['LBL_WEIGHT'] = 'Weight';
If adding a custom field 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.
Modifying Row Actions in the List Header
The Quotes List Header contains row actions to create and delete QLI groupings. The actions are identified by, the "check all" checkbox and vertical ellipsis or "hamburger" icon buttons. Editing the 'actions' will allow you to add more buttons to (or remove from) the "Group Selected" and "Delete Selected" buttons. The following section will outline how these items can be modified.


To modify the Row Actions in the List Header, you can copy ./modules/Quotes/clients/base/views/quote-data-list-header/quote-data-list-header.php to ./custom/modules/Quotes/clients/base/views/quote-data-list-header/quote-data-list-header.php or you may create a metadata extension in ./custom/Extension/modules/Quotes/clients/base/views/quote-data-list-header/. Next, modify the $viewdefs['Quotes']['base']['view']['quote-data-list-header']['selection']['actions'] index to add or remove your preferred actions.
Example
The example below will create a new view that extends the QuotesQuoteDataListHeader view and append a row action to the List Header action list.


First, create your custom view type that will extend the QuotesQuoteDataListHeader view.
./custom/modules/Quotes/clients/base/views/quote-data-list-header/quote-data-list-header.js
({
    extendsFrom: 'QuotesQuoteDataListHeaderView',
    initialize: function(options) {
        this.events = _.extend({}, this.events, options.def.events, {
            'click [name="lh-custom-button"]': 'actionClicked'
        });
        this._super('initialize', [options]);
    },
    /**
     * Click event
     */
    actionClicked: function() {
        app.alert.show('success_alert', {
            level: 'success',
            title: 'List Header Row Action was clicked!'
        });
    },
})


title: 'List Header Row Action was clicked!'
        });
    },
})
This code will append a click event to our field named lh-custom-button and trigger the actionClicked method. Once completed, add your new row action to the list header in the $viewdefs['Quotes']['base']['view']['quote-data-list-header']['selection']['actions'] index.
./custom/modules/Quotes/clients/base/views/quote-data-list-header/quote-data-list-header.php
<?php
$viewdefs['Quotes']['base']['view']['quote-data-list-header'] = array(
    'selection' => array(
        'type' => 'multi',
        'actions' => array(
            array(
                'name' => 'group_button',


array(
                'name' => 'group_button',
                'type' => 'rowaction',
                'label' => 'LBL_CREATE_GROUP_SELECTED_BUTTON_LABEL',
                'tooltip' => 'LBL_CREATE_GROUP_SELECTED_BUTTON_TOOLTIP',
                'acl_action' => 'edit',
            ),
            array(
                'name' => 'massdelete_button',
                'type' => 'rowaction',
                'label' => 'LBL_DELETE_SELECTED_LABEL',
                'tooltip' => 'LBL_DELETE_SELECTED_TOOLTIP',
                'acl_action' => 'delete',
            ),
            array(
                'name' => 'lh-custom-button',


array(
                'name' => 'lh-custom-button',
                'type' => 'rowaction',
                'label' => 'LBL_LH_CUSTOM_ACTION',
                'tooltip' => 'LBL_LH_CUSTOM_ACTION_TOOLTIP',
                'acl_action' => 'edit',
            ),
        ),
    ),
);
Finally, create labels under Quotes for the label and tooltip indexes. To accomplish this, create a language extension:
./custom/Extension/modules/Quotes/Ext/Language/en_us.lh-custom-action.php
<?php
$mod_strings['LBL_LH_CUSTOM_ACTION'] = 'Custom Action';


$mod_strings['LBL_LH_CUSTOM_ACTION_TOOLTIP'] = 'Custom Action Tooltip';
Once the files are in place, navigate to Admin > Repair > Quick Repair and Rebuild. Your changes will now be reflected in the system.
Group Header
The Group Header contains the name and options for each grouping of quoted line items.
Â
Modifying Fields in the Group Header


Â
Modifying Fields in the Group Header
To modify the Group Header fields, you can copy ./modules/ProductBundles/clients/base/views/quote-data-group-header/quote-data-group-header.php to ./custom/modules/ProductBundles/clients/base/views/quote-data-group-header/quote-data-group-header.php or you may create a metadata extension in ./custom/Extension/modules/Products/clients/base/views/quote-data-group-header/. Next, modify the $viewdefs['ProductBundles']['base']['view']['quote-data-group-header'] index to add or remove your preferred fields.
Example


Example
The example below will append the group total (product_bundles.subtotal) field to the Group Header. It's important to note that when adding additional fields, that changes to the corresponding .hbs file may be necessary to correct any formatting issues.
./custom/modules/ProductBundles/clients/base/views/quote-data-group-header/quote-data-group-header.php
<?php
$viewdefs['ProductBundles']['base']['view']['quote-data-group-header'] = array(
    ...
    'panels' => array(
        array(
            'name' => 'panel_quote_data_group_header',
            'label' => 'LBL_QUOTE_DATA_GROUP_HEADER',
            'fields' => array(
                array(
                    'name' => 'name',


array(
                    'name' => 'name',
                    'type' => 'quote-group-title',
                    'css_class' => 'group-name',
                ),
                'subtotal',
            ),
        ),
    ),
);
If adding a custom field 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.
Modifying Row Actions in the Group Header
The Quotes Group Header contains row actions to add QLIs and comments as well as editing and deleting QLI groupings. The actions are identified by, the plus and vertical ellipsis or "hamburger" icon buttons. The following section will outline how these items can be modified.


To modify the buttons in the Group Header, you can copy ./modules/ProductBundles/clients/base/views/quote-data-group-header/quote-data-group-header.php to ./custom/modules/ProductBundles/clients/base/views/quote-data-group-header/quote-data-group-header.php or you may create a metadata extension in ./custom/Extension/modules/ProductBundles/clients/base/views/quote-data-group-header/. Next, modify the $viewdefs['ProductBundles']['base']['view']['quote-data-group-header']['buttons']Â index to add or remove your preferred actions.
Example


Example
The example below will create a new view that extends the ProductBundlesQuoteDataGroupHeader view and append a row action to the Group Header vertical elipsis action list.
First, create your custom view type that will extend the ProductBundlesQuoteDataGroupHeader view.
./custom/modules/ProductBundles/clients/base/views/quote-data-group-header/quote-data-group-header.js
({
    extendsFrom: 'ProductBundlesQuoteDataGroupHeaderView',
    initialize: function(options) {
        this.events = _.extend({}, this.events, options.def.events, {
            'click [name="gh-custom-action"]': 'actionClicked'
        });
        this._super('initialize', [options]);
    },
    /**
     * Click event
     */


},
    /**
     * Click event
     */
    actionClicked: function() {
        app.alert.show('success_alert', {
            level: 'success',
            title: 'Group Header Button was clicked!'
        });
    },
})
This code will append a click event to our field named gh-custom-action and trigger the actionClicked method. Once completed, add your new row action to the appropriate button group inÂ $viewdefs['Quotes']['base']['view']['quote-data-group-header']['buttons']. For this example we will target a row action to the edit drop down that is identified in the arrays as havingÂ aÂ name of "edit-dropdown". Once found, add your new array to the buttons index of that array.


./custom/modules/ProductBundles/clients/base/views/quote-data-group-header/quote-data-group-header.php
<?php
$viewdefs['ProductBundles']['base']['view']['quote-data-group-header'] = array(
    'buttons' => array(
        array(
            'type' => 'quote-data-actiondropdown',
            'name' => 'create-dropdown',
            'icon' => 'fa-plus',
            'no_default_action' => true,
            'buttons' => array(
                array(
                    'type' => 'rowaction',
                    'css_class' => 'btn-invisible',
                    'icon' => 'fa-plus',
                    'name' => 'create_qli_button',


'name' => 'create_qli_button',
                    'label' => 'LBL_CREATE_QLI_BUTTON_LABEL',
                    'tooltip' => 'LBL_CREATE_QLI_BUTTON_TOOLTIP',
                    'acl_action' => 'create',
                ),
                array(
                    'type' => 'rowaction',
                    'css_class' => 'btn-invisible',
                    'icon' => 'fa-plus',
                    'name' => 'create_comment_button',
                    'label' => 'LBL_CREATE_COMMENT_BUTTON_LABEL',
                    'tooltip' => 'LBL_CREATE_COMMENT_BUTTON_TOOLTIP',
                    'acl_action' => 'create',
                ),
            ),
        ),
        array(


),
            ),
        ),
        array(
            'type' => 'quote-data-actiondropdown',
            'name' => 'edit-dropdown',
            'icon' => 'fa-ellipsis-v',
            'no_default_action' => true,
            'buttons' => array(
                array(
                    'type' => 'rowaction',
                    'name' => 'edit_bundle_button',
                    'label' => 'LBL_EDIT_BUTTON',
                    'tooltip' => 'LBL_EDIT_BUNDLE_BUTTON_TOOLTIP',
                    'acl_action' => 'edit',
                ),
                array(
                    'type' => 'rowaction',
                    'name' => 'delete_bundle_button',


'name' => 'delete_bundle_button',
                    'label' => 'LBL_DELETE_GROUP_BUTTON',
                    'tooltip' => 'LBL_DELETE_BUNDLE_BUTTON_TOOLTIP',
                    'acl_action' => 'delete',
                ),
                array(
                    'type' => 'rowaction',
                    'name' => 'gh-custom-action',
                    'label' => 'LBL_GH_CUSTOM_ACTION',
                    'tooltip' => 'LBL_GH_CUSTOM_ACTION_TOOLTIP',
                    'acl_action' => 'edit',
                ),
            ),
        ),
    ),
    ...
);
Finally, create labels under Quotes for the label and tooltip indexes. To accomplish this, create a language extension:


./custom/Extension/modules/Quotes/Ext/Language/en_us.gh-custom-action.php
<?php
$mod_strings['LBL_GH_CUSTOM_ACTION'] = 'Custom Action';
$mod_strings['LBL_GH_CUSTOM_ACTION_TOOLTIP'] = 'Custom Action Tooltip';
Once the files are in place, navigate to Admin > Repair > Quick Repair and Rebuild. Your changes will now be reflected in the system.
Group List
The Group List contains the comments and selected quoted line items.
Modifying Fields in the Group List


Modifying Fields in the Group List
To modify the Group List fields, you can copy ./modules/Products/clients/base/views/quote-data-group-list/quote-data-group-list.php to ./custom/modules/Products/clients/base/views/quote-data-group-list/quote-data-group-list.php or you may create a metadata extension in ./custom/Extension/modules/Products/Ext/clients/base/views/quote-data-group-list/. Next, modify the $viewdefs['Products']['base']['view']['quote-data-group-list']['panels'] index to add or remove your preferred fields.
Example


Example
The example below will remove the "Manufacturer Part Number" (products.mft_part_num) and append the "Vendor Part Number" (products.vendor_part_num) field to the Group List.
./custom/modules/Products/clients/base/views/quote-data-group-list/quote-data-group-list.php
<?php
$viewdefs['Products']['base']['view']['quote-data-group-list'] = array(
    'panels' => array(
        array(
            'name' => 'products_quote_data_group_list',
            'label' => 'LBL_PRODUCTS_QUOTE_DATA_LIST',
            'fields' => array(
                array(
                    'name' => 'line_num',
                    'label' => null,


'name' => 'line_num',
                    'label' => null,
                    'widthClass' => 'cell-xsmall',
                    'css_class' => 'line_num tcenter',
                    'type' => 'line-num',
                    'readonly' => true,
                ),
                array(
                    'name' => 'quantity',
                    'label' => 'LBL_QUANTITY',
                    'widthClass' => 'cell-small',
                    'css_class' => 'quantity',
                    'type' => 'float',
                ),
                array(
                    'name' => 'product_template_name',
                    'label' => 'LBL_ITEM_NAME',
                    'widthClass' => 'cell-large',
                    'type' => 'quote-data-relate',
                    'required' => true,


'required' => true,
                ),
                array(
                    'name' => 'vendor_part_num',
                    'label' => 'LBL_VENDOR_PART_NUM',
                    'type' => 'base',
                ),
                array(
                    'name' => 'discount_price',
                    'label' => 'LBL_DISCOUNT_PRICE',
                    'type' => 'currency',
                    'convertToBase' => true,
                    'showTransactionalAmount' => true,
                    'related_fields' => array(
                        'discount_price',
                        'currency_id',
                        'base_rate',
                    ),
                ),
                array(
                    'name' => 'discount',
                    'type' => 'fieldset',


'name' => 'discount',
                    'type' => 'fieldset',
                    'css_class' => 'quote-discount-percent',
                    'label' => 'LBL_DISCOUNT_AMOUNT',
                    'fields' => array(
                        array(
                            'name' => 'discount_amount',
                            'label' => 'LBL_DISCOUNT_AMOUNT',
                            'type' => 'discount',
                            'convertToBase' => true,
                            'showTransactionalAmount' => true,
                        ),
                        array(
                            'type' => 'discount-select',
                            'name' => 'discount_select',
                            'no_default_action' => true,
                            'buttons' => array(
                                array(


'buttons' => array(
                                array(
                                    'type' => 'rowaction',
                                    'name' => 'select_discount_amount_button',
                                    'label' => 'LBL_DISCOUNT_AMOUNT',
                                    'event' => 'button:discount_select_change:click',
                                ),
                                array(
                                    'type' => 'rowaction',
                                    'name' => 'select_discount_percent_button',
                                    'label' => 'LBL_DISCOUNT_PERCENT',
                                    'event' => 'button:discount_select_change:click',
                                ),
                            ),
                        ),
                    ),
                ),
                array(
                    'name' => 'total_amount',


),
                array(
                    'name' => 'total_amount',
                    'label' => 'LBL_LINE_ITEM_TOTAL',
                    'type' => 'currency',
                    'widthClass' => 'cell-medium',
                    'showTransactionalAmount' => true,
                    'related_fields' => array(
                        'total_amount',
                        'currency_id',
                        'base_rate',
                    ),
                ),
            ),
        ),
    ),
);
Next, create the LBL_VENDOR_PART_NUM label under Quotes as this is not included in the stock installation. To accomplish this, we will need to create a language extension:
./custom/Extension/modules/Quotes/Ext/Language/en_us.vendor.php
<?php


<?php
$mod_strings['LBL_VENDOR_PART_NUM'] = 'Vendor Part Number';
If adding a custom field 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.
Modifying Row Actions in the Group List
The Quotes Group List contains row actions to add/remove QLIs and comments. The actions are identified by, the vertical ellipsis icon buttons. The following section will outline how these items can be modified.


To modify the buttons in the Group List , you can copy ./modules/ProductBundles/clients/base/views/quote-data-group-list/quote-data-group-list.php to ./custom/modules/ProductBundles/clients/base/views/quote-data-group-list/quote-data-group-list.php or you may create a metadata extension in ./custom/Extension/modules/ProductBundles/clients/base/views/quote-data-group-list/. Next, modify the $viewdefs['ProductBundles']['base']['view']['quote-data-group-list']['selection'] index to add or remove your preferred actions.
Example