Wednesday, May 26, 2010

eScript Framework - Query return Array

In my post introducing my eScript Framework, I glossed over the functional content of the functions I included so let me go into some more detail before proceeding. The last function in the declaration was called QueryExprArrayReturn. This function will execute a query against the business component whose string name is passed to the function using a complete expression also passed. The last parameter is an array of fields whose values will be returned, via an associative array.

QueryExprArrayReturn : function( sBO, sBC, sExpr, aFields) {
// Use : Frame.QueryExprArrayReturn (sBO : string nsme of Business
Object,
// sBC : string nsme of Business Component,
// sExpr : search expression to be applied,
// aFields : array of fields to be returned)
// Returns : string replacing fields and parameters from the lookup BC and/or property set
var aFnd, bFound, iRecord, sField;
var aValues = new Array();
var rePattern = /(?<=\[)[^[]*(?=\])/g;
with (TheApp.GetBusObject(sBO).GetBusComp(sBC)) {
while ((aFnd = rePattern.exec(sExpr)) != null) ActivateField(aFnd[0]);
for (var c=0; c < aFields.length; c++) ActivateField(aFields[c]);
ClearToQuery();
SetViewMode(AllView);
SetSearchExpr(sExpr);
ExecuteQuery(ForwardOnly);
if (FirstRecord()) {
iRecord = 0;
for (var i=0; i < aFields.length; i++) {
aValues[aFields[i]] = new Array();
aValues[aFields[i]][iRecord] = GetFieldValue(aFields[i]);
}
while (NextRecord()) {
iRecord++;
for (var i=0; i < aFields.length; i++) {
aValues[aFields[i]][iRecord] = GetFieldValue(aFields[i]);
}
}
}
return(aValues)
}


What is occurring here is pretty straightforward:
  1. Instantiate a BC using the passed BO/BC strings
  2. Activate any fields used in the Search Expression
  3. Activate the fields needing to be returned
  4. Query using the passed search expression
  5. If at least one record is found, create an associtive array of field values where the first index is the field name and the second index is the record number
  6. Continue populating this array for each record found

Probably the most interesting aspect of this query is the use of a regular expression search of the passed in expression to activate and BC fields present there by identifying them in enclosing square brackets [].

Monday, May 24, 2010

eScript Framework - Logging

The ABS Framework apparently has a logging module that Jason describes. This is interesting because I have been building my own logging technique over the past couple of years that largely parallels what I believe the framework does. Understanding object prototyping redirected my thoughts a bit and helped me centralize the effort. My previous post introduced the Frame and TheApp objects. This post will introduce a new object: Log. The following script will also be added to the Application declarations section.


Object.prototype.Log = function() {
return {
step : function ( text, Lvl ) {
if ((Lvl == null)(TheApp.GetProfileAttr("CurrentLogLevel") >= Lvl)) {
var fp = Clib.fopen(OutPutFileName, "a");
Clib.fputs(Frame.GetLocalTime()+" "+gsIndent + text + "\n", fp);
Clib.fclose(fp);
}
},
RaiseError : function ( e ) {
if(!defined(e.errText)) e.errText = e.toString();
var sFunction = gaFunctionStack.pop();

Log.step("".rPad(100, "*"));
Log.step("*** - ERROR - "+sFunction+" - "+e.errCode);
Log.step("*** "+e.errText.replace(/\n/g, "\n"+gsIndent+"".rPad(20," ")+"*** "));
Log.step("".rPad(100, "*")+"\n");
Log.step("---End Function "+sFunction+"\n");

var sLength = gaFunctionStack.length;
gsIndent = "".rPad(giIndent*sLength, ' ');

if (sLength>0) Log.step("<<-Returning to Function "+gaFunctionStack[sLength-1]+"\n"); throw(e); }, StartStack : function ( sType, sName, sMethod, Lvl ) { gaFunctionStack.push(sName+"."+sMethod); gsIndent = "".rPad(giIndent*gaFunctionStack.length, ' '); if (TheApp.GetProfileAttr("CurrentLogLevel") >= Lvl) {
Log.step(" ");
Log.step("".rPad(100, "-"));
Log.step("".rPad(100, "-"));
Log.step(sType+": "+sName);
Log.step("Method: "+sMethod+"\n");
}
},
Stack : function ( sFunction, Lvl ) {
gaFunctionStack.push(sFunction);
gsIndent = "".rPad(giIndent*gaFunctionStack.length, ' ');

if (TheApp.GetProfileAttr("CurrentLogLevel") >= Lvl) {
Log.step(" ");
Log.step(">".rPad(100, "-"));
Log.step("Function: "+sFunction+"\n");
}
},
Unstack : function ( sReturn, Lvl ) {
var sFunction = gaFunctionStack.pop();
if (TheApp.GetProfileAttr("CurrentLogLevel") >= Lvl) {
var sString = "";
if (sReturn != "") sString = " - Return: "+sReturn;
Log.step("---End Function "+sFunction+sString+"\n");
}
var sLength = gaFunctionStack.length;
gsIndent = "".rPad(giIndent*sLength, ' ');

if ((TheApp.GetProfileAttr("CurrentLogLevel") >= Lvl)&&(sLength>0))
Log.step("<<-Returning to Function "+gaFunctionStack[sLength-1]+"\n"); }, PropSet : function (Inputs, Lvl) { // Print out the contents of a property set. if (TheApp.GetProfileAttr("CurrentLogLevel") >= Lvl) {
PSDepth++; // Dive down a level
var InpChildCount, inprop, inpropval, inpropcnt;
var BlankLine = ' ';
var Indent = "".lPad(giIndent*PSDepth, " ") + ToString(PSDepth).lPad(2, "0") + ' ';

Log.step(BlankLine);
Log.step(Indent + '---- Starting a new property set ----');
InpChildCount = Inputs.GetChildCount();
Log.step(Indent + 'Value is ........ : "' + Inputs.GetValue() + '"');
Log.step(Indent + 'Type is ........ : "' + Inputs.GetType() + '"');
Log.step(Indent + 'Child count ..... : ' + ToString(InpChildCount));

var PropCounter = 0;
inprop = Inputs.GetFirstProperty();
while (inprop != "") { // Dump the properties of this property set
PropCounter++;
inpropval = Inputs.GetProperty(inprop);
Log.step(BlankLine);

var PropCountStr = ToString(PropCounter).lPad(2, "0");
Log.step(Indent+'Property '+PropCountStr+' name : <'+inprop + '>');
Log.step(Indent+'Property '+PropCountStr+' value : <'+inpropval + '>');
inprop = Inputs.GetNextProperty();
}

// Dump the children of this PropertySet
if (InpChildCount != 0) {
for (var ChildNumber = 0; ChildNumber < InpChildCount; ChildNumber++) {
Log.step(BlankLine);
Log.step(Indent + 'Child Property Set ' + ToNumber(ChildNumber + 1) + ' of ' + ToNumber(InpChildCount) + ' follows below.');
Log.step(Indent + 'This child is on level ' + ToNumber(PSDepth));

// Recursive call for children, grandchildren, etc.
Log.PropSet(Inputs.GetChild(ChildNumber), Lvl);
}
}
PSDepth--; // Pop up a level
}
}
}
}();
var OutPutFileName;
//Indent child prop sets this many spaces to the right for each level down.
var giIndent = 2;
var PSDepth = 0; // How deep in the property set tree, what level
var CurrentLogLevel = 2;
//used in debugStack function so store called functions
var gaFunctionStack = new Array();
var giStackIndex = 0; //Where in the function stack the current function resides
var gsIndent = ''; //used in debug methods to identify stack indents


In addition, I added the following to the Application Start event. The prerequisite for this is the new profile attribute I created in this post, and the creation of a new system parameter, 'Framework Log Path':


var sPath = Frame.GetSysPref("Framework Log Path");
sPath = sPath.replace(/\\$/, ""); //Remove trailing backslash if used
OutPutFileName = sPath+"\\Trace-"+
TheApp.LoginName()+"-"+
Frame.GetLocalTime("%02d%02d%d%02d%02d%02d")+".txt";
try {
Log.step("Log Application Start Event", 1);
}
catch(e) {
//default to OOTB Log File Location:
OutPutFileName = "Trace-"+TheApp.LoginName()+"-"+
Frame.GetLocalTime("%02d%02d%d%02d%02d%02d")+".txt";
Log.step("Invalid Preference - Framework Log Path: "+sPath, 0);
}
//Get the System Preference Log Level. Get the Log Level set for this user (if provided) and
//then set the log level for this session
var sLogLevel = Frame.GetSysPref("CurrentLogLevel");
if (TheApp.GetProfileAttr("User Log Level") != "")
TheApp.SetProfileAttr("CurrentLogLevel", TheApp.GetProfileAttr("User Log Level"));
else TheApp.SetProfileAttr("CurrentLogLevel", sLogLevel);
Log.step("Session Logging Level: "+TheApp.GetProfileAttr("CurrentLogLevel"), 1);


Here is an example of these functions in use in a PreInvokeMethod event of a BC:


function BusComp_PreInvokeMethod (MethodName) {
try {
Log.StartStack("Business Component", this.Name(), MethodName, 1);
var bReturn;
var sVar1 = "TEST"
switch(MethodName) {
case "TestMethod":
Log.step("Variable 1: "+sVar1 ,1);
TestMethod(sVar1 );
bReturn = CancelOperation;
break;
}

Log.Unstack(bReturn,0);
return(bReturn);
}
catch(e) {
Log.RaiseError(e);
}
}


And this is how it would be used in a method:


function TestMethod (sVar1) {
try {
Log.Stack("TestMethod", 1);
Log.step("sVar1: ".lPad(30,".")+sVar1+"\n", 2);
sVar1 += sVar1 + sVar1;
Log.Unstack("N", 2);
}
catch(e) {
Log.RaiseError(e);
}
}


The result of this provides an individual log file placed in the directory specified by the system parameter. Each method call is indented 2 spaces.

UPDATE: I am no HTML wizard but am learning. I updated tags to make code easier to read

The Framework

It has been a while but I want to return to the eScript Framework. I have already put up a couple of posts about some potential functions the Framework should include. So lets put a wrapper around this based on Jason's post. My thinking is that this code will all be placed in the Application declarations sections. I am still working on understanding the Object prototypes for modifying BCs which I hope will add significantly more functionality. Here is the script so far:


Object.prototype.TheApp = this;
Object.prototype.Frame = function() {
return {
AddToDate : function ( srcDate, iDays, iHrs, iMin, iSec, nSign ) {
//Use : Frame.AddToDate ( srcDate : Date Object
// iDays, iHrs, iMin, iSec : Integer Numbers
// nSign : 1 or -1 {1 to ADD to the srcDate
// -1 to SUBTRACT from the srcDate } )
//Returns : date object, after adding/subtracting iDays, iHrs, iMin and iSec to the srcDate
var retDate = srcDate;
retDate.setDate(retDate.getDate()+nSign*iDays);
retDate.setHours(retDate.getHours()+nSign*iHrs);
retDate.setMinutes(retDate.getMinutes()+nSign*iMin);
retDate.setSeconds(retDate.getSeconds()+nSign*iSec);
return(retDate);
},
DateToString : function (dDate) {
//Use: Frame.DateToString ( dDate : Date Object )
//Returns: A string with the format "mm/dd/yyyy" or "mm/dd/yyyy hh:mi:ss"
var sMon = ToString(dDate.getMonth()+1);
if (sMon.length==1) sMon = "0" + sMon;
var sDay = ToString(dDate.getDate());
if (sDay.length==1) sDay = "0" + sDay;
var sHrs = ToString(dDate.getHours());
if (sHrs.length==1) sHrs = "0" + sHrs;
var sMin = ToString(dDate.getMinutes());
if (sMin.length==1) sMin = "0" + sMin;
var sSec = ToString(dDate.getSeconds());
if (sSec.length==1) sSec = "0" + sSec;
if (sHrs == "00" && sMin == "00" && sSec == "00")
return(sMon+"/"+sDay+"/"+dDate.getFullYear());
else return(sMon+"/"+sDay+"/"+dDate.getFullYear()+" "+sHrs+":"+sMin+":"+sSec);
},
StringToDate : function ( sDate ) {
//Use: Frame.StringToDate(sDate: A string with format "mm/dd/yyyy" or "mm/dd/yyyy hh:mi:ss"
//Returns: a Date Object
var aDateTime = sDate.split(" ");
var sDate = aDateTime[0];
var aDate = sDate.split("/");
if (aDateTime.length==1)
return (new Date(ToNumber(aDate[2]),
ToNumber(aDate[0])-1,
ToNumber(aDate[1])));
else {
var ArTime = aDateTime[1];
var aTime = ArTime.split(":");
if (aTime[0]=="00" && aTime[1]=="00" && aTime[2]=="00")
return (new Date(ToNumber(aDate[2]),
ToNumber(aDate[0])-1,
ToNumber(aDate[1])));
else {
return (new Date(ToNumber(aDate[2]),
ToNumber(aDate[0])-1,
ToNumber(aDate[1]),
ToNumber(aTime[0]),
ToNumber(aTime[1]),
ToNumber(aTime[2])));
}
}
},
GetSysPref : function ( sPreference ) {
//Use: Frame.GetSysPref( sPreference: the preference name in the system preference view )
//Returns: the value in the system preference view for this preference name
var boPref = TheApp.GetBusObject("System Preferences");
var bcPref = boPref.GetBusComp("System Preferences");
with (bcPref) {
ClearToQuery();
SetSearchSpec("Name", sPreference);
ActivateField("Value");
ExecuteQuery(ForwardOnly);
if (FirstRecord()) return(GetFieldValue("Value"));
else return("");
}
},
SetSysPref : function ( sPreference, sValue ) {
//Use: Frame.SetSysPref( sPreference: the preference name in the system preference view,
// sValue: the value of the preference )
var boPref = TheApp.GetBusObject("System Preferences");
var bcPref = boPref.GetBusComp("System PreferencesUpd");

with (bcPref) {
ClearToQuery();
ActivateField("Value");
SetSearchSpec("Name", sPreference);
ExecuteQuery(ForwardOnly);

if (FirstRecord()) SetFieldValue("Value", sValue);
else {
NewRecord(NewBefore);
SetFieldValue("Name", sPreference);
SetFieldValue("Value", sValue);
WriteRecord();
}
}
},
DiffDays : function (date1, date2) {
// Use : Frame.DiffDays ( date1 : Starting Date object, date2 : Another Date object )
// Returns : Number of days between date1 and date2
return ((date2.getTime()-date1.getTime())/(1000*60*60*24));
},
GetLocalTime : function (sFormat) {
// Use : Frame.GetLocalTime ()
// Returns : string of the current timestamp
var dNow = new Date();
var sNow;
if (sFormat != null)
Clib.sprintf(sNow, sFormat, dNow.getMonth()+1, dNow.getDate(),
dNow.getFullYear(), dNow.getHours(), dNow.getMinutes(), dNow.getSeconds());
else Clib.sprintf(sNow, "%02d/%02d/%d %02d:%02d:%02d", dNow.getMonth()+1,
dNow.getDate(), dNow.getFullYear(), dNow.getHours(), dNow.getMinutes(),
dNow.getSeconds());
return (sNow);
},
QueryExprArrayReturn : function( sBO, sBC, sExpr, aFields) {
// Use : Frame.QueryExprArrayReturn (sBO : string nsme of Business Object,
// sBC : string nsme of Business Component,
// sExpr : search expression to be applied,
// aFields : array of fields to be returned)
// Returns : string replacing fields and parameters from the lookup BC and/or property set
var aFnd, bFound, iRecord, sField;
var aValues = new Array();
var rePattern = /(?<=\[)[^[]*(?=\])/g;
with (TheApp.GetBusObject(sBO).GetBusComp(sBC)) {
while ((aFnd = rePattern.exec(sExpr)) != null) ActivateField(aFnd[0]);
for (var c=0; c < aFields.length; c++) ActivateField(aFields[c]);
ClearToQuery();
SetViewMode(AllView);
SetSearchExpr(sExpr);
ExecuteQuery(ForwardOnly);

if (FirstRecord()) {
iRecord = 0;
for (var i=0; i %lt; afields.length; i++) {
aValues[aFields[i]] = new Array();
aValues[aFields[i]][iRecord] = GetFieldValue(aFields[i]);
}
while (NextRecord()) {
iRecord++;
for (var i=0; i < afields.length; i++)
aValues[aFields[i]][iRecord] = GetFieldValue(aFields[i]);
}
}
}
return(aValues)
}
}
}();


This is in addition to the string prototype modifications discussed in earlier posts.

Tuesday, May 4, 2010

Making an MVF Required

If you were to do a google search for this requirement you would probably find a lot of posts returned so I do not want to claim I am doing anything new. In fact, My Oracle Support even has a post about doing this declaratively and they claim this has been added to bookshelf for 8.x as the documentation defect is shown to be fixed but I cannot find the relevant page. (Search for Document ID ID 476071.1). Anyway I just wanted to put all this information in one place for my own reference and for anyone else that finds this useful.

First, the configuration:

The key thing here is that since the calculated field name has the same name as the MVF with just a space added, the message displayed to the user will be the same as well. Next, the appearance:

To make the red asterisk appear, add the following text to the Applet Control Caption - String Override whereever in the string you need the asterisk:

<img src="images/icon_req.gif" alt="Code Validated field" border="0" space="0" hspace="0">

Keep in mind Siebel automatically adds the colon (:) after the field label so that in Siebel 8.x, the red asterisk will appear before the colon while other required fields will have the asterisk appear after the colon. If you wish to be even more precise, and don't we all, you can remove the field label Siebel creates for you all together and create your own so you can place the colon where you wish. This is described in Document 852952.1 on My Oracle Support in a pretty superfluosly wordy way, so here is the excerpted nickel version:

  1. First remove the existing Field Label of HTML type Text.
  2. Drag a drop a Control of Type Label in the Grid Layout Applet web template
  3. Set the Caption for this Label Control. To show for example,

Last Name: *

Enter the following in the Caption - String Override:

Last Name:<img src="images/icon_req.gif" alt="Code Validated field" border="0" space="0" hspace="0">

Set the property HTML Type = Label


As for making the asterisk conditionally appear (for instance to support the BC level functionality enforced by the calculated field in the Required Field User Property), there is not really a way to do that, but a close approximation if you only have one is to use toggle applets which toggle on the same conditional value that the fields user property is based on.

Friday, April 30, 2010

Escript Framework - lTrim & rTrim

I want to make sure I am making regular contributions to our open source eScript framework function library, so today's theme will be to continue with some tools for the string object not included in the standard escript string library. Last time I talked about lPad and rPad, so let's continue with their inverse, lTrim and rTrim. These are pretty basic concepts, present in VB and SQL, but the idea is simply to remove all spaces from either the front or back of a string. Metalink actually provides a couple of approaches to this, on based on regular expressions from Siebel 7.5 and earlier (before the replace function),
String.prototype.lTrim = function() {
var aReturn = this.split(/^\s*/);
return(aReturn.join(""));
}
String.prototype.rTrim = function() {
var aReturn = this.split(/\s*$/);
return(aReturn.join(""));
}
And here is a looping approach:
String.prototype.rTrim = function() {
var iCount = 0;
var iLength = this.length;
while ((iCount<iLength)&&(this.substring(iLength-iCount-1, iLength-iCount)==" ")) iCount++;
return(this.substring(0, iLength - iCount));
}
String.prototype.lTrim = function() {
var iCount = 0;
var iLength = this.length;
while ((iCount<iLength)&&(this.substring(iCount, iCount+1)==" ")) iCount++;
return(this.substring(iCount, iLength));
}
But if you are using Siebel 7.5 or later, using the replace function with regular expressions that find end of string anchored and beginning of string anchored white space, respectively, should be the best approach:
String.prototype.rTrim = function() {
//Use: String.rTrim()
//Returns string trimmed of any space characters at the end of it
return(this.replace(/\s*$/, ""));
}
String.prototype.lTrim = function() {
//Use: String.lTrim()
//Returns string trimmed of any space characters at the beginning of it
return(this.replace(/^\s*/, ""));
}

Wednesday, April 28, 2010

Modifying the Personalization Profile

You may have found Profile Attributes to be a useful addition in Siebel 7 as a better way to use global variables. The most common use of profile attributes is exactly that. You use the script expression:
TheApplication().SetProfileAttr("CurrentLogLevel", 5);
This variable can then be referenced elsewhere in script:
var iLogLevel = TheApplication().GetProfileAttr("CurrentLogLevel");
Alternatively, you can get this value in a calculated field using just:
GetProfileAttr("CurrentLogLevel")
OK, so that is pretty elementary as it is documented pretty well in bookshelf. This assumes a relatively dynamic value of the global variable, one that is determined programatically during a user session, and whose value is lost when the session ends. But what if you want to reference a user attribute that persists. There are already a couple of specialized functions that reference some user attributes:
PositionName()
LoginName()
LoginId()
PositionId()
But what if you want to add your own? So here is my requirement. I want to store a custom logging level attribute that can be set on a user by user basis. I will go into more detail in future posts about what I might want to do with this value. So the first thing to do is to either find a place to put this value or to extend a table to make a place. I prefer the latter, so I am going to add a new number column, X_LOG_LEVEL to the S_USER table:
This field should then be exposed in the Employee BC (it could alternatively be exposed in the User BC if an eService or eSales application was in play but I am going to keep this simple for now). Create a new single value field to expose this column (I added some validation too):
Now, in order to actually use this value, I will need to expose it in the GUI. So lets put it in the Employee List applet so it will be visible in the Administration - User -> Employees view.
Once the list column exists, edit the web layout and add this control to the list in the Edit List mode. Next, I am going to expose this field in a very special BC, Personalization Profile. This BC is instantiated when the user logs in and its fields basically represent all the potential attributes of the logged in user including the User, Employee, Contact, Position, Division, and Organization. As I will show in a minute, its fields are referenceable as profile attributes. There is already a join in this BC to the S_USER table so just create a new SVF to expose this column.Finally lets add some script to the Application Start event for the application you are using. What I want to do here is set a global variable type profile attribute as I described in the beginning of this post called CurrentLogLevel to the value on the User record (which I get from the Personalization Profile) if one was set, otherwise to set it to a constant value. Then if the logging level is sufficiently high, start application tracing and push a line to that new file:
var sLogLevel = 3;
if (TheApplication().GetProfileAttr("User Log Level") != "")
TheApplication().SetProfileAttr("CurrentLogLevel",
TheApplication().GetProfileAttr("User Log Level"));
else TheApplication().SetProfileAttr("CurrentLogLevel", sLogLevel);
if (TheApplication().GetProfileAttr("CurrentLogLevel") > 4) {
TheApplication().TraceOn("Trace-"+TheApplication().LoginName()+".txt", "Allocation", "All");
TheApplication().Trace("Application Started");
}
Ok. Now compile everything. The base case is with no user log level set. When you open this application, the application start event will trigger and since I have not done anything yet with my user log level, it will default to 3 and no trace file will be created. You'll have to trust me so far. Next navigate to Administration - User -> Employees, and query for your login. Right click to show the columns, and move User Log Level to the Selected Columns list. Now set this field value to 5. Log out, and log back in. You will now find a file in you \BIN directory called Trace-SADMIN.txt (I obviously logged in as SADMIN but the filename is dynamic as well) with the line:
Application Started.
Done. I have used this for a log attribute I will talk about more in future posts but you can use this feature to store any type of data on a table linked to the logged in user. This is sometimes useful for referencing functional information relating to certain business processes. You could use an attribute from this BC in a calculated field on a different BC to determine whether a record on that BC should be read only for instance. Good luck

Tuesday, April 27, 2010

The Big Picture - Let the Customer Do the Work

This is another entry in what I hope to be a series of Big Picture posts that are more about CRM technology strategy than step by step config or code snippets. My last post kicket it off, discussing how automation is where the value is. I stand by that statement but I should atleast add one very important addendum. I drew a picture of a CRM implementation as starting a huge data model in which your client can begin collecting lots of detailed information about customers and their transactions. Transactions involve not just their leads and orders, but also the service requests, activities, and other custom task records which together create a profile of a customer's interactions with the client. Automation generally targets the transaction side of that data model. For instance, when a customer calls in to report a problem, an automation project could result in a series of service requests automatically generated and assigned to the right people at the right time with the right attributes set describing work that needs to be done to address that problem.


Another great way to "automate" though is to actually eliminate the layers between the customer and the data representing them. In other words, let customers manage their own data wherever possible. This makes the most sense in updating profile information like name and address corrections, but it can get far more complex depending on the nature of the client's business. The tools to implent this can housed within Siebel through eService, etc, or external (a third party or custom built portal application that integrates to Siebel). Either way, customer updates directly updates master data in the Siebel database (or wherever the customer master is stored).


I have been at clients that are afraid of this paradigm, afraid that user's will enter garbage data, but mainly afraid of losing control. While it makes some sense to build a light validation framework around these interfaces, the goal of the framework should be to make things easier for the customer, not to lock down data. At the end of the day, the customer owns their relationship with the client and it is not in their interest to purposefully corrupt this data. Plus anyone interested in doing so can just as easily do so over the phone. Allowing customer's to directly update as much of their profile as your client's business allows should reduce the service request volume handled by the client's support staff. At a minimum, the creation of service requests through the internet can smooth out call center operations reducing peak call times by rerouting some of those calls to the web, and reducing downtime by providing emails to respond to in the interim between call peaks.


This concept applies to Sales as well as Service organizations. The ability to actually complete orders over the internet though will be largely constrained by the complexity of the product being sold. Clients will have to weigh the incremental value of making some additional sales over the internet (a universe of prospective customers vs just those the in house sales staff can explicitly reach) vs the potentially reduced margins of a simplified product offering capable of being sold in this way.