Showing posts with label How To. Show all posts
Showing posts with label How To. Show all posts

Monday, September 12, 2011

eScript Framework on 8.1

Converting the eScript framework to 8.1 proved a bit troublesome for me as the Siebel strong type engine has apparently dropped support for prototyping Siebel objects, such as a Business Service.  This makes the implementation a bit less clean since without being able to declare a prototype of the Log or Frame objects on application start, we are left with having to have every framework function be a child of the Application object.  This being the case, I consolidated the Frame and Log objects from the 7.8 framework into a single Utility object since there was not as much advantage in separating them.  Instead of the elegant 7.8 calls:

Log.Stack("MyMethod",1);
Log.Step("Log the time as "+Frame.Timestamp("DateTimeMilli"),3);
Log.Vars("VariableName", varValue,3)
Log.Unstack("",1);

we instead do this:

TheApplication().logStack("Write",this)
TheApplication().Utility.logStep("Log the time as "+
    TheApplication().Utility.Timestamp("DateTimeMilli"));
TheApplication().Utility.logVars("VariableName", varValue)
TheApplication().Unstack("");

Oh well.  To mitigate this somewhat, I have added a number of enhancements since the initial series of posts, which I will try to discuss sometime soon.
  • Automatically tie log level to the function doing the logging (Stack/Unstack vs variables for instance), hence no need for the numeric last parameter to all logging functions (though it is still optional as an override)
  • Added support for unix file systems
  • Standardize the identification of logging record Ids (by passing the 'this' reference it will append the row id for methods with Write, Delete and Invoke in the name)
To implement the basic framework in 8.1, you need something like this in the Application Start event:
        this.Utility = TheApplication().GetService("ETAR Utilities");
        this.Utility.Init();

Here is the Declarations section:

var gsOutPutFileName;
var gsFileName;
var gsLogMode;
var giIndent = 2; //Indent child prop sets this many spaces to the right for each level down.
var giPSDepth = 0; // How deep in the property set tree, what level
var gaFunctionStack = new Array(); //used in debugStack function to store called functions
var giStackIndex = 0; //Where in the function stack the current function resides
var gsIndent = ''; //used in debug methods to identify stack indents
var giLogBuffer = 0;
var giLogLines = 0;
var gsLogPath = "";
var gsLogCache = "";
var gsLogSession = "";
var giErrorStack = 0;
var ge = new Object();
var gStack = new Object();
var gCurrentLogLvl;

The Utilities business service is a cached service in tools.  It's Init function looks like this:

giErrorStack = 0;
ExtendObjects();
gsLogMode = GetSysPref("Framework Log Mode");
gsLogMode = (gsLogMode == "" ? "FILE" : gsLogMode.toUpperCase());
gsLogSession = TimeStamp("DateTimeMilli");

if (TheApplication().GetProfileAttr("ETAR User Log Level") != "")
    gCurrentLogLvl = TheApplication().GetProfileAttr("ETAR User Log Level");
else gCurrentLogLvl = GetSysPref("CurrentLogLevel");
giLogBuffer = GetSysPref("Log Buffer");
gsLogPath = GetSysPref("Framework Log Path");
try {
     var os;
     os = Clib.getenv("OS");
} catch(e) { os = "UNIX Based"; }
try {
   gsFileName = "Trace-"+TheApplication().LoginName()+"-"+gsLogSession+".txt"
  //A Windows OS indicates a thick client. Assume the path is the dynamicly
  //determined Siebel_Home\Log directory, or ..\log
  if (os.substring(0, 7) == "Windows") {
//  gsLogPath = gsLogPath.replace(/\\$/, "");  //Remove trailing backslash if used
//  gsLogPath = gsLogPath.replace(/\x47/, "\\");  //switch invalid OS directory seperators
    gsLogPath = "..\\Log\\";
    gsOutPutFileName = gsLogPath+gsFileName;
  } else {
    gsLogPath = gsLogPath.replace(/\x47$/, "");  //Remove trailing backslash if used
    gsLogPath = gsLogPath.replace(/\\/, "/");  //switch invalid OS directory seperators
    gsLogPath = gsLogPath+"/";
    gsOutPutFileName = gsLogPath+gsFileName;
 }
} catch(e) {
  gsLogPath = "";
  gsOutPutFileName = gsFileName;
}

Wednesday, January 19, 2011

BI - Upload Limitation

I have recently been designated the BI technical resource on my project so am looking at the BI capabilities (on 7.8) for the first time. Despite a fairly complicated and mistake laden patch upgrade which I do not even want to get into, it is a pretty powerful tool, much better architected than Actuate. Anyway, there are also some pretty glaring limitations as well on how it is administered that require so little effort to fix, I decided to just go ahead and fix them.

My main beef is that the architecture requires your BI report developer to have access to both the BI file system and the Siebel Server file system. I suppose you could set this up in a way that minimizes security risk, but it just seems so unnecessary. Essentially, to upload a new BI Report Template, the developer creates a record in the BI Report Template administration view, attaches the two template files (an RTF and an XLF) and clicks the upload button. So far, so good. The problem is that these template files must also exist in a specific place in the Siebel Server file system as well to generate a report. But the code behind that button does not take the extra step to just copy the files to where they need to go. Also, there is an existing product defect where modifications to an existing report record require the developer to go into the BI File system and delete the entire directory containing that report template. So that is where I step in.

First I added two new System Parameters indicating the locations of the BI and Siebel home directories. There is a way to grab environment variables through script but I did not feel like investigating this so let's call that phase II. For example, here are my two:


NameValue
BIHomeDirectoryE:\OraHome
SiebelHomeDirectoryE:\sea78\siebsrvr


Then, we need to trap the call to upload the templates file. This call is performed from 'Report Template BC' by the 'Upload' method. We need to always delete the directory before this upload is called. We also want to delete the existing template file from the Siebel server file system. Here is a script to place in the PreInvoke method to accomplish that (there are also some references to the Log and Frame objects):

switch (MethodName) {
case "Upload":
try {
Log.StartStack("Business Component", this.Name()
+".PreInvoke", MethodName, 1);
this.WriteRecord();
var sReturn, sCommand;
var sSiebel = Frame.GetSysPref("SiebelHomeDirectory")
+"\\XMLP\\TEMPLATES";
var sPath = Frame.GetSysPref("BIHomeDirectory");
var sFile = this.GetFieldValue("ReportTmplFileName")
+"."+this.GetFieldValue("ReportTmplFileExt");

sPath = sPath
+"\\XMLP\\XMLP\\Reports\\SiebelCRMReports\\"
+this.GetFieldValue("Report Name");
Log.stepVars("BI Report Path", sPath, 3);

sCommand = 'rmdir "'+sPath+'" /S /Q';
sReturn = Clib.system(sCommand);
Log.stepVars("Command",sCommand,"Success?",sReturn,3);

sCommand = 'del "'+sSiebel+'\\'+sFile+'"';
sReturn = Clib.system(sCommand);
Log.stepVars("Command",sCommand,"Success?",sReturn,3);
} catch(e) {
Log.RaiseError(e);
} finally {
Log.Unstack("", 1);
}
break;
}
return (ContinueOperation);
Ok. That addresses the product defect for updates. Now the second part is to copy these template files to the Siebel server file system once the template files are uploaded. The following script can be added to the InvokeMethod event:

switch (MethodName) {
case "Upload":
try {
Log.StartStack("Business Component", this.Name()
+".Invoke", MethodName, 1);
var sReturn, sCommand;

var sSiebel = Frame.GetSysPref("SiebelHomeDirectory")+
"\\XMLP\\TEMPLATES";
var sPath = Frame.GetSysPref("BIHomeDirectory");
var sFile = this.GetFieldValue("ReportTmplFileName")
+"."+this.GetFieldValue("ReportTmplFileExt");

sPath = sPath
+"\\XMLP\\XMLP\\Reports\\SiebelCRMReports\\"
+this.GetFieldValue("Report Name");
Log.stepVars("Source Path",sPath,"Target Path",
sSiebel,"File to copy",sFile, 3);
sCommand = 'copy "'+sPath+'\\'+sFile+'" "'+sSiebel
+'\\'+sFile+'"';
sReturn = Clib.system(sCommand);
Log.stepVars("Command",sCommand,"Success?",sReturn,3);
} catch(e) {
Log.RaiseError(e);
} finally {
Log.Unstack("", 1);
}
break;
}

And there you go.

Friday, January 7, 2011

ADM - List Of Values

There are plenty of posts on support web discussing the issues with migrating LOVs, but for my own sanity, I thought I would summarize all of the relevant issues in one place.

First, we need to address the defects. These are documented on support in Document 731411.1 but I will summarize here:
(1) Go to BC 'List Of Values Child (UDA)'
(2) Add a new field 'Parent Type' based on Join 'Parent LOV' and Column 'TYPE' with Text Length '30'.
(3) Expand the pickmap for the 'Parent' field. Replace pickmap field 'Type' with 'Parent Type' and uncheck Constrain flg.
(4) Go to the integration object 'UDA List Of Values'
(5) Find the Integration component 'List Of Values Child (UDA)'
(6) Add a new field to the integration component with Name = 'Parent Type'. Data Type = 'DTYPE_TEXT', Length = '30', Type = 'Data', External Name = 'Parent Type', External Data Type = 'DTYPE_TEXT', External Length = '30', External Sequence = '38', XML Tag = 'ParentType'
(8) Compile changes.
The SR then goes into some more detail on why after all that it still does not quite work. To understand, we need to see that the LOV ADM Integration Object is Hierarchical in one dimension. That is, there is the LOV_TYPE record and then there are the value records. But LOVs are frequently Hierarchical in two dimensions, by virtue of the Parent value. What I mean is that a given LOV value record will always have one 'parent' record, it's type or technical parent, and may have a second parent record, it's functional parent, if you will.

ADM loads the first, technical parent in the standard way, through the relationships of the Integration Object. To load the functional parent though, ADM must run in two passes, the first to create all the parent and child records, and the second to relate them. This is necessary because we cannot guarantee the sequence with which LOV value records will be placed in the extract file. If these value records do not exist in the target already, and the parent is alphabetically (or however else we chose to sort the records) after the child, then it would error if ADM did not take this approach. So how ADM takes two passes is by virtue of the ADM Data Type explorer. You will notice that the explorer does not actually specify the foreign key fields of an object to link them to each other. Its only purpose is to run ADM in multiple passes. But the twist is that ADM will actually process dependent data types setup in the explorer in reverse order, importing the children before the parent. I personally find this confusing from a terminology perspective. Perhaps a better way of naming these Data Types is to use 'LOV-2ndPass' instead of 'LOV-HierParent' and 'LOV-1stPass' instead of 'LOV-HierChild'. This way when we set up the search specifications for an ADM Export session, it is clear what we are trying to do.

OK, one more wrinkle to throw into the mix (just when you thought it was all making sense). There is actually a third parent relationship involved. That is the records that populate the S_LOV_REL table. I will be honest; I do not use the LOV explorer view that often and I don't really know what the point of this table is. In theory it can make LOVs M:M but I just don't think this is practical. Nevertheless, there are some vanilla uses of LOVs where these records are in fact used that way. The one that comes to mind is in payments, where the PAYMENT_TYPE_CODE values are children of the PAYMENT_METHOD_CODE and there are S_LOV_REL records created to store the relationships. The same issue applies when migrating these relationships. The related value must exist prior to the relationship being built.

One final note. I think the whole not deleting LOVs is well intended but more likely to cause confusion than solve anything. Here is why. Users can and will just change the Name/Value of a value record to something else in which case any sense of history is lost anyway. There are no foreign key relationships to LOVs so business data using these values is unaffected regardless. But others may disagree so this step is completely optional. I remove the no delete properties from the 'List Of Values Child (UDA)' BC and Integration Component. (I also allow deletes from the GUI but that is a separate issue). So my migration methodology is to synchronize values between environments for an initial release. You would take a different approach on a point release where values are likely to have been added directly to production and therefore may not exist in your DEV and TEST environments.

Anyway, what are we trying to do. Quite simply, we are trying to create all the Value records in pass 1, then we need to relate them to each other in Pass 2. I have already discussed how to group LOVs together for a release. This is where I diverge from Siebel's example because I am trying to think of real life scenarios where I am deploying releases, not just one LOV_TYPE. When creating the ADM Project/Session, here are the session items I use:


Data TypeChild DeleteDeployment Filter
LOV-2ndPassY[Release] = '1.1'
LOV-1stPassN[Release] = '1.1' AND [List Of Values Relationship.Name] IS NULL

What this means is that the first pass includes all LOV_TYPE records that have been marked for this release and all LOV value records related to them. The second part of the expression basically just insures that no relationship records are included in the first pass. When ADM attempts to set the parent value on a child, it may not be able to find it so it will log a warning and move on. In the second pass, ADM will load all the Relationship records and set the Parent values that it missed on the first pass. I have also set child delete to true on the second pass so that this job effectively synchronizes the value records for the type records marked.

Monday, November 15, 2010

A Basic Interface - Integration Object User Props

I know this is meant to be a basic interface with so complexity, but let's be realistic about the requirements we are likely to get. Even a simple upsert of something as basic as a service request is likely to require a bit of digging into bookshelf so that the interface is able to mimic basic GUI functionality. I will discuss some of the most commonly used User Properties necessary to implement even an advanced interface. When in doubt about the syntax of any of these properties, take a look for an example in the Tools flat view.

PICKLIST

This is the most common Integration Component Field User Property you will see and it basically tells the EAI Siebel Adapter to validate the picklist in the interface. This property is generally created by the wizard so I bring it up only because validating the picklist here will allow for several different ways to interpret a picklist field value described by some of the user properties below.

PicklistUserKeys

In the GUI, when you type an account name in the Account field on another BC that has a picklist of Accounts, and there is more than one record matching that name (with different locations), a pick applet will pop open with the constrained list of accounts having that name. The GUI is letting a user decide which of the multiple records returned was meant to be picked. An interface does not have that luxury, so the PicklistUserKeys Integration Component Field User Property is provided to mimic this action. The value of this property should be a comma separated list of fields representing the logical key of the picklist record to look up. These fields must all be present in the integration component (though there values can be null). The 'PICKLIST' user property must also exist for the field where this property is used and its value must be 'Y'.

Ignore Bounded PickList

When a picklist is validated in the interface and the value passed is not found, the EAI Siebel Adapter stops processing and returns an error. If the data is expected to sometimes be missing though, you may want the foreign key to just be left blank. For instance, maybe the service request, in our example is tied to an order via a back office order number, but the order was never loaded. Add this user property with a value of 'Y' in combination with the PICKLIST user property with a value of 'Y'. The EAI Siebel Adapter will look up the record by the user key provided (can also be used in combination with PickListUserKeys) but if it is not found, will set the field to blank in the integration object before applying the data. Keep in mind that this property will only work as expected if the Picklist object the underlying BC uses to constrain the field is set to No Insert equals True, otherwise, the EAI Siebel Adapter will try to insert a record. Also note that in bookshelf there is a typo in that there should be spaces between the words of the property name.

FieldDependency

It is easy in the GUI to determine the order of the fields being picked, either by training or by sequencing the fields in a particular way during applet design. This may help set the fields that will be used to constrain the value of another field, frequently in a hierarchical picklist. In EAI, we achieve this result through this user property. It can be used multiple times with a sequence number, just like other BC and applet user properties. The value is a field integration component field name. Siebel claims that pickmapped constraints are automatically taken into account, and that may typically be the case, but I have seen times when it does not work, so this is a good fall back.

Friday, October 29, 2010

A Basic Interface - Web Service Workflow

Just about every interface consists of two basic components: the integration object(s) and the workflow or business service. I will demonstrate a workflow approach which will give you more opportunity to customize down the road.

It is here that we begin to differentiate the integration by the communication mechanism. Because I am designating this integration as a Web Service, that will drive the type of data this workflow will expect as an input and output. The workflow I build will eventually be exposed as a WSDL to be consumed by an external program. That WSDL should have the definition of the message it is expecting, in this case, the XSD, or definition of the Integration Object we just built. How we accomplish this is to set the Input Process Property to a Data Type of 'Integration Object' and to actually specify the integration object we built, in the Integration Object attribute of the process property.


You can also see my place holder for the SR Number that I want to return to the external system in the response message. The 'IncomingXML' property is already in the format needed to be passed to the EAI Siebel Adapter, so there is no conversion necessary. And we are assuming that the data being passed is exactly as it should be applied. You will create the following steps which I will explain (other than Start and End which are self explanatory):
The 'Upsert SR' is a Business Service calling 'EAI Siebel Adapter'. Now here is the another design decision to be made. Each of the available methods differentiate exactly how the data should be applied. But there are two broad determinations. If we were to use the Execute method, then the 'operation' element which exists in each component of the IO would be used to determine how the data should be applied. This gives more control the calling system (or a data map which I will discuss later). The other set of methods essentially comprise a One Size Fit All to applying all the data uniformly. I will use the latter approach here and set the method to 'Upsert'. There is only one component in my IO, so if it exists, it will be updated, otherwise it will be inserted. The input arguments for this step are the IncomingXML message from the external system and a parameter telling the EAI Siebel Adapter to create the Status Object.

There is one Output Argument. We no longer care about the input message at this point because it will have been applied so we just overwrite it with the return, which in this case will be the status key.
The last step in the WF is another Business Service step calling 'PRM ANI Utility Service', 'GetProperty' method. This business service has a plethora of useful methods for manipulating property sets. This particular method will extract the value of a field from an integration object instance. Here are the inputs:
The output is to set the process property 'SRNumber' with the Output Argument, 'Property Value'. When the return message is sent back to the calling system, this property will exist with the generated SR Number.

Simulating/Troubleshooting this WF from within tools is difficult as built so I sometimes add a bypass step off the start branch to read the integration object from a file. I may talk about this later but want to keep this post pretty straightforward. So for now, this workflow can just be deployed, checked in and activated.

A Basic Interface - Building the Integration Object

I am not sure how easy it will be to summarize EAI in a couple of blog posts as there are definately a lot of ifs and buts in the design process. Nevertheless, I think it wold be useful to show how to build a basic interface using a couple of different techniques. Frequently you client's enterprise architecture will drive which to use.

Integration generally takes one of three forms
  • Query - Returns a data set of source data to be displayed in the target system
  • Schema Update - Takes a hierarchical data structure and applies it to the target system
  • Functional Action - Triggers a service to perform some set of business rules
There is perhaps some overlap here and any of these can be inbound or outbound to siebel, but this is a general way of categorizing your interfaces. And within each there are several different ways to implement more specific requirements.

Regardless of approach, the basic component of most interfaces is the structure of how data is viewed or applied. Let's say we need to Upsert a Service Request. A Schema Update assumes a hierarchical organization of data using the Integration Object data structure. Bookshelf provides extensive instruction on how these are built and configured to achieve certain goals so I will only touch on the highlights.

First, create an Integration Object in Siebel: from the Tools File Menu, New Object Wizard, EAI Tab, choose Integration Object. In the wizard, select the Project and choose 'EAI Siebel Wizard' from the second dropdown, and click Next. For the purpose of this example, we can just use the Service Request business object as the source object and the root BC will be Service Request. Enter a name of your choosing and click Next. In the next wizard page, deselect all child objects for which there are no fields to set. In this case that will be all of them except for the root as the more objects and fields in the message, the longer it will take the various architecture components to parse and translate the message. Click Next, then Finish on the next page.

Your Integration Object has been created. The next step is to verify the user keys. An integration object needs to have a valid user key in order to do an upsert. This basically specifies which key fields to use to find a record to update. In my example for Service Request, a key was not generated by the wizard so I will create one. Navigate to Integration Component Key in the explorer under the Service Request Integration Component. Create a new record, provide a name, set the sequence number to 1 and the key type to 'User key'. Create a child record in Integration Component Key Fields, provide a name and set the Field Name to 'Id'.

Another optional step we will use in this example is the Status Key. After creating a service request, I want to return the service request number to the external system as verification of success and so this SR can be referenced later by the customer. To do this we use the Status Key. This is basically a structure of the data set we wish to return from the EAI Siebel Adapter call and pass back to the calling system. A Status Key can be specified for each Integration Component so the final data set is the structure of all the keys combined hierarchically. In this case, navigate to Integration Component Key in the explorer under the Service Request Integration Component, create another new record, provide a name, 'StatusKey, set the sequence number to 1 and the key type to 'Status key'. Create a child record in Integration Component Key Fields, provide a name and set the Field Name to 'SR Number'.

Finally, while not absolutely necessary, you should inactivate all fields you are not using for each Integration Component. For an inbound upsert to Siebel, the calling system does not need to provide all the fields that are active in the IO schema, but if the field is active in the IO, then the external system Could send that data element which may have undesired affects depending on the interface. Make sure all fields used in the key are activated as well as all fields being passed from the external system. Unlike BC field lengths, the length property of an Integration Component Field is more important as when an XSD is generated and provided to the external system, this property will frequently be used by the web development tool to validate the data entered into that field. You can also change the XML Tag attribute to a label recognized by the external system (so long as spaces are removed).

One thing to keep in mind is that if an insert is desired, then the calling system should just pass a constant to the user key field, 'Id' so that Siebel will not find a record and a new one will be created. A value like '_New_Record_' is a safe value because the '_' will never be part of a generated row id.

Friday, July 23, 2010

My Barcode Promised Land

The effort of trial and error, traversing dead ends, and determining what I could not do, led me eventually to what I could. Let me start by saying that if I was a Siebel engineer (completely unaware of what constraints they had to work with) I would have provided an Application level method called something like BarcodeScan that could be trapped. I could then put a runtime event on it and trigger a wokflow when I was done. But then again I also would not have coded in the limitations I mentioned earlier.

Barring all that, I still needed a couple of basic things:
  • Hook to trigger additional functional logic
  • Do lookups on Serial Numbers
Additionally, it would be nice to:
  • Minimize the number of clicks
  • Do lookups on the child record of a BC
  • Parse the input so that I could do different stuff based on the type of data
Given those must-haves and nice-to-haves, I decided to hack the business service, trap the methods in question and just do my own thing. I should mention, that my initial approach was more from a wrapper perspective than a replace perspective. That is, I thought I could just trap the method, do my stuff, then continue with the vanilla method. Here is the problem though. Since everything that happens in the vanilla method threads occurs out of the GUI context, I cannot leverage any Active... methods. Therefore to do something as simple as update the record returned by the vanilla lookup, I would have to requery for it in my own objects to get it in focus to update it. Well if I am requerying for it, what is the point of doing the same query twice? I can just do my own query once in the Active object and then trigger my post events.

Let me start by walking through the most important Must-Have

Hook to trigger additional functional logic
I have sort of hinted at how this was achieved in general. Once I realized that the 'HTML FS Barcoding Tool Bar' was getting called, I modified the server script on this service to log when its methods are called. The important method here is 'ProcessData' which is the one method called regardless of the processing mode in use. At this point you have the barcode data and the Entry mode. You can also determine what view you are on via ActiveViewName. I trapped the Find, New and Update methods in the PreInvokeMethod event to store the current processing mode in profile attribute:
switch (MethodName) {
case "Find":
case "New":
case "Update":
TheApp.SetProfileAttr("BarcodeProcessMode", MethodName);
break;
}
With these three fields, the View, Process Mode, and Entry Mode, I can query the FS Barcode Mappings BC for a unique record.

boBCMappings = TheApp.GetBusObject("FS Barcode Mappings");
bcBCMappings = boBCMappings.GetBusComp("FS Barcode Mappings");
with (bcBCMappings) {
ClearToQuery();
SetViewMode(AllView);
ActivateField("Field");
ActivateField("Applet BC");
SetSearchSpec("View", sView);
SetSearchSpec("Entry Mode", sEntryMode);
SetSearchSpec("Process Mode", sProcessMode);
ExecuteQuery(ForwardOnly);
bFound = FirstRecord();

if (bFound) {
...
What I want to get from that record for now is the lookup field. I also need to know the Active BC to do the lookup in. Again, I cannot use ActiveBusComp or ActiveApplet so I just added a join to the FS Barcode Mappings BC to the repository S_APPLET table based on the applet name already stored in the Admin BC and added a joined field based on S_APPLET.BUSCOMP_NAME. I still feel like there is a better way to do it, but that is where I am at right now. Anyway, from the admin record I have a BC to instantiate, a field to set a search spec on, and the text value of the search spec.
sField = GetFieldValue("Field");
sBusComp = GetFieldValue("Applet BC");

boObject = TheApp.ActiveBusObject();
bcObject = boObject.GetBusComp(sBusComp);
with (bcObject) {
ClearToQuery();
SetViewMode(AllView);
ActivateField(sField);
SetSearchSpec(sField, sLogicalKey);
ExecuteQuery(ForwardOnly);
bFound = FirstRecord();

if (bFound) {
...
My client has multiple barcode processes so all this could be happening in different places. So the last step is to add some logic to branch out my hook. I am using the BC for now but we could make this more robust:
switch (sBusComp) {
case "Service Request":
ProcessSR();
break;

case "Asset Mgmt - Asset":
ProcessAsset();
break;
}

Thursday, July 22, 2010

Spelunking in the Barcode Cavern

My new client would like to use a Barcode scanner for a whole variety of Field Service applications:
Shipping Label to Lookup and RMA Order and update some fields
Asset Label to Lookup or Create an RMA Order Line Items and update some fields
Asset Label to Lookup a Repair record and update some fields

Siebel Bookshelf and Supported Platforms provides some basic information. There are a couple of approaches to using a Barcode scanner
  • Treat it like any data entry device. In other words, you prepare your record (click new, Clear to Query, etc.), click into a field, scan your barcode, the scanner copies the translated barcode value to the field, then you do what you want (save the record, execute query, etc).
  • Use the Barcode ToolBar. This has some basic modes (New, Update, Find), an administration area that ties a View to one or more modes and a field. So when you navigate to a view, Siebel (when the barcode toolbar is turned on through an object manager parameter), checks to see of any barcode admin records exist for that view and the currently selected mode. If so these appear in a dropdown in the toolbar that a user can select a value from. If the User then scans something, the Application "Processes" the barcode depending on the mode, either doing a query based on a specified field, updating a field on the current record, or creating a new record and populating a specified field.
This sounds groovy until you hear about some of the limitations and start thinking about a more realistic process. So here are the limitations as I understand them:
  • Only some Barcode Types (think fonts) are supported.
  • The processing can only occur in the primary BC of the BO, or the Parent BC in a master detail view.
  • Serial Numbers cannot be looked up (I am still investigating why this is but I am guessing it has to do with them possibly not being unique).
  • Only barcode scanners that support using customizable control character before and after the scanned input will work
  • A single input value is taken (so no splitting of a concatenated value)
  • You basically have to tell the toolbar what value to expect (again, no intelligent parsing)
Prototyping:
  • Insure you have the Field Service, and Barcode license keys
  • In the Field Service cfg file (if using a thick client), set the ShowBarcodeToolbar parameter to TRUE. Intuitively enough, this will make the Barcode toolbar appear in your app upon restart.
  • Click the enable button (far right hand button) on the toolbar
  • As you navigate to a view, the application will perform a query of the 'FS Barcode Mappings' BC, or S_BC_ENTRY_TRGT table for admin records corresponding to the current view and the currently selected processing mode (the three buttons to the left of the dropdown in the toolbar each correspond to a different mode). If you think about it, this is sort of similar to how Actuate reports are tied, except you can actually administer this a bit in the GUI.
  • We can mimic a barcode scan by using <ctrl-\>, followed by the translated value we are trying to scan (SR number for instance), followed by another <ctrl-\>
  • If you want to use different control character than <ctrl-\> (because maybe that one is already taken or something), these are set on the 'HTML FS Barcoding Tool Bar' business service as User Properties. I will leave them be.
So in my real life example, I will:
  1. Navigate to All Service Requests
  2. Click Enable on toolbar
  3. Click the right most left side button on the toolbar, 'Find'
  4. Leave the dropdown as 'Serial Number'
  5. Hit <ctrl-\>
  6. Type in an SR # I can see in the list
  7. Hit <ctrl-\> again
  8. The Application should query for the SR # I entered
I am now going to dive figuring out a better way to customize this behavior. I'll be back.

Wednesday, July 21, 2010

About defaults, picks, maps and SetField events

That is an eclectic list of things in the title, and no I do not intend to talk about them all in detail other than to discuss a bit about how they interact and some of the design implications they may cause. So let me start with another list:
  • Pick Maps do not cascade
  • Fields set by a pick map cause a SetFieldValue event
  • Defaults do not cause a SetFieldValue event
  • On Field Update Set BC User Prop will trigger a SetFieldValue event
  • SetFieldValue event triggers a Pick
  • Setting a field to itself does not trigger a SetFieldValue event
So those are the important findings I had to deal with when implementing a seemingly simple requirement. My client had a contact type and sub type. The contact type should be denormalized from the related account's type. Finally, they want to set the contact sub type dynamically to a different value depending on the contact type. By dynamically, I mean not hard coded, so it can be changed without a release.

Let me put all that in functional terms by providing an example. The Account Type has a static LOV with values 'Bank' and 'Government'. The Contact can potentially be created as a child of an account, inheriting information from the account record, and triggering Parent expression default values, or can be created from the Contact screen without an account, but with the option to set the account later. When an account is specified for a contact, the contact type will be set to match the account type, otherwise the contact type should be set to 'Other'. If the Contact type is 'Bank', the contact sub type should get set to 'Retail', and if the contact type is 'Government', the sub type should be set to 'HUD'. So the basic configuration we started with was to put the desired 'dynamic' sub type value in the Low column on the LOV table. Then set up the pick map for contact type as such:

Field Picklist Field
Contact Type Value
Contact Sub Type Low

It would be convenient to just set the pick map similarly on Account Type as:

Field Picklist Field
Account Type Value
Contact Type Value

But the first rule above states this will not work because pick maps do not cascade. This makes some sense as you could conceivably end up with some circular logic. Or in the case where the contact is created as a child of an account, to predefault the Contact Type to the Account Type. But again, according to the rules above, a predefault will not trigger a SetField and hence no pick map.

So in order to trigger the pick map on Contact Type, we need to trigger a SetFieldValue event on this field. What to do. Oh, and I did not want to use script. My solution had a couple of dimensions.
  1. When a contact is created on the Contact Screen and the account is picked, I am going to trigger a set field value on the Account Type by creating a joined field on the Contact BC called Account Type, and add this field to the Account pick map. So this will trigger my SetFieldValue event. I then will add an 'On Field Update Set' BC User property to the Contact BC so that when the joined Account Type field is updated, set the Contact Type to the Account Type. Using a User Property will then trigger the SetFieldValue event on Contact Type which will then trigger the pick map to set the Contact Sub Type. So far so good.
  2. My approach on the scenario when a Contact is created as a child of an Account is not as clean. The problem here is that Predefaults do not trigger SetFieldValue events. And in this case, all the account information will already have been set via Predefault so there is no field being explicitly set by a user to trigger the User property. So I had to get creative. What I did was similar to above but placed identical user properties on Contact First and Last name fields. Since these are required fields that are typically entered first, they will trigger the user properties to set the contact type and sub type. In order to minimize the UI impacts of this admittedly kloogy design, I wanted the visible Contact Type in the applet to default correctly to the Account Type from the parent record. This means that when the User sets the First Name (or the Last) the Contact Type will already have the correct value so the User Property would essentially set it to itself. The last rule above states this will not trigger the SetFieldValue event. To get around this I create two User Properties in sequence, the first to set the Contact Type to null, and the second to set it back to the Account Type. Because I am putting the properties on both the First and Last name (to accommodate different user's field population sequences), I also want to add a conditional to the user properties to not execute if the Sub Type has already been set.
What does all this leave us with? In addition to the pick map on the Account field mentioned first, here are the On Field Update Set user properties on the Contact BC:
  1. "Account Type", "Contact Type", "[Account Type]"
  2. "First Name", "Contact Type", "", "[Contact Sub Type] IS NULL"
  3. "First Name", "Contact Type", "[Account Type]", "[Contact Sub Type] IS NULL"
  4. "Last Name", "Contact Type", "", "[Contact Sub Type] IS NULL"
  5. "Last Name", "Contact Type", "[Account Type]", "[Contact Sub Type] IS NULL"
I am going to leave it there, but this actually gets even more complicated. Because a contact can be created from a pick applet from a service request, I also had to account for predefaulting the account to the SR's account and this impact this would have on predefaulting Contact Type and Sub Type. If anyone would like to see how this is done, here is where to start.

Tuesday, June 29, 2010

eScript Framework - Logging Variables

Here is another entry into the logging framework previously discussed. The idea behind this function is to Log multiple variable values to separate lines of our log file using a single line of script. This keeps our script pristine and makes the log file well organized and easy to read. The script to call the function is as follows:

Log.stepVars("Record Found", bFound, " Account Id", sId);
The expected arguments are name/value pairs where the name is a descriptive string (could just be the name of the variable) and the value is the variable itself that we want to track. There is no limit to the number of pairs. There is an optional last parameter to indicate the enterprise logging level (stored in system parameters) above which this line should be written.

The results will be written to the log file as:

06/29/2010 13:33:53 ................Record Found: Y
06/29/2010 13:33:53 ..................Account Id: 1-ADR45
The script to implement follows. This is meant to be added as a new method in the 'eScript Log Framework' business service.

function stepVars () {
var Args = arguments.length;
var iLvl = (Args % 2 == 0 ? 0 : arguments[Args - 1]);
var iParams = (Args % 2 == 0 ? Args : Args - 1;
var sProp, sValue;

for (var i=0; i < iParams; i++) {
sProp = arguments[i++]+": ";
sValue = arguments[i];
Log.step(sProp.lPad(30, ".")+sValue, iLvl);
}
}
Also, a new line will need to be added to the Init section to instantiate this function on Application start:

Log.prototype.stepVars = stepVars;
I want to draw particular attention to two java features which may be useful in other applications. The first is how to reference a variable number of arguments in a function call. Notice the special array variable, 'arguments'. This array is defined as all of the arguments passed to this function with no special declarations. It can be referenced just like any other array. There are some exceptions with how this array can be manipulated though with push() and pop() not working as you might expect.

The second, is how to assign a variable using an inline if, ( condition ? value if true : value if false). The condition is any expression that will evaluate to either true or false. The first expression after the ? is the value returned if the condition evaluates to true, and the last expression is what is returned if the consition evaluates to false.

Friday, May 28, 2010

Framework Logging Performance

I have been working with my eScript logging framework for a couple of months now and it has been extremely helpful with debugging complicated script procedures. What keeps it from being truly useful in a production environment though is it is too damn slow. In an ideal world, I could leave logging on at all times, or at least for long periods of times for users or groups I want to keep track of. In order to do this, I need to make sure they do not notice a significant degradation in their work day.

The way the logging object functions work in the framework is basically to send one line at a time to the step function which then opens a file, writes the content along with a timestamp, and closes the file. It is these file operations that take the great majority of performance time. A given complicated script takes about 7 seconds with logging turned up to 5 which outputs a 63kb log file on my thick client. While I could expect server performance to be a bit faster, just turning logging off, reduces the performance time to under 2 seconds. What to do...

Buffer the output. It strikes me that I will do the exact same thing Siebel does with its own log files. Ever notice that the SQL Spool file or server log files are not always completely up to date with what you are executing in the GUI? This is because the application keeps a buffer of output and only writes the buffer when it is full. So I will do the same thing. I will store a new system parameter called 'Log Buffer' which will equal the number of lines to buffer. I will then create some new global variables to keep a running line count and one to buffer the output.


var giLogBuffer = Frame.GetSysPref("Log Buffer");
var giLogLines = 0;
var giLogCache = "";


All I have to do is modify my step function to leverage these values. Here is my step function again from the framework Log object with my mods:


step : function ( text, Lvl ) {
if (((Lvl == null)||(TheApp.GetProfileAttr("CurrentLogLevel") >= Lvl))&&
(giLogLines >= giLogBuffer)) {
var fp = Clib.fopen(OutPutFileName, "a");
Clib.fputs(giLogCache, fp);
Clib.fputs(Frame.GetLocalTime()+" "+gsIndent + text + "\n", fp);
Clib.fclose(fp);
giLogLines = 0;
giLogCache = "";
}
else {
giLogLines++;
giLogCache += Frame.GetLocalTime()+" "+gsIndent + text + "\n"
}
}


After setting the new buffer parameter to 20, performance improved drastically. My old 7 second run went down to 2 seconds. Could not even notice that logging was ramped up. My only concern is that things I really want to see don't appear in the log right away, like errors. So I need to modify the RaiseError function to artificially fill the buffer and force a log dump. Here is the new line I inserted (followed by the existing line):


giLogLines = giLogBuffer; //insure errors gets written to log file
Log.step("---End Function "+sFunction+"\n");

I would probably need to do something similar on the Application Close event. Not sure if user's hitting windows X will trigger this though. Something to think about.

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 20, 2010

How to call Workflow - Custom Remote

My previous posts discussed some of the mechanisms to call a workflow process along with their pros and cons. I alluded to how the mode that was desired would determine which business service to call. Workflow Process Manager is the vanilla business process which allows for executing a workflow process in Local Synchronous mode. Siebel provides another business service, Server Requests, which is capable of starting a task in a different component, hence running the process remotely. Unfortuneately, the necessary inputs to this business service must be passed in a child property set, so this business service cannot be called directly from the action of an action set using the business service context attribute. What needs to be done is to is to call a custom shell business service which fills a child property set with the correct attributes, then calls the vanilla Server Request business service.

Let's see how this is done. First of all, a new custom business service needs to be created:

Now create a new custom method for this service:

Finally, create the following method arguments for this new method:

Ok. Now for the script. The script I will about to specify has several features:

  • Calls the Server Requests business service which runs a workflow process in the Workflow Process Manager server somponent in its own thread
  • Input Properties to the Server Requests business service cannot be passed through the business service context attribute of an Action, as Siebel expects these properties as a child property set.
  • Can be called from a Run Time Event-Action by setting all properties as Profile Attributes
  • Can be called from a business service or workflow process by setting all properties directly as Input properties
  • Takes input properties, either from from profile attributes or from input property set, and passes them as custom input properties to a workflow process.
  • Profile Attribute name will be "PassThrough1", for instance, and the profile attribute value must be in the format "", ""
  • Can optionally specify a specific application server to run on, otherwise will allow the load balancer to assign the server
  • By default, will run in Remote Synchronous mode if user is connected, otherwise will run Remote Asynchronously.
  • Can optionally be set to be run as explicitly Remote Asynchronously

This is written in eScript and there are plenty of different ways to go about this, but this is a rough guide (actually, not so rough):

Monday, April 19, 2010

How to call Workflow - Custom

An alternative to the "Managed" runtime events, meaning that the process of activating workflow process creates and updates runtime events and action sets, is to create your own runtime events and action sets. This allows you to create complex sequences of workflow process calls, perhaps setting several profile attributes that are needed in the workflow, calling different versions a workflow process based on mutually exclusive conditional expressions, and calling workflow in different calling modes. With great power comes great responsibility. Walking away from the simple management the activation process provides gives you a lot of flexibility but it also makes your system more complicated and deployment a bit trickier. I think I have said deployment is trick either way now, which when you are talking about complicated processes is probably true regardless. I tend to think the custom route is easier to understand because you control all the steps and the names are more logical because they are your names.

So let me start by defining what I mean by custom managed events. You can achieve the same affect as the managed architecture by creating your own runtime event in the GUI (Administration - Runtime Events) with the same parameters used in the tools workflow pallet, start branch step attributes.

Then you create a custom Action Set with as many custom Actions as necessary. Conditional expressions can be used to prevent the instantiation of a workflow process manager component unnecessarily.The step that needs to actually calls the workflow will have an Action Type of 'BusService'. The Business Service Name will depend on what mode you want the workflow to run in. To simply replicate what the managed process does, just set the Business Process Name to 'Workflow Process Manager', the Method to 'RunProcess', and the Context to "ProcessName", "workflow name" where workflow name is. . . you know. This is a Name, Value pair format for properties accepted by the business service.
It is important to recognize that dynamic process properties cannot be passed to the WF through the business service context. This is because process properties are expected as a child property set. Plus the business service context cannot resolve expressions. If you need to pass dynamic process properties to the workflow, you have two options. You can either set a profile attribute that can be referenced in the workflow or you can create a shell service which I will get into in my next post.

How to call Workflow - Managed

Workflow can be called a variety of ways, with a variety of advantages for each. This will be the first post in a series about the pros and cons of these various mechanisms, along with a tool to assist with some of these approaches.

The most basic, declarative, out of the box way to call workflow is through a run time event managed by the start step of the Workflow process. Let’s take a basic workflow that is meant to call a custom business service when the Account is updated. If you select the branch from the start step, the WF Step Branch applet appears beneath the pallet.

The Event attributes specify a Runtime Event and Action Set that will be created in the GUI when the workflow is deployed and activated. The Action Set has the naming convention Workflow_Row_ID where Row_ID is that of the Action Set record. The Action Set has a single action whose name is Workflow_Row_ID_Row_ID where the first Row_ID is that of the deployed workflow process (can be found in the Active Workflow Processes applet of the Workflow Deployment view). I am not really sure what the second is.

A workflow process called in this way is considered to be running in Local Synchronous mode. The Local refers to the fact that is executing from the same thread that is was called from. So if a user action in the GUI triggers the run time event, then the workflow would be executed within the Sales Object Manager component (or whichever); if the an EAI update were to trigger the runtime event, the workflow would be executed in the EAI Object Manager. Therefore turning up event logging to troubleshoot a workflow called in this manner would require doing so to the Object Manager. The Synchronous part refers to the fact the user session will wait until the workflow has completed execution before control is returned to the user.

If instead of a business service, you had Siebel operation steps in the workflow, it is important to realize that any queries executed would take the most restrictive visibility of the object being queried. In the workflow above which is based on an update to the Account BC, adding a Siebel Operation Query on the Account BC would automatically apply Sales Team visibility to the query. Where this sometimes has an impact is if Assignment Manager runs synchronously between the time the triggering record is committed and when the workflow executes. In that case, the account may no longer be visible to the person who updated the record.

Some of the complexities in managing workflow this way are when you need to do a series of things in a particular order. Finding Workflow with this naming convention can be confusing, but in theory, knowing the naming convention means you can identify the action set you need, and you could then add your own custom actions to that action set, say to set some profile attributes prior to calling the workflow. Or you can add a conditional expression to the action. The hard part is when you need to deploy a new version of that workflow. The deployment and activation process which manages the naming convention can corrupt your custom actions, requiring you to set them up all over again. Deployment in general can get tricky with this approach because the Row Ids are used in the names and Row Ids between environments will differ for the same process. This means that you need to deploy and activate workflows in each environment to generate the runtime events and action sets, but if you need to migrate runtime events and action sets for other reasons, either for personalization or other custom development, you will need to be careful that your workflow managed runtime events from your source environment to not migrate and hence become duplicated in the target environment.

UPDATE: I recalled another serious limitation of this approach.  Workflow called in Local Synchronous mode will only execute if the active business object at the time the runtime event is triggered matches the business object of the workflow.  This implies that if you have a runtime event on the write of an Activity, you would need multiple workflows, one for each business object the Action BC appears in that needs to trigger the workflow.  If you have determined this is still the best approach to use, I suggest a series of shell workflow processes that call a single primary workflow as a subprocess where the primary is based on the main Activity BO, and the shells are based on the various other BOs, all having a single step.