Wednesday, November 5, 2014

Poeplecode Record toString method

Record toString method

This may be one of my favorite shortcuts I've created. For logging or debug it will give you the entire contents of any record in a string. Just like the Java toString class method.

Example


Local Record &rec = CreateRecord(Record.ADM_APPL_PROG);
rem populate record with some values;
MessageBox(0, "", 0, 0, " To String function : %1 ", recToString(&rec));
The output looks like this:
To String function : EARLY_FA_OFFER[abc123,1111,22,12345678,33,2014-11-05,1,4444,55.5]

Function


Function recToString(&R) Returns string;
   Local number &f;
   Local string &txt = &R.name | "[";
   
   For &f = 1 To &R.FieldCount
      If &f > 1 Then
         &txt = &txt | ","
      End-If;
      &txt = &txt | &R.getfield(&f).value;
   End-For;
   
   Return &txt | "]";
End-Function;

Monday, June 9, 2014

Input File using File Layout

The Input File

In this example the input file is a typical comma separated CSV. The requirement is to read the input file and with each line do some type of processing and determine if some of the data is needed to be written to the database. My example is a postal code CSV where we are only interested in a few of the columns.

CSV Example

PostalCode,City,Province,AreaCode,Latitude,Longitude,CityMixedCase 
"L7L 0A1","MYCITY","ON","905","47.386131","-76.774994","Mycity" 
"L7L 0A2","MYCITY","ON","905","47.416172","-76.821386","Mycity" 
"L7L 0A3","MYCITY","ON","905","47.380328","-76.764695","Mycity" 
"L7L 0A4","MYCITY","ON","905","47.374206","-76.760002","Mycity"

Record and File Layout

The first thing I created was a derived record MCM_CC_PSTCDWRK with only the fields I'm interested in from the CSV. I'm going to use postal code, mixed case city and the province. I used the following existing peoplesoft fields:

  1. POSTAL
  2. CITY
  3. STATE

Now I created a File Layout Definition using this record.  In the file layout I add additional FileFields to fill in the for the unused data in the CSV.  Make sure your file layout is defined as a CSV format and if you file uses quotes around the data you need to setup a Default Qualifier as double quote in your file layout segment properties.  Because there are 2 city names in the file I put the matching work record city name in the position of the city name I intend to keep.  In my case the mixed case city position.

Peoplecode

Here we use the File.ReadRowset to get in row in the csv and I knew in my case there was always a header row at the beginning so before starting the loop I perform and extra read from the CSV.  Each data row is then copied to our derived record and only common named fields are copied.  Then we can process the record any way we want before getting the next row from the CSV.

Local File &PostalFile;
Local Record &rec = CreateRecord(Record.MCM_CC_PSTCDWRK);
Local string &sFileName;
Local Rowset &rowSet;

&PostalFile = GetFile(&sFileName, "R", %FilePath_Absolute);
If &PostalFile.IsOpen Then;
   If &PostalFile.SetFileLayout(FileLayout.MCM_CC_PSTCD) Then
      /* Skip the File Header row by reading 2 rows. */
      &rowSet = &PostalFile.ReadRowset();
      &rowSet = &PostalFile.ReadRowset();
      While &rowSet <> Null
         &rowSet.GetRow(1).MCM_CC_PSTCDWRK.CopyFieldsTo(&rec);
         /* some kind of &rec processing */ 
         updatePostalCode(&rec);
         &rowSet = &PostalFile.ReadRowset();
      end-while;
   end-if;
end-if;
&PostalFile.Close();


Thursday, May 29, 2014

Evaluate True, for large report processing

Report Requirement

The processing required for many reports or file exports (csv) I've been creating have many data collection calls.  Sometimes multiple tables can be joined to reduce the number of unique calls but often the there are times where data is not found or the returned data needs some analysis.  If the design also calls for different reporting or actions based on the analysis it forces you to make many individual calls.  

In my example we are using one large SQL to query to get a initial set of Employee ID's.  With each employee ID I as asked to do about 10 other steps of data collection and analysis each step could lead to one of the following results:
  • Skip to the next Employee ID
  • Write the report row and skip to next Employee ID
  • Continue Processing
  • Insert Data 
My initial build was using a while loop through the employees with if conditions inside however it became apparent that my nested IF conditions were getting out of hand.

My Solution 

The solution I found to create very clean and readable code that also matched the design documentation was to use an EVALUATE TRUE statement.

As you can see the &report record is a derived record that is used to accumulate the data as we go.  I used local functions to do the mini calls if I the requirement is to skip an employee when a record is not found the function simply returns FALSE.  This will stop the processing of any more WHEN statements in the EVALUATE.  In some cases I return a whole record if the data is needed for future calls and in this case I do my check for a null record returned within the WHEN statement.

Main Loop Processing

/* Data & Records */
Local Record &admApplProg, &extAcadDataA, &extAcadDataB, &mcmFaErlyAvg;
Local Record &report;
/* Processing Variables */
Local boolean &resumeEmplid;

&admApplProg = CreateRecord(Record.ADM_APPL_PROG);
&Sql = CreateSQL(SQL.SQL1, &admApplProg);
While &Sql.Fetch(&admApplProg)
   &resumeEmplid = True;
   &report = CreateRecord(Record.MCM_AD_FFA_RPT);
   &report.EMPLID.Value = &admApplProg.EMPLID.Value;
   &report.ADM_APPL_NBR.Value = &admApplProg.ADM_APPL_NBR.Value;
   &report.ACAD_CAREER.Value = &admApplProg.ACAD_CAREER.Value;
   &report.ACAD_PROG.Value = &admApplProg.ACAD_PROG.Value;
   &report.PROG_ACTION.Value = &admApplProg.PROG_ACTION.Value;
   
   Evaluate True
   When &resumeEmplid
      /* Drop if EXT_ORG_ID not found */
      &extAcadDataA = getMaxExtAcadData(&report, &admApplProg.EMPLID.Value);
      If None(&extAcadDataA) Then
         &resumeEmplid = False;
      End-If;
   When &resumeEmplid
      /* Valid Canadian Citizen */
      &resumeEmplid = isValidCndApplicant(&REPORT, &extAcadDataA);
   When &resumeEmplid
      /* retrieve an Education Source */
      &resumeEmplid = hasEducSrc(&report);
   When &resumeEmplid
      /* Read Adm_APPL_PLAN */
      &resumeEmplid = hasAcadPlan(&report, &admApplProg);
   When &resumeEmplid
      /* Retrieve the students averages */
      &resumeEmplid = hasMcmAvg(&report, &admApplProg);
   When &resumeEmplid
      /* read Early Financial Aid value*/
      &mcmFaErlyAvg = getErlyAvg(&report, &admApplProg);
      If None(&mcmFaErlyAvg) Then
         &resumeEmplid = False;
      End-If;
   When &resumeEmplid
      /* 9. Define the FA Year*/
      &resumeEmplid = hasFaYear(&report, &admApplProg);
   When &resumeEmplid
      /* Write the report row */
      writeRecordToFile(&report);
   End-Evaluate;
      
End-While;

Functions

Here you'll see some functions that are returning records and some that return the Boolean of successful data retrieval or not.  I have not include all the functions just some of the examples.  One thing you will notice is that I'm passing the main &report record into the functions.  This is because PeopleCode uses pass by reference so you can set values in the local fuction &REPORT will be retained after your function returns to the mainline.

If you are curious about my exists check on function hasFaOffer I have previously blogged this example.

/* Identify last school attended */
Function getMaxExtAcadData(&REPORT) Returns Record;
   Local SQL &SqlExtAcad;
   Local Record &extAcadData;
   Local String &saveOrgId;
   
   &extAcadData = CreateRecord(Record.EXT_ACAD_DATA);
   &SqlExtAcad = CreateSQL(SQL.MAX_EXT, &extAcadData, &RECORD.EMPLID.value);
   While &SqlExtAcad.Fetch(&extAcadData)
      If None(&saveOrgId) Then
         &saveOrgId = &extAcadData.EXT_ORG_ID.Value;
      Else
         /* if more then one EXT_ORG_ID is found report and skip process */
         If &saveOrgId <> &extAcadData.EXT_ORG_ID.Value Then 
            &REPORT.REASON.value = "Verify EXT_ORG_ID";
            Return Null;
         End-If;
      End-If;
   End-While;
   If None(&extAcadData.EXT_ORG_ID.Value) Then
      Return Null;
   End-If;
   Return &extAcadData;
End-Function;

Function getErlyAvg(&REPORT, &AAP) Returns Record;
   Local string &qry, &result;
   Local Record &record = CreateRecord(Record.MCM_FA_ERLY_AVG);
   &qry = "%SelectAll(:1 A) A WHERE ...";
   SQLExec(&qry, &record, ... , &record);
   If All(&record.INSTITUTION.Value) Then
      &REPORT.MCM_FA_AWARD_VALUE.value = &record.MCM_FA_AWARD_VALUE.Value;
      &REPORT.MCM_FA_MIN_SCH_AVG.value = &record.MCM_FA_MIN_SCH_AVG.Value;
      &REPORT.MCM_FA_MAX_SCH_AVG.value = &record.MCM_FA_MAX_SCH_AVG.Value;
      Return &record;
   Else
      Return Null;
   End-If;
End-Function;

Function hasFaYear(&REPORT, &AAP) Returns boolean;
   Local string &result;
   
   SQLExec(SQL.MCM_0784_SQL3, &AAP.EMPLID.value, ... , &result);
   &REPORT.AID_YEAR.value = &result;
   Return True;
End-Function;

Function hasFaOffer(&REPORT, &AAP) Returns boolean;
   Local string &exists;
   Local number &result;
   
   SQLExec(SQL.MCM_0784_SQL4, &AAP.EMPLID.value, ... , &exists, &result);
   If All(&exists) Then
      &REPORT.EARLY_OFFER_TOTAL.value = &result;
      Return True;
   Else
      Return False;
   End-If;
End-Function;


Proof of Concept

Here is a small little code snippet I used as a proof of concept

Function getTrue(&array) Returns boolean;
   MessageBox(0, "", 0, 0, " getTrue Function ");
   &array.Push("True");
   Return True;
End-Function;

Function getFalse(&array) Returns boolean;
   MessageBox(0, "", 0, 0, " getFalse Function ");
   &array.Push("False");
   Return False;
End-Function;

Local boolean &resume = True;
Local array of string &evalCount;

&evalCount = CreateArrayRept("", 0);

Evaluate True
When &resume
   MessageBox(0, "", 0, 0, " Evaluate 1 ");
   &evalCount.Push("1");
When &resume
   MessageBox(0, "", 0, 0, " Evaluate 2 ");
   &evalCount.Push("2");
   &resume = getTrue(&evalCount);
When &resume
   MessageBox(0, "", 0, 0, " Evaluate 3 ");
   &evalCount.Push("3");
When &resume
   MessageBox(0, "", 0, 0, " Evaluate 4 ");
   &evalCount.Push("4");
   &resume = getFalse(&evalCount);
When &resume
   MessageBox(0, "", 0, 0, " Evaluate 5 ");
   &evalCount.Push("5");
When &resume
   MessageBox(0, "", 0, 0, " Evaluate 6 ");
   &evalCount.Push("6");
   &resume = getTrue(&evalCount);
End-Evaluate;

MessageBox(0, "", 0, 0, " Array %1 ", &evalCount.Join(", "));

Wednesday, May 21, 2014

PeopleSoft Manual Auto Numbering

Record Setup

My setup for an auto numbering in a peoplesoft record was simply to define the next value at save time.  There is another way to do this that includes built in functions and requires more setup in some of the PeopleSoft delivered tables.  However, my approach was for a small number of users and records so I used the simpler approach.  The Record just needs to have an ID value that will trigger some PeopleCode at save time.  Create your new record with the default value with a trigger such as "New".



People Code

Now in the PeopleCode we need to setup some logic in both SearchInit and SavePreChange.  The SearchInit checks the PeopleSoft variable Mode and if it's (A)dd Mode then we are going to make sure ID value is set to the same default value "NEW" because the record default is not used in this case.  The SavePreChange code will be triggered when the new record is attempting to save to the database.  The logic will check for our "NEW" value and if the ID is still that we select the max value on the table and add 1 before saving.

SearchInit

If %Mode = "A" Then
   MCM_MRF_IP_SCN.MCM_IP_ID.Value = "NEW";
   SetSearchDialogBehavior(0);
End-If;

SavePreChange

Local string &nextId;

If MCM_MRF_IP_SCN.MCM_IP_ID.Value = "NEW" Then
   SQLExec("Select max(MCM_IP_ID)+1", &nextId);
   MCM_MRF_IP_SCN.MCM_IP_ID.Value = &nextId;
End-If;

This code is limited to only numeric values in the ID column.  In our environment we are using a char to hold the numeric and our customer wants leading zeros on all the numbers.  There was also a special condition requested for the customer to have obsolete or test data in the table that would have been prefixed with an alpha char i.e. ID = "X01" or test data may be ID = "T01".   I was asked to ignore these cases that would have caused the query to fail anyways because they are not numeric and arithmetic can't be done on the value returned.  The final solution looked like the following:

Query for ONLY numeric ID's and add Leading Zeros

Local string &nextId;
Local string &qry;

If MCM_MRF_IP_SCN.MCM_IP_ID.Value = "NEW" Then
   &qry = "Select max(MCM_IP_ID)+1 from %table(MCM_MRF_IP_SCN) ";
   &qry = &qry + " where REGEXP_LIKE(MCM_IP_ID, '^[[:digit:]]+$') ";
   SQLExec(&qry, &nextId);
   &nextId = Rept("0", 3 - Len(&nextId)) | &nextId;
   MCM_MRF_IP_SCN.MCM_IP_ID.Value = &nextId;
End-If;

The Message Catalog

Define a message

The utility page for creating your messages if found in:
Main Menu > People Tools > Utilities > Administration > Message Catalog

 Message Set is a grouping of related messages each with their own number.  The message text is what will be displayed in the Message log or on the screen.  If the users clicks on Explain in the message log they will get the Message Text and Description from the message catalog.  


Triggering a Message

When you kick out a message from Application Engine it will try to resolve the text using the Set number followed by message number.  If the message is not found the default text in the example below will be provided.  You can also include place holders for both default text and a message catalog version.

MessageBox(style, title, message_set, message_number, default_text [,parameters] )

In this example I created a set 21007 and Message number 7 is an employee Count.


App Engine Message Example

MessageBox(0, "", 21007, 7, "Count = %1 ", &mainCount);


No Catalog Example

In some cases you want to spit out a message during testing but it's not going to be included in the final release of message.  For these the common practice is to use set 0 and message number 0.  We will remove them or comment the messages out before moving to production.

MessageBox(0, "", 0, 0, " Testing Main Counter = %1 ", &mainCount);

Thursday, April 24, 2014

SQLExec for Number Column

The Snag

Today I was hitting a problem using the SQLExec function with a query against a single number column.  I needed to retrieve a specific value for an employee and if no rows are found my program would skip this employee.  The logic seemed simple enough something like the following:

SQLExec(SQL.MCM_SNLF_BN_RCD_NBR, &employee, &EmplRcd);
If None(&benefitEmplRcd) Then
   MessageBox(0, "", 20002, 4, " No Employee Record skip employee");
   &record = Null;
   Return &record;
End-If;

BAD! 

When you run this code the query is against a Number column and that number column even if no record is found will return a 0 not a null and your IF condition will never be true.  Also if zero is a valid value for Employee record then code after using that value could execute against all the wrong data.

Solution

My solution to this is pretty easy simply include another key value of your query in my case Employee ID which will never be empty and also a String value.  Use that column as your check for existence instead of the number column.

Local  &emplidExists;
SQLExec(SQL.MCM_SNLF_BN_RCD_NBR, &employee, &emplidExists, &EmplRcd);
If None(&emplidExists) Then
   MessageBox(0, "", 20002, 4, " No Employee Record skip employee");
   &record = Null;
   Return &record;
End-If;

Friday, March 21, 2014

The PeopleSoft State Records and Run Controls

Goal

We have a process that generates an Export file.  The following are some of the simple requirements for this process request.

  • There is a need to allow the customer to request this file at any time and to provide the File Name of the file. 
  • If the file exists the process will fail with a status of "No Success" so we will provide a Delete Flag for the requester to have the existing one deleted if found.
  • A header in the export file needs a Sequence Number from the requester


Overview

You can think of the state record as the record of assigned variables that exists for an single instance of an application engine execution process.  Each execution will have its own record and can be unique to that process run.  A state record is passed variables from the process request record commonly called the Run Control.  There can also be fields on a state record to indicate process completion status or restart positions or any other useful information you'd like to store between executions or restarts.  To be able to restart using previous state records you need to commit your state record to the database in a physical table.  In many cases this is not required so a Derived record is used and will only exist in memory during that execution.  In this example I'm going to demonstrate a very simply run control and state record where we can pass in variables like the desired file name for output.

Records

Run Control

We are going to need two records the first is the Run Control which will be a physical table to store each user's values for this Application Engine run requests.  It is an SQL table that must have two keys the RUN_CNTL_ID and OPRID.  These fields are required because this table is a child of PRCSRUNCNTL which to my knowledge serves little purpose because the only other columns on the parent table are language codes.  After this you can add any fields that you need to pass into your application Engine.  In this example we are passing in FILENAME, SEQNUM and DELETE_FLAG

State Record

For the second record the state record we are NOT going to use a SQL table because we don't need to commit and retain process information after completion of this Application Engine process.  So your record can be set to Derived which means it will only exist in memory during the execution process.  This record must have a single key column PROCESS_INSTANCE. Add additional fields that your Run Control is passing to the execution process or Application Engine status flags you wish to set in your PeopleCode.
* Note: AE_APPSTATUS is just one example if a field which gives developers a way to flag the return status of this process.  It's a way to flag the run as Success, No Success or Warnings using your own peoplecode.

Run Control Page

This page is where the customer requests the run of this Application Engine Process.










In this example I'm going to pass into my Application Engine process 3 variables.  File Name, Sequence Number and a Delete Flag value.  At any time in my application Engine I will have the ability to query a copy of these values stored on my state record during that instance of processing.  The page is built by copying an existing Run Control page and this will ensure the correct Run Control sub pages are in place.  These sub pages are what display the ID, Language, Report Manager, Process Monitor and Run button on your new page.
We are only making changes to the title and our custom Run Control fields in Yellow and we leave the subpage and Derived titles as is.

Component

The search page assigned to this Run Control component is the parent table PRCSRUNCNTL this means that a single Run Control ID can be used across all your different Run Control pages.


The table structure shows how the parent table links to each individual run control record and that run control record is copied into your Process Instance State Record during your run.

Application Engine

Now you are ready to put this to use in your Application Engine.  First you need to open the properties of your App Engine and add your State Record.



To populate the values of your state record at runtime you need to have an SQL step at the very top of you Application Engine.  This first SQL action will query your Run Control table with the requester's ID and Run Control ID and copy the fields with the same names to your state record.  The follow step can be your first Application Engine PeopleCode process.  This is where you can access your state record and to use the values passed in.


SQL

%Select(FILENAME, SEQNUM, DELETE_FLAG) 
 SELECT FILENAME, SEQNUM, DELETE_FLAG   
 FROM PS_MCM_TRN_DS_RC 
 WHERE OPRID = %OperatorId AND RUN_CNTL_ID = %RUNCONTROL


Now that the State record is has your values they can be access in any of the PeopleCode used within the current Application Engine process.  The following code will just display it in the log file but you can use the values in queries, properties, settings or generating a file with a specific name.

PeopleCode

MessageBox(0, "", 0, 0, "Application Engine! ");
MessageBox(0, "", 0, 0, "State Record FILENAME = %1", MCM_TRN_DS_AET.FILENAME);
MessageBox(0, "", 0, 0, "State Record SEQNUM = %1", MCM_TRN_DS_AET.SEQNUM);
MessageBox(0, "", 0, 0, "State Record DELETE_FLAG = %1", MCM_TRN_DS_AET.DELETE_FLAG);