Customizing SugarCRM Advanced Search in 5.x – Part I

The form itself is part of the new MVC ListView code.

The view is called with the following default params when transitioning from Basic to Advanced Search:

action ListView
module Contacts
search_form_only true
search_form_view advanced_search
to_pdf true

The view controller eventually calls ListViewSmarty with the above. If the above is the example situation, the view controller is found at modules/Contacts/views/view.list.php. The actual call is towards the end of the display() method. This method instantiates the 5.x Search class, populates it with a seed SugarBean (Contacts in this case), then calls setup().

$search->setup() is passed an array named $searchdefs. This is provided by a metadata file generally found in the module’s metadata folder, logically named “searchdefs.php”. setup() will eventually call _build_field_defs() which is tasked with the job of parsing the passed metadata.

To get $searchdefs setup for a custom search criteria, one must add an array with some fairly cryptic and totally undocumented metadata. I only did as much as I needed to get the customization working, but most if not all the valid attributes can be parsed out of the code in include/SearchForm/SearchForm2.php. The pertinent method call is _build_field_defs(), and towards the end of the call, you’ll see feelers for attributes named “function” and “params”. Taking advantage of some of the hooks offered here, you can put together a decent custom search widget as needed (I went the ‘function’ route to return ‘html’).

The pertinent array that I added to $searchdefs[‘Contacts’][‘layout’][‘advanced_search’]:

array(
‘name’ => ‘tags’,
‘label’ => ‘LBL_TAGS’,
‘type’ => ‘varchar’,
‘function’ => array(
‘name’ => ‘testAdvSearch’,
‘include’ => ‘modules/Tags/Tag_utils.php’,
‘returns’ => ‘html’,
)
),
This feeds off merging $searchdef metadata with the module’s vardef metadata. The catch is that it iterates through the module’s metadata before merging, so in effect, you must add a full attribute entry to do any kind of customization. Crap design here.

In 5.x, vardefs are generated from custom/Extension/modules/[module]/Vardefs/[file].php. By adding a simple PHP file with the appropriate array structure, it’s fairly easy to partition out customizations. All files are picked up and merged with “final” or “published” concatenations which are build when running a full Extension rebuild via Admin -> Repair. Don’t forget to do this if you’re putting custom strings in for a given module.

Once the field is complete, then it’s time to worry about processing the user’s input.

SugarCRM Reports Customizations – Custom Filter Hacking

Goal:

Customize the UI Front-end of SugarCRM Reports to allow custom widgets for use with Reports filtering. The particular case is a series of dependent dropdowns with a key/value lists totalling over 18,000 items.

Assumptions:

1. Each filter equates to a single WHERE clause in Report query generation. Custom widget values may or may not play nice with this format, fortunately my case does.

2. All dependent dropdown values may be used (up to four), but they must be used sequentially; i.e., if a value in a 3rd degree dropdown is the desired filter value, the first and second dropdowns must be selected in sequence. The counter-point to this would be to support a dropdown at the fourth degree with over 18,000 options.

Plan of Attack

1. Hook or implement the javascript generation routines to allow calling of my DD javascript. Maybe with a custom attribute in the vardefs?

2. The key of each enumerated list (option key/value pairs) is what is stored in their respective columns in the Cases module; once this value is obtained, then the query can run as usual ([WHERE] [col] = ‘[value]’). This would be the most workable solution.

Performance impact of trying to filter on a 3rd/4th degree dependency should be negligible, even slightly faster since those four columns should be indexed.

Solution Implementation Steps:

1. The javascript file that supplies the metadata for the Reports UI is ./cache/modules/modules_def_[lang].js.

  • That file is generated as a sub-routine when within the Reports module.
  • The direct call is made when a Reports object is instantiated. The call is the cache_modules_def_js() method of the Reports class.
    • This is a perfect example of how this module is horribly written. The call to “cache” the above file is left as a: if(true || somecondition) {…}
  • The above inline includes a PHP file that generates the javascript cache file: ./modules/Reports/templates/templates_modules_def.php.
    • This is where the customization will occur.

2. To properly hook the javascript generation routine, the usage of that metadata must be explored. On a typical Reports creation screen a tabbed UI is generated based on the setup sequence (brand new vs. Saved Report).

template_reports_report($reporter, $args)

All Reports javascript calls that aren’t generated dynamically live in ./include/javascript/report_addtionals.js

Clicking “Add Filter” calls addFilter().

This creates a row in the Filters table with 5 cells. The cell that we need to hack is cell (column) 4, the “input_cell”.

Creating the “input_cell” is a call to addFilterInput(input_cell, filter). This call parses the generated field metadata and builds up the widget. Our particular instance is an “enum” field_type which calls either addFilterInputSelectMultiple() or addFilterInputSelectSingle() depending on other attributes.

At this point, we have the option of over-loading addFilterInputSelectSingle or hooking a third option at the previous step in the call stack (addFilterInput).  Either way, to make this all as upgrade-safe as possible, we need to leave the various javascript files alone and only replicate the minimum amount of code.

The other hard-criteria is that dependent dropdowns get their enum pairs via an asynchronous call which doesn’t block code execution which may make the rest of the addFilterInput() call fail.

  • force the async call to run synchronously?

SugarCRM Reports Customizations

Assumptions:

  1. Goal is custom rendering of data on Report generation/views
  2. Data is text-based
  3. No pre-existing SugarWidgets will work
  4. Schema is fubar, so hard-coded widgets are necessary.

Salient Points:

  1. Render happens with this stack:
    • modules/Reports/index.php : template_reports_report()
    • modules/Reports/templates/templates_reports.php : template_list_view()
    • modules/Reports/templates/templates_list_view.php : template_list_row()
    • modules/Reports/templates/templates_list_view.php : line 386
  2. Actual render manipulation of a cell of data:
    1. modules/Reports/Reports.php : line 1692 – $display = $this->layout_manager->widgetDisplay($display_column);
    2. Above is dependent on definition of $display_column which is an attribute of the associative array $this->report_def ($this is an instance of Report)
      1. $report_def comes from the saved metadata contained in a SavedReport or is passed via POST from the preceding form.
      2. The targetted change in metadata is an attribute “widget_class” that pertains to a SugarWidgetField[class]. This can be leveraged to customize output.
        1. The trick is how to add that attribute on the fly?

So? how to?

The unfortunate structure of Reports requires hard-coded customizations to manipulate data display beyond the OOTB widget Sugar ships.

For in-browser Reports:

My field was of type “varchar” which corresponds to the widget found at: include/generic/SugarWidgets/SugarWidgetsFieldsvarchar.php.

The Reports code instantiates the widget class, feels for certain methods and calls those methods if found. The interesting one here is “displayList()“. It doesn’t exists in the OOTB widget, so I created a new method and populated with some seriously hacky code to manipulate my data as I needed.

For PDF Reports

You would think that given the mess that Reports is, it would try to keep its separate outputs as similar as possible and maybe emulate a MVC paradigm. Too much to hope for; presentation and content code are intermingled as if no thought to code upkeep was ever entertained. Bitching aside, the way to do it:

modules/Reports/templates/templates_pdf.php : template_listview_pdf()

Find the while() loop that iterates through the returned rows/columns and stuff the returned text into a multi-D array that is passed as metadata to the EzPDF class. Insert your logic hook based on whatever criteria in the while loop and profit.

For CSV Reports

Much like the PDF reports hack above, the solution is implemented within the while() loop:

modules/Reports/templates/templates_export.php : template_handle_export()

SugarCRM Reports Suck

SugarCRM Reports customizations are a friggin PITA. There is no separation between presentation and business logic making any kind of enhancement, let alone bug fixes, intuitive, pleasant, or in some cases, doable at all. Forget upgrade safe, as bugridden as the code is, any changes you make will more than likely get stomped in the next patch.

I haven’t had to work in this mess in months, but a recent project has me trying to create a customization that has me wading through this steaming pile just to get a standard Rows & Columns Reports to show a value that is not the exact string stored in the cell.