Wednesday, January 1, 2020

PHPStorm and Code Completion with BeanFactory

PHPStorm and Code Completion with BeanFactory

I have been using PHPStorm with SugarCRM for most of a decade now and it is great.  For writing PHP code it is at the top of the heap.  The one place where I have a problem is with code completion with SugarCRM and BeanFactory.  This video shows what I am talking about.



Tuesday, December 31, 2019

My new blog

My name is Ken Brill.  I live in central Missouri about 100 miles south of St. Louis. My wife and I run a small 30-acre hobby farm here in Missouri, and I work as a developer from a small, dark corner in my basement.  Out here we only have satellite Internet (Viasat) so that's fun.  They advertise 25Mbs but I rarely see anything better than 12Mbs.  It does the job but I miss my cable modem sometimes.

On the farm, we raise about a dozen goats and sheep, 2 cows, 3 horses, 7 dogs (ranging from a Chihuahua and Maltese to a pair of Great Pyrenees, 6 cats of no discernible use, 4 kids (all girls), and a few dozen Chickens, Guinea fowl, Peacocks, Turkeys, and Ducks.  We have been at it for 5 years now so we are still learning but it's a lot of fun when it's not freezing outside.




The last 12 years I have specialized in SugarCRM.  I began with version 2.5 when a small company I worked for started using it and I have been working on SugarCRM ever since.  I had my own consulting company for a few years where I sold a product called 'Sugar Fully Loaded' that was essentially the SugarCRM CE product with several popular add-ons already loaded and merged (no such thing as upgrade-safe back then).  I spent 9 years after that working at SugarCRM in different roles and now I work remotely as a senior developer on an enterprise level installation and we are getting ready to roll out version 7.10 next year.

Wednesday, March 27, 2019

Adding Audit Logs to Reports

My users had a need to query the audit log for various modules in ways that the current built in system would not allow.  Now I could create advanced report after advanced report but really I hate making advanced reports. So I added myself a JIRA task to create some way of reporting on the Audit Logs.

All the files are available for download from the GitHUB link at the end of this post.

This was my list of criteria for this project
  1. I need a way to report on any modules audit log
  2. I need a way that allows a manager to create their own reports
  3. That method has to be familiar to them already (this was really me saying I wanted it in the Reports module)
  4. It must not allow editing of Audit Logs in any way
  5. It must be fast over millions of records (some of our audit logs are huge)
  6. It must translate all the raw data back into English
  7. It must allow our system admins to include that new audit_events table data in the reports
  8. It cant break the current Audit code

Wednesday, November 7, 2018

Asynchronous Bean Saves

Why I needed this

In my SugarCRM installation I have many logic hooks that update values in other modules.  For example, when you save a note it, in the before_save logic hook, it updates a "Last Note Update" field (among others) in the related Case.  So that means that you save the note and it has to run all the logic hooks, workflows, advanced workflows and SugarLogic on that note and then it would have to run all that stuff on the related case as well, all while the user waits for their save to finish.

In the past to avoid the overhead of a bean save I would just update the related bean with a direct SQL call.  But this means you lose out on logic_hooks, workflow and all the rest.  So if you can off-load all that extra bean save overhead to a JobQueue job, it will speed up saves while not impacting workflow or logic hooks the way a direct SQL call would.  The JobQueue is explained in the developer guide here.

So this is an example of the kind of code I am talking about and the new code I use


<?php
//OLD WAY
$opportunityBean = BeanFactory::getBean('Opportunities', $bean->parent_id);
$opportunityBean->last_note_created_c = $bean->date_entered;
$opportunityBean->last_note_c = $bean->description;
$opportunityBean->save();
//NEW WAY
$data = array('last_note_created_c' => $bean->date_entered,
'last_note_c' => $bean->description);
//I feed it the name of the module and the ID instead of a BEAN because
// I don't want to waste the time loading the bean here. Let the JobQueue do that
updateBean('Opportunities', $bean->parent_id, $data);
view raw bs_hook.php hosted with ❤ by GitHub

The 'new' code would call a function I have placed in a file called custom/Extension/application/Ext/Utils/UpdateBean.php.  
This makes it available anywhere in the app. Custom utilities are in the developer guide here.  That file looks like this

<?php
/**
* @param string $beanModule
* @param string $beanID
* @param array $data
*/
function updateBean($beanModule, $beanID, $data = array())
{
$job = new SchedulersJob();
$job->name = "Update trigger_workflow_c - {$beanModule}:{$beanID}";
$data = array(
'beanModule' => $beanModule,
'beanID' => $beanID,
'updatedFields' => $data);
$jsonData = json_encode($data);
$job->data = $jsonData;
$job->target = 'class::updateBeanJob';
//user the job runs as admin so we don't have permission issues
$job->assigned_user_id = '1';
// Now push into the queue to run
$jq = new SugarJobQueue();
$jobid = $jq->submitJob($job);
}
view raw UpdateBean.php hosted with ❤ by GitHub
That function submits a job to the job queue and it will run the next time cron runs.  So you bean save might be delayed by a minute but your user doesn't have to sit through it.  The final file, the job itself is in a file called custom/Extension/modules/Schedulers/Ext/ScheduledTasks/updateBeanJob.php and looks like this


<?php
if (!defined('sugarEntry') || !sugarEntry) {
die('Not A Valid Entry Point');
}
class updateBeanJob implements RunnableSchedulerJob
{
public function setJob(SchedulersJob $job)
{
$this->job = $job;
}
public function run($data)
{
$decodedData = json_decode($data);
//Get needed data
$beanModule = $decodedData->beanModule;
$beanID = $decodedData->beanID;
$updatedFields = $decodedData->updatedFields;
//Load Bean
// I disable teams and the cache just in case, the Job Queue usually runs as admin
// so I am not sure it is really necessary.
$params = array('use_cache' => false, 'disable_row_level_security' => true);
$focus = BeanFactory::getBean($beanModule, $beanID, $params);
//Fill in data
// I iterate through the array fed to the class and update them in the loaded Bean
foreach ($updatedFields as $fieldName => $fieldValue) {
$focus->$fieldName = $fieldValue;
}
//Save
$focus->save();
//always return TRUE as if you don't the Job will be marked as failing even though it worked.
return true;
}
}

So thats it, easy Asynchronous bean saves.

Friday, October 19, 2018

A Custom SugarLogic Expression

I made my first custom SugarLogic expression tonight. It turned out to be fairly easy but the Manual has small error in it and it fails to explain one thing.

 So here is the complete story.  First, this is the Developer Guide Manual page for this. Its fairly complete but it tells you to run the SugarLogic repair from the 'Schedulers' menu instead of the 'Repair' menu, small detail. It also does not really go over where the little 'how to' popup text is created. I will go over that here.

 For this demo I made a simple SugarLogic expression that returns the number of years from a date until now. It was to show the age of a person based on a birthday field. Here is the code that I placed in this file 'custom/include/Expressions/Expression/Date/NumberOfYearsExpression.php'

<?php
/**
* <b>NumberOfYears(Date d)</b><br>
* Returns number of years since the specified date.
*/
class NumberOfYearsExpression extends NumericExpression
{
/**
* Returns the entire enumeration bare.
*/
function evaluate() {
$params = DateExpression::parse($this->getParameters()->evaluate());
if(!$params) {
return false;
}
$now = TimeDate::getInstance()->getNow(true);
//set the time to 0, as we are returning an integer based on the date.
$params->setTime(0, 0, 0); // this will be the timestamp delimiter of the day.
$tsdiff = $params->ts - $now->ts;
$diff = (int)ceil($tsdiff/31536000);
return $diff;
}
/**
* Returns the JS Equivalent of the evaluate function.
*/
static function getJSEvaluate() {
return <<<EOQ
var then = SUGAR.util.DateUtils.parse(this.getParameters().evaluate(), 'user');
var now = new Date();
then.setHours(0);
then.setMinutes(0);
then.setSeconds(0);
var diff = then - now;
var years = Math.ceil(diff / 31536000000);
return years;
EOQ;
}
/**
* Returns the operation name that this Expression should be
* called by.
*/
static function getOperationName() {
return "NumberOfYears";
}
/**
* All parameters have to be a date.
*/
static function getParameterTypes() {
return array(AbstractExpression::$DATE_TYPE);
}
/**
* Returns the maximum number of parameters needed.
*/
static function getParamCount() {
return 1;
}
/**
* Returns the String representation of this Expression.
*/
function toString() {
}
}

Once you have the file in place you run the Admin > Repair > Rebuild Sugar Logic Functions repair and then clear your local browser cache. That last step is crucial.

There is a popup on the Expression Editor that explains hwo to use a function, in this case it looks like this


The text for this popup comes from that comment at the top of the file there, I dont know how or where they do that and it seems rather silly to do it that way but there you go.  The comment in question is above the CLASS in the code

/**
 * <b>NumberOfYears(Date d)</b><br>
* Returns number of years since the specified date.
*/


Monday, October 8, 2018

Make a new team the primary team with confirmation alert

Where I work we use Tasks to pass around work from one department to the next.  So a Lead might come in and a Task on that Lead is assigned to the Sales team.  Once the Sales team finishes whatever they need to do they assign the Task to the Legal team.  The Legal team then passes it to the Order Management team and so on.

One of the issues with this was that we couldnt preadd the teams as tasks take their own path sometimes and it muddied reports when we did that.  So each team needed to add the next team to the Task and mark it as Primary. 

Well, as with most manual workflows, it didn't always get marked as primary.  So I came up with the code below to ask the user, after they add a team, if they want to make it the primary team and if they say yes, then mark it as primary.  It took forever to figure out how to tell if a team had been added, and I dont really like this solution but it worked the best so I went with it.

So first the code, it just goes in the record.js of whatever module you want to add this to.

({
extendsFrom: 'RecordView',
initialize: function (options) {
this._super('initialize', [options]);
},
_render: function () {
this._super('_render');
this.model.on('change:team_name', this.onChangeTeamName, this);
},
onChangeTeamName: function () {
var teamField = this.getField('team_name');
var teams = teamField.value;
var teamIDs = [];
var teamLock = sessionStorage.getItem('s2s_teamLock');
if (teamLock == 1) {
return null;
}
//Only run this on an edit view
if (this.action != 'edit') {
sessionStorage.removeItem('s2s_teamLock');
return null;
}
for (i = 0; i < teams.length; i++) {
if (!_.isEmpty(teams[i]['id']) && !_.isUndefined(teams[i]['id'])) {
teamIDs[i] = teams[i]['id'];
} else {
//If we get here then the user is adding a team
sessionStorage.setItem('s2s_teamArray', JSON.stringify(teamIDs));
return null;
}
}
//if there are no teams then just return
if (teamIDs.length == 0) {
return null;
}
var prevTeams = sessionStorage.getItem('s2s_teamArray');
if (_.isEmpty(prevTeams) || _.isUndefined(prevTeams)) {
sessionStorage.setItem('s2s_teamArray', JSON.stringify(teamIDs));
prevTeams = teamIDs;
} else {
prevTeams = JSON.parse(prevTeams);
}
var arrayDiff = this.arrayDiff(teamIDs, prevTeams);
if (arrayDiff.length > 0) {
var teamIndex = teamIDs.indexOf(arrayDiff[0]);
if (!_.isUndefined(teams[teamIndex])) {
var teamDisplayName = teams[teamIndex]['name'];
var message = app.lang.get('MSG_PRIMARY_TEAM001', this.module, {
teamName: teamDisplayName
});
app.alert.show('message-id', {
level: 'confirmation',
messages: message,
autoClose: false,
confirm: {
label: 'Yes'
},
cancel: {
label: 'No'
},
onConfirm: function () {
sessionStorage.setItem('s2s_teamLock', '1');
teamField.setPrimary(teamIndex);
sessionStorage.removeItem('s2s_teamLock');
sessionStorage.removeItem('s2s_teamArray');
}
,
onCancel: function () {
sessionStorage.removeItem('s2s_teamLock');
sessionStorage.removeItem('s2s_teamArray');
}
});
}
}
return true;
},
arrayDiff: function (a1, a2) {
var a = [], diff = [];
for (var i = 0; i < a1.length; i++) {
a[a1[i]] = true;
}
for (var i = 0; i < a2.length; i++) {
if (a[a2[i]]) {
delete a[a2[i]];
} else {
a[a2[i]] = true;
}
}
for (var k in a) {
diff.push(k);
}
return diff;
}
});
view raw record.js hosted with ❤ by GitHub


Then add a line to the language file custom/include/language/en_us.lang.php that reads.  Notice how I am filling in the 'TeamName' variable with the app.lang.get() code in the record.js file.

$app_strings['MSG_PRIMARY_TEAM001'] = "Set \"{{teamName}}\" to be the primary team?";
view raw en_us.lang.php hosted with ❤ by GitHub


With this code in place (after a QR&R of course) when a new team is added, the system will ask the use if they want that team marked as primary.  If they say yes then it will be.

Undocumented Alert() options - Confirmation label buttons

Normally, on a confirmation alert the two options are Confirm and Cancel.  You can change the labels on these two buttons with the code below.


app.alert.show('message-id', {
level: 'confirmation',
messages: message,
autoClose: false,
confirm: {
label: 'Yes'
},
cancel: {
label: 'No'
},
onConfirm: function () {
sessionStorage.setItem('s2s_teamLock', '1');
teamField.setPrimary(teamIndex);
sessionStorage.removeItem('s2s_teamLock');
sessionStorage.removeItem('s2s_teamArray');
}
,
onCancel: function () {
sessionStorage.removeItem('s2s_teamLock');
sessionStorage.removeItem('s2s_teamArray');
}
});
view raw record.js hosted with ❤ by GitHub