Quantcast
Channel: Kris Halsrud – Customer FX
Viewing all 168 articles
Browse latest View live

Showing Local Time in the Infor CRM (Saleslogix) Web Client Ticket History Tab

$
0
0

I wrote previously about how to adjust the journal table so that dates do not show the UTC values recorded as data changes against a ticket. That previous post was back when the grid was a SLXGrid and control and the code contained an example of how to convert the time based on the server’s time zone. not ideal since your users are likely not in the the same timezone as the web server.

The Ticket Hisotry tab in Infor CRM 8.2 now utilizes as Editable SData based grid control. These grids are constructed client side using Dojo and are populated via a client side SData feed. The good news with that is now we can work with javascript to get the user’s timezone and adjust the dates. Let’s take a look at how to do that.

The Editable grid has a Columns collection exposed. Opening this we can see the columns listed in the grid.
Infor CRM EditableGrid Columns Collection

Each text based column has a Custom Format Property. Since we want to change both the OldValue and NewValue columns so they display the local date/timew rather that the UTC date/time, we will want to modify the Custom Format Function of each column. The workflow is exactly the same for both so I am just going to walk though changing one. When you do it you will need to do it to both columns.

Opening the Custom Format Function of the OldValue column we can see existing code in place.
Infor CRM EditableGrid Columns Format Code Editor

At the very end of the code you will see a block of case statements:

switch (fieldName) {
	case "STATUSCODE":
		return formatPicklistid("Ticket Status", value);
	case "ASSIGNEDTOID": case "RECEIVEDBYID": case "COMPLETEDBYID":
		return formatSdata("owners", value);
	case "ACCOUNTID":
		return formatSdata("accounts", value);
	case "CONTACTID":
		return formatSdata("contacts", value);
}
return value;

We want to add additional conditions, to check for our date fields. There are several, so we can do something like this:

switch (fieldName) {
	case "STATUSCODE":
		return formatPicklistid("Ticket Status", value);
	case "ASSIGNEDTOID": case "RECEIVEDBYID": case "COMPLETEDBYID":
		return formatSdata("owners", value);
	case "ACCOUNTID":
		return formatSdata("accounts", value);
	case "CONTACTID":
		return formatSdata("contacts", value);
	case "ASSIGNEDDATE": case "COMPLETEDDATE": case "RECEIVEDDATE":
		return formatTime(value);
}
return value;

For my new case statement I am returning the result of a new function called formatTime. Lets see that:

var formatTime = function(time) {
	var formatTime = function (time) {		          
        var date = new Date(time + ' UTC');
        return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();		            		        
};

Remember this function needs to be declared before it is first used, so this has to be placed above our case statements. This new function takes the date value and simply adds UTC to the end of it. Then we can case that to the local equivalent date and time using toLocaleDateString and toLocaleTimeString, which for me looks like 01/20/2010 12:35 PM.

The final complete code:

(function () {
		    var cache = {};
		    return function (value, inRowIndex, cell) {
		        // Special format for some of the history fields
		        var item = cell.grid.getItem(inRowIndex);
                var fieldName = cell.grid.store.getValue(item, "FieldName").toUpperCase();

                if(cache[fieldName + "/" + value]){
                    return cache[fieldName + "/" + value];
                }		        
		        var formatPicklistid = function (pickListName, itemid) {
		            var deferred = new dojo.Deferred();
		            var result = itemid;
		            var config = {
		                pickListName: pickListName, // Required
		                storeMode: "id",
		                displayMode: "text"
		            };
		            this.pickList = new Sage.UI.Controls.PickList(config);
		            this.pickList.getPickListData(deferred);
		            deferred.then(dojo.hitch(this,
                        function (data) {
                            dojo.forEach(data.items.$resources, function (pickListItem, index, array) {
                                //console.log(item.id + ' === ' + id);
                                if (pickListItem.$key === itemid) {
                                    result = pickListItem.text;
                                }
                            }, this);

                        }),
                        function (e) {
                            console.error(e); // errback
                        }
                    );
		            return result;
		        };
		        var formatSdata = function (resourceKind, id) {
		            console.log("Format sdata: " + resourceKind + " / " + id);
		            var def = new dojo.Deferred();
		            dojo.xhrGet({
		                url: "slxdata.ashx/slx/dynamic/-/" + resourceKind + "('" + id + "')?format=json&select=Id",
		                handleAs: "json",
		                load: function (data) {
                            cache[fieldName + "/" + value] = data.$descriptor;
		                    def.resolve(data.$descriptor);
		                }
		            });
		            return def;
		        };
				
		        var formatTime = function (time) {		            
		            var date = new Date(time + ' UTC');
		            return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();		            
		        };
		        
		        switch (fieldName) {
		            case "STATUSCODE":
		                return formatPicklistid("Ticket Status", value);
		            case "ASSIGNEDTOID": case "RECEIVEDBYID": case "COMPLETEDBYID":
		                return formatSdata("owners", value);
		            case "ACCOUNTID":
		                return formatSdata("accounts", value);
		            case "CONTACTID":
		                return formatSdata("contacts", value);
			    case "ASSIGNEDDATE": case "COMPLETEDDATE": case "RECEIVEDDATE":
		                return formatTime(value);
		        }
		        return value;
		    };
		})()


Infor CRM 8.2.1 Web Client issues with Milliseconds

$
0
0

We recently had a client who ran into issues trying to edit some of their activities in the Infor CRM web client. The same activities could be edited in the Windows client successfully.
When attempting to edit the record in the web client the system throws an exception with event log stack trace shown below.

The issue is that the Infor CRM web client is not handling dates that include milliseconds.

In the underlying SQL queries the entity model attempts to find records based on where conditions containing dates. There where condition looks something like “where datefield = ‘20151014 17:37:11.000′” If your date field is actually something like “20151014 17:37:11.783” then the SQL query does not return the record because the times are not equivalent.

Steps to reproduce:

  • Find an existing or create a new activity.
  • Get the activity ID
  • Run this sql script against the activity ID you get:
update activity set startdate='2015-10-14 17:37:11.783', alarmtime='2015-10-14 17:37:11.783' where activityid='VCZV0A00NY2Z'
  • Now try to edit the activity. You will get the error.
  • Now run this SQL script against the activity ID you get:
  • update activity set startdate='2015-10-14 17:37:11.000', ALARMTIME='2015-10-14 17:37:11.000' where activityid='VCZV0A00NY2Z'
  • The activity can now be edited.
  • To fix this you can run a SQL script like this

    update sysdba.activity set alarmtime = DATEADD(ms, -DATEPART(ms, alarmtime), alarmtime) where datepart(ms, alarmtime)>0
    update sysdba.activity set StartDate = DATEADD(ms, -DATEPART(ms, StartDate), StartDate) where datepart(ms, StartDate)>0

    Using the SQL function getdate() or getutcdate() includes milliseconds so if you are populating dates directly in SQL you can get this behavior. You can instead use a different SQL function to get the date with no milliseconds specified:

    select CONVERT(DATETIME2(0),SYSDATETIME()) --gets local date time
    select CONVERT(DATETIME2(0),SYSUTCDATETIME()) --gets UTC current date time

    Stack Trace:

    "slxErrorId": "SLXCF3F7BA36D6FB743",
    "mitigation": "AjaxMessagingServiceError (500)",
    "date": "2016-03-16T08:07:45",
    "utc": "2016-03-16T15:07:45",
    "message": "Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [Sage.SalesLogix.Activity.Activity#VCZV0A00NY2Z].",
    "source": "NHibernate.Persister.Entity.AbstractEntityPersister, NHibernate, Version=3.3.1.4000, Culture=neutral, PublicKeyToken=aa95f207798dfdb4",
    "type": "NHibernate.StaleObjectStateException",
    "stackTrace": " at NHibernate.Persister.Entity.AbstractEntityPersister.Update(Object id, Object[] fields, Object[] oldFields, Object rowId, Boolean[] includeProperty, Int32 j, Object oldVersion, Object obj, SqlCommandInfo sql, ISessionImplementor session)\r\n at NHibernate.Persister.Entity.AbstractEntityPersister.UpdateOrInsert(Object id, Object[] fields, Object[] oldFields, Object rowId, Boolean[] includeProperty, Int32 j, Object oldVersion, Object obj, SqlCommandInfo sql, ISessionImplementor session)\r\n at NHibernate.Persister.Entity.AbstractEntityPersister.Update(Object id, Object[] fields, Int32[] dirtyFields, Boolean hasDirtyCollection, Object[] oldFields, Object oldVersion, Object obj, Object rowId, ISessionImplementor session)\r\n at NHibernate.Action.EntityUpdateAction.Execute()\r\n at NHibernate.Engine.ActionQueue.Execute(IExecutable executable)\r\n at NHibernate.Engine.ActionQueue.ExecuteActions(IList list)\r\n at NHibernate.Engine.ActionQueue.ExecuteActions()\r\n at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session)\r\n at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event)\r\n at NHibernate.Impl.SessionImpl.Flush()\r\n at Sage.SalesLogix.Activity.Entities.Activity.Save() in c:\\Users\\Administrator\\AppData\\Roaming\\Sage\\Platform\\Output\\implementation\\Activity.cs:line 1505\r\n at Sage.SalesLogix.Activity.Activity.Save()\r\n at Sage.SalesLogix.Activity.Entities.Activity.Sage.Platform.Orm.Interfaces.IPersistentEntity.Save() in c:\\Users\\Administrator\\AppData\\Roaming\\Sage\\Platform\\Output\\implementation\\Activity.cs:line 1621\r\n at Sage.Platform.NHibernateRepository.NHibernateRepository.SaveImpl(Object instance)\r\n at Sage.Platform.NHibernateRepository.NHibernateRepository.Sage.Platform.Repository.IRepository.Save(Object instance)\r\n at Sage.Platform.EntityFactory.Save(Object instance)\r\n at Sage.SalesLogix.SystemAdapter.Activities.ActivityRequestHandler.SaveEntity(IActivity entity)\r\n at Sage.Platform.SData.RequestHandlerBase`3.InternalPut(SDataUri uri, String ifMatch, TFeedEntry entry)\r\n at Sage.Platform.SData.RequestHandlerBase`3.Put(TFeedEntry entry)\r\n at Invoke3cd06cf009c44daabf3b80c0c55a4460.Invoke(Object , IRequest )\r\n at Sage.Integration.Messaging.RequestTargetRegistration.RequestTargetInvoker.Invoke(IRequest request)\r\n at Sage.Integration.Messaging.Request.Process(RequestTargetInvoker invoker)\r\n at Sage.Integration.Adapter.AdapterController.RealAdapterController.Process(IRequest request)\r\n at Sage.Integration.Adapter.AdapterController.RealAdapterController.ProcessWorker(IProtocolRequest protocolRequest)\r\n at Sage.Integration.Adapter.AdapterController.Process(IProtocolRequest request)\r\n at Sage.Integration.Messaging.MessagingService.Process(IProtocolRequest protocolRequest)",
    "targetSite": "Boolean Update(System.Object, System.Object[], System.Object[], System.Object, Boolean[], Int32, System.Object, System.Object, NHibernate.SqlCommand.SqlCommandInfo, NHibernate.Engine.ISessionImplementor)",
    "fullException": "Sage.Common.Syndication.DiagnosesException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [Sage.SalesLogix.Activity.Activity#VCZV0A00NY2Z] ---> NHibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [Sage.SalesLogix.Activity.Activity#VCZV0A00NY2Z]\r\n at NHibernate.Persister.Entity.AbstractEntityPersister.Update(Object id, Object[] fields, Object[] oldFields, Object rowId, Boolean[] includeProperty, Int32 j, Object oldVersion, Object obj, SqlCommandInfo sql, ISessionImplementor session)\r\n at NHibernate.Persister.Entity.AbstractEntityPersister.UpdateOrInsert(Object id, Object[] fields, Object[] oldFields, Object rowId, Boolean[] includeProperty, Int32 j, Object oldVersion, Object obj, SqlCommandInfo sql, ISessionImplementor session)\r\n at NHibernate.Persister.Entity.AbstractEntityPersister.Update(Object id, Object[] fields, Int32[] dirtyFields, Boolean hasDirtyCollection, Object[] oldFields, Object oldVersion, Object obj, Object rowId, ISessionImplementor session)\r\n at NHibernate.Action.EntityUpdateAction.Execute()\r\n at NHibernate.Engine.ActionQueue.Execute(IExecutable executable)\r\n at NHibernate.Engine.ActionQueue.ExecuteActions(IList list)\r\n at NHibernate.Engine.ActionQueue.ExecuteActions()\r\n at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session)\r\n at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event)\r\n at NHibernate.Impl.SessionImpl.Flush()\r\n at Sage.SalesLogix.Activity.Entities.Activity.Save() in c:\\Users\\Administrator\\AppData\\Roaming\\Sage\\Platform\\Output\\implementation\\Activity.cs:line 1505\r\n at Sage.SalesLogix.Activity.Activity.Save()\r\n at Sage.SalesLogix.Activity.Entities.Activity.Sage.Platform.Orm.Interfaces.IPersistentEntity.Save() in c:\\Users\\Administrator\\AppData\\Roaming\\Sage\\Platform\\Output\\implementation\\Activity.cs:line 1621\r\n at Sage.Platform.NHibernateRepository.NHibernateRepository.SaveImpl(Object instance)\r\n at Sage.Platform.NHibernateRepository.NHibernateRepository.Sage.Platform.Repository.IRepository.Save(Object instance)\r\n at Sage.Platform.EntityFactory.Save(Object instance)\r\n at Sage.SalesLogix.SystemAdapter.Activities.ActivityRequestHandler.SaveEntity(IActivity entity)\r\n at Sage.Platform.SData.RequestHandlerBase`3.InternalPut(SDataUri uri, String ifMatch, TFeedEntry entry)\r\n at Sage.Platform.SData.RequestHandlerBase`3.Put(TFeedEntry entry)\r\n at Invoke3cd06cf009c44daabf3b80c0c55a4460.Invoke(Object , IRequest )\r\n at Sage.Integration.Messaging.RequestTargetRegistration.RequestTargetInvoker.Invoke(IRequest request)\r\n at Sage.Integration.Messaging.Request.Process(RequestTargetInvoker invoker)\r\n at Sage.Integration.Adapter.AdapterController.RealAdapterController.Process(IRequest request)\r\n at Sage.Integration.Adapter.AdapterController.RealAdapterController.ProcessWorker(IProtocolRequest protocolRequest)\r\n at Sage.Integration.Adapter.AdapterController.Process(IProtocolRequest request)\r\n at Sage.Integration.Messaging.MessagingService.Process(IProtocolRequest protocolRequest)\r\n --- End of inner exception stack trace ---",
    "hashCode": "FA95BF4E-5A43D045-FC7846A0",
    "pid": 3716,
    "identity": {
    "name": "0017596",
    "isAuthenticated": true,
    "authenticationType": "Forms"
    },
    "version": "8.2.0.1211",
    "logger": {
    "level": "ERROR",
    "location": "Sage.Platform.Diagnostics.ErrorHelper.LogException(:0)",
    "name": "Global",
    "message": "Integration Messaging MessagingService unhandled exception [Saleslogix Error Id=SLXCF3F7BA36D6FB743]"
    },
    "request": {
    "looksLikeAjax": true,
    "isLocal": true,
    "method": "PUT",
    "url": "http://localhost/SlxClient/slxdata.ashx/slx/system/-/activities(\"VCZV0A00NY2Z\")?_compact=true&include=%24descriptors&format=json&_t=1458140865357",
    "referrer": "http://localhost/SlxClient/Account.aspx?entityid=ACZV0A00A8KP&modeid=Detail",
    "ipAddress": "::1",
    "userAgent": "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
    "userLanguages": "en-US; en;q=0.8"
    },
    "extendedExceptionInfo": [
    {
    "type": "NHibernate.StaleObjectStateException",
    "message": "Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [Sage.SalesLogix.Activity.Activity#VCZV0A00NY2Z]",
    "source": "NHibernate",
    "targetSite": "Boolean Update(System.Object, System.Object[], System.Object[], System.Object, Boolean[], Int32, System.Object, System.Object, NHibernate.SqlCommand.SqlCommandInfo, NHibernate.Engine.ISessionImplementor)",
    "stackPosition": 1,
    "entityName": "\"Sage.SalesLogix.Activity.Activity\"",
    "identifier": "\"VCZV0A00NY2Z\""
    }
    ],
    "browser": {
    "type": "Chrome49",
    "name": "Chrome",
    "version": "49.0",
    "majorVersion": 49,
    "minorVersion": 0.0,
    "platform": "WinNT"
    },
    "server": {
    "machineName": "SERVER",
    "timeZone": "Pacific Standard Time",
    "commandLine": "C:\\Windows\\SysWOW64\\inetsrv\\w3wp.exe -ap \"Saleslogix\" -v \"v4.0\" -l \"webengine4.dll\" -a \\\\.\\pipe\\iisipmf4ae07a0-527e-4a72-b137-6a46c798241c -h \"C:\\inetpub\\temp\\apppools\\Saleslogix\\Saleslogix.config\" -w \"\" -m 0",
    "versionString": "Microsoft Windows NT 6.2.9200.0",
    "is64BitOperatingSystem": true,
    "host": {
    "siteName": "Saleslogix",
    "applicationId": "/LM/W3SVC/2/ROOT/SlxClient",
    "applicationPhysicalPath": "c:\\inetpub\\wwwroot\\SlxClient\\",
    "applicationVirtualPath": "/SlxClient",
    "isDebuggingEnabled": false,
    "isHosted": true,
    "maxConcurrentRequestsPerCPU": 5000,
    "maxConcurrentThreadsPerCPU": 0
    },
    "logonUser": {
    "name": "SERVER\\WebDLL",
    "authenticationType": "Forms",
    "impersonationLevel": "Impersonation",
    "isAnonymous": false,
    "isGuest": false,
    "isSystem": false
    }
    }
    }
    

    Determining if a User is a Member of a Team in Infor CRM Web Part 2

    $
    0
    0

    Back in 2009, Ryan wrote about how to determine if a user is in a team. His last code snippet example is no longer valid as those methods now return different object types. His old code was this:

    Sage.SalesLogix.Security.SLXUserService usersvc = (Sage.SalesLogix.Security.SLXUserService)Sage.Platform.Application.ApplicationContext.Current.Services.Get<Sage.Platform.Security.IUserService>();
    Sage.SalesLogix.Security.User user = usersvc.GetUser();
     
    System.Collections.Generic.IList<Sage.SalesLogix.Security.Owner> teamlist = Sage.SalesLogix.Security.Owner.GetTeamsByUser(user);
     
    string list = string.Empty;
    foreach (Sage.SalesLogix.Security.Owner team in teamlist)
    {
        list += team.OwnerDescription + "rn";
    }
    QFTextBox.Text = list;
    

    The new code should be this:

        System.Collections.Generic.IList<Sage.Entity.Interfaces.IOwner> teamlist = Sage.SalesLogix.Security.Owner.GetTeamsByUser(Sage.SalesLogix.API.MySlx.Security.CurrentSalesLogixUser);
     
        string list = string.Empty;
        foreach (Sage.Entity.Interfaces.IOwner team in teamlist)
        {
            list += team.OwnerDescription + "rn";
        }
        QFTextBox.Text = list;
    

    Running a Crystal Report from a Button with Custom Conditions in the Infor CRM (Saleslogix) Web Client

    $
    0
    0

    In a previous post I talked about how to run a report based on the current entity from the click of a button.

    btnPreview.OnClientClick = @"var oReporting = Sage.Services.getService('ReportingService');
    oReporting.showReport('Sales Order:SalesOrder Detail', 'SALESORDER', Sage.Utility.getCurrentEntityId());
    return false;"; 
    

    That code calls the showReport method which requires 3 inputs, the report name and family, the main table to add the condition to, and the ID of that table to restrict down to. This work well for most cases, but what if you want to run a report and pass in your own custom parameters? Well you can use a couple of other aspects of the reporting service to do that. Calling the setReportJob requires you to pass in the plugin ID of the report, the main table to restrict and the ID of that table. However instead of then immediately showing the report this method lets you then change the rsf property to be some completely unrelated query to run the report with. The completed sample code looks like:

        btnPreview.OnClientClick = @"var oReporting = Sage.Services.getService('ReportingService');
        if (oReporting) {     
            var pluginId = oReporting.getReportId('Account:Account Detail');
            oReporting.setReportJob(pluginId, 'ACCOUNT', Sage.Utility.getCurrentEntityId());
            oReporting.reportJob.rsf = "{ACCOUNT.ACCOUNTID} = '123' and {ACCOUNT.TYPE} = 'customer' ";
            oReporting._showReport();
        }
        return false;";
    

    Building a new Job Service Job in Infor CRM Web- Step by Step

    $
    0
    0

    Using the power of the job service can open up a lot of tasks within the web client. By executing out of the web context they are a perfect place to run long running processes and because they can be scheduled easily, can allow a lot of cool things to happen.

    There is not a lot of documentation on exactly how to create a new job service job however which is where this post comes in. The first thing to know are the jobs are compiled assemblies. That means we are going to use Visual Studio to build this. I am not going to go into the ins and outs of using VS. What I will do is to define what you need minimally to put a job together.

    Step 1
    In Visual Studio, create an class library project.

    Step 2
    Add the required assemblies. All of these files can be found in a deployed Infor CRM client web site Bin folder. The assemblies required are:

    • NHibernate
    • Quartz
    • Sage.Entity.Interfaces (to work with the entity model)
    • Sage.Platform
    • Sage.Scheduling
    • Sage.SalesLogix.BusinessRules (if relying on that for existing methods)

     

    Step 3
    Add custom usings (the custom ones are after the line break)

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Globalization; 
    
    using Quartz;
    using Sage.Entity.Interfaces;
    using Sage.Platform.Orm;
    using Sage.Platform.Scheduling;
    using Sage.SalesLogix.BusinessRules;
    using System.ComponentModel;
    

    Step 4
    Create your public class name. This should inherit from SystemJobBase if you want the job to be tagged as a system job in the web client. The signature would look like:

    public class MyJobIsCool : SystemJobBase
    {
    }
    

    The class name will be used to select it as a job available to be used within the Application Architect, so name it something meaningful.

    In addition, you should add attributes to your class as follows:

    [DisallowConcurrentExecution]
    [Description("My Description")]
    [DisplayName("My Name")]
    public class MyJobIsCool : SystemJobBase
    {
    }
    

    These attributes are exposed by the job service and are what shows in the web client as the job name and description.

    Step 5
    Create the OnExecute method within your class. Only one method is required for your class. An overriden OnExecute. That signature look like this:

    protected override void OnExecute()
    {
    }
    

    Within that method there are a couple of built in outputs you can use:

    • Phase (string)- corresponds to the output of the Phase column in the Job Manager within the web client. Used as a main level status field.
    • PhaseDetail (string)- corresponds to the Phase Detail column in the Job Manager within the web client. Used as a more granular status within the main Phase.
    • Progress (decimal?)- corresponds to the progress bar percent complete (0-100). You can decide how this is set, either for the overall task or for within the Phases you are working in. Setting it to 100 does not mean anything, it is just informational.

    The following is a completed sample file that runs a query retrieving all contacts where the last name starts with A. It then loops through the records and updates all of their phone numbers to be a specific value. (not a real valuable example but hey)

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Globalization; 
    
    using Quartz;
    using Sage.Entity.Interfaces;
    using Sage.Platform.Orm;
    using Sage.Platform.Scheduling;
    using Sage.SalesLogix.BusinessRules;
    using System.ComponentModel;
    
    
    namespace FX.JobService
    {    
        [DisallowConcurrentExecution]
        [Description("My Description")]
        [DisplayName("My Name")]
        public class MyJobIsCool : SystemJobBase
        {
            private static void Log(string Message)
            {
                using (System.IO.TextWriter writer = new System.IO.StreamWriter(@"C:\Temp\Test_debug_log.txt", true))
                {
                    writer.WriteLine(string.Format("{0} - {1}", DateTime.Now, Message));
                }
            }
    
            private static readonly string _entityDisplayName = typeof(IContact).GetDisplayName();
            protected override void OnExecute()
            {
                //Set the main phase status
                Phase = "Running";
                //Set the phase detail status
                PhaseDetail = "Starting";            
                using (var session = new SessionScopeWrapper())
                {
                    //Set the phase detail status
                    PhaseDetail = string.Format("Composing Query {0}", _entityDisplayName);
                    
                    // Now we build our list of records
                    // I chose contacts starting with A for a lastname                
                    var contacts = session.QueryOver<IContact>()
                        .WhereRestrictionOn(c => c.LastName).IsLike("a%")
                        .List<IContact>();
    
                    //now work with my list of contacts
                    if (contacts != null)
                    {
                        //Set the main phase status
                        Phase = "Doing the work";
                        //Set the phase detail status
                        PhaseDetail = string.Format("Processing {1} {0} records", _entityDisplayName, contacts.Count);
                        //Write to my text file
                        Log(PhaseDetail);
    
                        //Initialize a counter
                        var counter = 0;
                        foreach (var contact in contacts)
                        {
                            //Here we can do a sample update like this:
                            contact.WorkPhone = "2128675309";
                            contact.Save();
    
                            // halt processing if interrupt requested by job server
                            if (Interrupted)
                            {
                                PhaseDetail = "Job was stopped by a user";
                                Log(PhaseDetail);
                                return;
                            }
                            // update job progress percentage
                            Progress = 100M * ++counter / contacts.Count;                        
                        }
                    }
                    else
                    {
                        // no records to process
                        //Set the phase detail status
                        PhaseDetail = string.Format("No qualifying records for {0}", _entityDisplayName);
                        //Write to my text file
                        Log(PhaseDetail);
                    }
                }
                //Set the main phase status
                Phase = "Complete";
                //Set the phase detail status
                PhaseDetail = "Complete";
                //Write to my text file
                Log("We are done!");            
            }
        }
    }
     
    
    

    Step 6
    Compile the assembly. Now take the compiled dll file and copy it into the Saleslogix Job Service portal, under the Support Files/bin folder. If you do this in Application Architect at this point you will need to close the AA and re-open it to ensure the job is available for the next step.

    Step 7
    Add the job to the list of jobs available in the job service portal.

    • In the AA, under portal manager, double click on the Saleslogix Job Service portal.
    • Under the Jobs tab, right click on the Jobs folder and choose Add.
    • To the right in the type name, do the drop down. The list will compile. You should now see your assembly name space.
    • Expand it out to your public class name (i.e. MyJobIsCool).
    • Save your changes to the portal.

    Step 8
    Deploy the SLXJobService portal. It is possible you might need to restart the SLX Job Server service. Not sure on that.

    You should now be able to log in to the web client, and under administration/job manager and be able to see your new job listed under the Definitions tab.

    Using Style to Manipulate Control Locations in the Infor CRM Web Client

    $
    0
    0

    The quickforms in Infor CRM are rather limited in their layout options. Everything is rendered into table rows and cells and sometimes those layouts are not what is needed. You could always make a custom smart part and design away, but for a simple tweak to the placement of a control you can use server side code on a quickform page load to manipulate a control’s style. This can affect the appearance of controls nicely. Sometimes it takes a bit of playing with an d some styles may exist with base styles in the CSS files. You just need to try out these types of changes to see if they work.

    To move a control on the screen to the left of where it normally is and down slightly:

    Control.Attributes.Add("Style", "position:relative;left:-65%;top:9px;");
    //or
    Control.Style["position"] = "relative";
    Control.Style["left"] = "-65%";
    Control.Style["top"] = "9px";
    

    To make a label stretch across the screen and set the background to red:

    Control.Attributes.Add("Style", "background-color:red; position:absolute;left:0;right:0;");
    

    Infor CRM SLXJobService Base Directory

    $
    0
    0

    I always forget what this default is supposed to be. here it is:

    %ALLUSERSPROFILE%\Sage\Scheduling\Tenants

    Infor CRM Merge Contact- Handling Account Product Contacts

    $
    0
    0

    When performing a merge of contacts in the Infor CRM web client it handles merging a lot of child records off the contact. One area that is not touched is the account product area where a contact is defined. if you are merging a contact associated to an account product, the merged contact remains associated to the account product rather than being updated to the contact it was merged with.

    This posts shows how to add this feature.

    The first thing to do is expand out the entity model in the Application Architect.

    Go to the Contact entity’s Rules.

    Double click the MergeContact rule.

    On the Post Execute task you can see all of the secondary steps to handle moving the child records.

    Go ahead and click add to add a new post-step
    Infor CRM Add new Post Step
    Name the post something unique. In my case I called it “MergeContact_FXMoveAssets” I didn’t follow the Infor naming convention in case they ever decide to add this step by default, having a duplicate step name will cause problems.
    InforCRM Post Step

    You will now see your step. Go ahead and click the Edit Code Snippet link.

    The first thing you will need to do is add an assembly reference. Unfortunately the assembly we need is not located anywhere convenient. It is called Sage.SalesLogix.Services.PotentialMatch.dll I copied the dll from the Bin folder of the web portal to my base build path’s assemblies sub-folder. Then I added an assembly reference in my custom step:
    Infor CRM add Assembly Reference

    Now with that assembly we can add the code that will use that service which is passed into our event to get the Source Contact Id. From there we use a SQL statement to update all of the account products that have that contact associated and then update them with the new contact, which is also passed in:

    			Sage.SalesLogix.Services.PotentialMatch.MergeProvider mp = mergeProvider as Sage.SalesLogix.Services.PotentialMatch.MergeProvider;			
    			string sourceContactId = mp.Source.EntityId;	
    			string targetContactId = mp.Target.EntityId;	
    			string dupId = string.Empty;			
    			
    			if(contact.Id.ToString()!=sourceContactId) dupId=sourceContactId;
    			if(contact.Id.ToString()!=targetContactId) dupId=targetContactId;
    			
    			Sage.Platform.Data.IDataService datasvc = Sage.Platform.Application.ApplicationContext.Current.Services.Get<Sage.Platform.Data.IDataService>();
    		    using (System.Data.OleDb.OleDbConnection conn = new System.Data.OleDb.OleDbConnection(datasvc.GetConnectionString()))
    		    {
    		        conn.Open();
    		        using (System.Data.OleDb.OleDbCommand cmd = new System.Data.OleDb.OleDbCommand("update accountproduct set contactid = '" + contact.Id.ToString() + "' where contactid='" + dupId + "'", conn))
    		        {
    		            int o = cmd.ExecuteNonQuery();						
    		        }
    		    }			
    
                result = true;
    

    Infor CRM Web Client – Running a Report from a Button and Allowing Users to Choose the Output Format

    $
    0
    0

    In previous posts I had written about how to use javascript and the client side Infor report service to invoke a report from a button.

    In this post I talked about the simplest way which just runs the report for the current entity and saves it as a PDF.

    In this post I talked about how using a different service call allows for custom record selection criteria to be added, rather than the single primary ID being passed in by the first link.

    Using that second option gives us another possibility, allowing the user to define what kind of output they want.

    To do this we can modify our call to the setReportJob and instead of calling showReport, we instead create a options object, passing in our parameters and then call startWizard, passing in our options object. In our custom options object we simply pass into the wizardOptions just to hide step 1, rather than the defaul of hiding step 1 and 3 (the output type).

    Here is a complete sample:

            var oReporting = Sage.Services.getService('ReportingService');
            if (oReporting) {
                var pluginId = oReporting.getReportId('Opportunity:Opportunity Detail');
                oReporting.setReportJob(pluginId, 'OPPORTUNITY', Sage.Utility.getCurrentEntityId());            
                var contextSvc = Sage.Services.getService('ClientEntityContext');
                var context = contextSvc.getContext();
                var entityId = context.EntityId;
                oReporting.reportJob.rsf = "{OPPORTUNITY.OPPORTUNITYID} = 'Q1RASA501HGU' or {OPPORTUNITY.DESCRIPTION} = 'Winner Winner Chicken Dinner' ";
                            
                var options = {
                    reportId: pluginId,
                    reportOptions: {
                        wizardOptions: {
                            hiddenSteps: [1]
                        },
                        scheduleOptions: {
                            executionType: 0
                        },                    
                        conditionOptions: {
                            conditionsConnector: 'And',
                            recordSelectionFormula: oReporting.reportJob.rsf
                        }
                    }
                };
                oReporting.startWizard(options);
    

    Resizing a Editable Grid in the Infor CRM Web Client

    $
    0
    0

    The web client’s editable grid does not resize well when you expand a tab that the grid may exist on. You end up with a lot of white space at the bottom of the grid where you could be showing data from the grid. Unfortunately the architecture of the tab workspace does not offer an easy way to automatically hook into the tab drag events to be able to resize a grid automatically. This post will give you a way to do it manually, by adding a toolbar button above the grid that the user can click to allow the grid to fill the available space that the tab occupies.

    Lets take a look at step by step how to do this.
    First open the quickform in the Application Architect.
    On the Toolbar above the grid, right click and choose Add Control Button…Button.
    Set the properties as follows:

    • Button Type- Icon
    • Caption- Resize
    • Image- Pick an appropriate image
    • ToolTip- Resize the Grid
    • On Client Click- return resizeGrid();

    Now on the form, choose Load Actions. Add a new load action, action of C# Snippet Action Item. Set the On Repaint Event to True.

    Now lets get into the code:
    The first bit of code we will use is this:

            string gridID = string.Empty;
            foreach (Control c in Controls)
            {
                var cont = c as ScriptResourceProvider;
                if (cont != null)
                {
                    gridID = cont.ID.Replace("_Strings","");
                    break;
                }
            }
    

    What we are doing here is getting the control added to the page as part of the editable grid deployment which is a ScriptResourceProvider. We do this because the ID of this control happens to be close to what we need for the rest of our code and is based on the tab name and grid control name. We strip of the “_Strings” part of the ID returned and set that as our gridID variable.

    The next part of code constructs a javascript function which will be injected onto the page. The function basically does 4 things:
    1 Gets the grid DOM element, it’s parent div (with an ID ending in _Container), and the grid header div (ID ending in _HeaderBar).
    2 Calculates the size available using the window.innerHeight and then subtracting the Y position of the header div, the height of the header div, and an extra 10 pixels.
    3 With the calculated height it then sets the height style attribute of both the grid and it’s parent div.
    4 Using dojo, gets the instance of the dojo grid control and then calls the grid’s resize and update methods which actually sizes the grid into the resized div controls.

    Here is the code:

            string jscript = @"function resizeGrid() {
            var gridid = '" + gridID + @"';
            var parentgrid = document.getElementById(gridid + '_Container');
            var griddiv = document.getElementById(gridid );       
            var headerdiv = document.getElementById(gridid + '_HeaderBar');
            var h = window.innerHeight;
            var w = headerdiv.clientHeight
            var t = headerdiv.getBoundingClientRect().top;    
            var newheight = h - t - w - 10;    
            griddiv.style.height = newheight + 'px';
            parentgrid.style.height = newheight + 'px';    
            var grid = dijit.byId(gridid);
            grid.resize();
            grid.update();
            return false;}";        
    

    Next we create another javascript string we will also inject on the page. This one uses the dojo ready event which will automatically run our resize code when the page loads.

            string jscript2 = @"var resizeTimeoutId;
                require(['dojo/ready'], function (ready) {
                ready(function () {
                    window.clearTimeout(resizeTimeoutId);
    	                resizeTimeoutId = window.setTimeout('resizeGrid();', 300);
                });
            });";
    

    Finally we inject our javascript onto the page:

            ScriptManager.RegisterStartupScript(Page, GetType(), "FXResizer", jscript, true);
            ScriptManager.RegisterStartupScript(Page, GetType(), "FXResizer2", jscript2, true);
    

    Now when our tab is opened it will automatically resize the grid to fit. Also we have allowed the user to manually call resize by the toolbar button that we added to the page, which will call the resizeGrid() function injected to the page. This allows for them to resize the grid after the do things client side, like dragging tabs around and things.

    A word of warning, this code will not work if the user drags the tab to the middle view. This assumes the tab is on the normal row of tabs along the bottom.

    Infor CRM 8.3 Upgrade – Issues with Proper Schema Creation in the Database

    $
    0
    0

    We recently ran into an issue when upgrading a customer to 8.3 that I wanted to discuss as it is a potentially big issue that could prevent an upgrade from working correctly, despite no warnings or error messages to alert you.

    One of the first steps to upgrading is to install the 8.3 VFS project and to check the “Rebuild schema from project” when you do the restore. This checkbox adds a step which uses the entity model to “back fill” any missing database components. For instance the 8.3 project backup adds a bunch of new fields to a lot of tables (See the Upgrading to Infor CRM v8.3.pdf, starting on page 61). In the upgrade, these new database elements are simply added as properties to the entity model. By checking the Build Schema checkbox what you are really telling the system is to use the entity model as the master schema definition, and generate any missing components into the database that exist in the entity model but not the database.

    This rebuild schema option is also available under the Tools menu in the Application Architect.

    When running the Rebuild Schema step a message box will show saying “Gathering Managed Entities”. Also, in the Output window you will get details listed of what is is doing.

    While there were no errors or warnings logged, there was an “info” message that revealed a problem:

    INFO  - ALTER TABLE ALTER COLUMN CITY failed because one or more objects access this column. : The object ‘V_Opportunities’ is dependent on column ‘CITY’. : The object ‘AKS_Opportunities’ is dependent on column ‘CITY’.

    The customer’s database we were upgrading had SQL views with Schema Binding enabled. Normally the schema update process is the following for tables:
    INFO – Inspecting schema for Address
    INFO – Checking Address for Schema changes
    INFO – Removing indexes for CITY
    INFO – Removing indexes for STATE
    INFO – Recreating Indexes for ADDRESS
    INFO – Schema changes applied for Address

    Because the SQL error was happening, it was not ever reaching the point where it altered the Address table. Therefore ADDRESS5, ADDRESS6, COUNTRYCODE and ENTITYTYPE were never added.

    By looking at the output window I found similar messages for other tables as well. The end result was that the new database schema elements that were supposed to be created in some tables were not.

    Hopefully Infor will address this and actually report this kind of stuff as errors during this rebuild schema routine so that errors are properly reported as such and occur when restoring the project with that checkbox on or when using the menu item.

    While this particular client had a problem with address fields being created, it could potentially be an issue with any of the tables in the system. Without an error being reported it is very likely people will have no idea a problem occurred or that their DB was not properly upgraded until later.

    So read those output window messages. Even output that doesn’t show as an error or warning can actually be telling you of a problem.

    Infor CRM Web Client – Using Style Attributes to Manipulate Control Appearance

    $
    0
    0

    With how the quick forms are constructed into asp.net user controls when the web client is deployed, it can be difficult to manipulate the appearance of controls since they exist within unnamed divs and table elements. There are some cases where you can implement custom style attributes to a control to make them appear in ways you may want. You can add this kind of code to the load action of the quick forms.

    For instance, if you have 2 controls side by side and you want to overlay one control on top of the other, you could do something like this:

    lblAgentNumber.Attributes.Add("Style", "position:relative;left:-65%;");

    Infor CRM Style 1

    To make a label stretch across the top of the screen and have a red background, you could do this:

    QFLabel.Attributes.Add("Style", "background-color:red; position:absolute;left:0;right:0;");

    Infor CRM Style 2

    To make a label box occupy more space instead of having such a large part of the screen taken up by the label:

    QFTextBox.Attributes.Add("Style", "position:relative;left:-30%; width:130%");

    Infor CRM Style 3

    Customizing the Name of the Infor CRM web client

    $
    0
    0
    A lot of customers have asked to have the web client modified from the generic Infor CRM branding to their own company branding. While a lot of this is fairly easy to accomplish by modifying files directly in the VFS, there is one particular file that is a bit hidden. Lets go through the files: […]

    Breaking change in 8.3 editable grid in the Infor CRM web client

    $
    0
    0
    If you have an editable grid with a Custom Format Function in a version before 8.3 the function signature looked like this: Now in 8.3, the format function needs to look like this: If you try to use the pre 8.3 code in an 8.3 system the editable grid will not display. Come on Infor.

    Breaking Change on the LeadSearchAndConvert.ascx.cs file in Infor CRM 8.3x

    $
    0
    0
    Breaking Campaign Target Logic In older version of the custom smart part LeadSearchAndConvert, there is code in the ConvertLeadToNewAccountAndContact method that converts lead campaign target data into the newly created contact record. In older version this code look like: In 8.3 that Lead business rule no longer exists. Instead there is a new method in […]

    Modifying the Edit Address Dialog in the Infor CRM Web Client

    $
    0
    0
    In the Infor CRM web client, we have gotten asked how to modify the address dialog that comes up when you click the pencil icon on the address control, like on the Account details screen. As usual things are a little more complicated than it seems. The Edit address screen is actually two different things […]

    Breaking Change in the OpportunitySnapshot.ascx.cs for Infor CRM 8.3x

    $
    0
    0
    Prior to 8.3, the ExchangeRate table in the Infor CRM database used the column CurrencyCode as the primary ID of the table. Because of this in the custom Opportunity Smart part OpportunitySnapshot, there was a method to get the exchange rate called GetExchangeRateData that used the GetById method to return the ExchangeRate record based on […]

    Errors installing Web Manifests in Infor CRM 8.3

    $
    0
    0
    I was recently working with a client who was having trouble installing the 8.3 Web Action items manifest (Infor v8.3 Action Items VFS.zip). Within this manifest are actions like create triggers or drop indexes. Whenever these Database Definition Language commands were attempted they would receive the dreaded and completely useless “Object Reference not Set to […]

    Infor CRM Web Error- The bundle could not be created because this manifest has invalid items.

    $
    0
    0
    There is a problem adding quick forms to web manifest if they contain an image resource reference that does not match case.

    Infor CRM Summing a Property of an Entity Collection Using Linq

    $
    0
    0
    A post showing how to use Linq to sum an entity collection quickly.
    Viewing all 168 articles
    Browse latest View live