Portal

Modifying Lawson screens for ImageNow LearnMode

A question was posed to me recently regarding how to effectively and efficiently use ImageNow LearnMode on a Lawson screen. Specifically, the problem revolves around fields that exist on Lawson tabs that are not displayed when the page is loaded. This means that no DOM objects are built and ImageNow cannot read them. Take HR11, for example, where my company links the Process Level to a Custom Property. The problem is, the Process Level for an employee is on the ‘Assignment’ tab which is not the default tab. Because of this, Lawson does not build the DOM for the tab until you click on the ‘Assignment’ tab even though the data exists for it (the AGS transaction returned all data, even if the form hasn’t been populated).

From an ImageNow perspective, the tradition is to use the getAppData VB function in LearnMode to parse through the entire hierarchy to find the data you want. This presents several problems as each user in Lawson may have different bookmarks, search bars, etc and thus a different hierarchy. The exact location of the element that you want to get data from (like the process level field) will move it’s relative position on a per user basis. I have used this method myself and I know that a lot of Perceptive implementation consultants do it too, but that doesn’t make it right.

Note: I’m not discussing getAppData here because it could be a whole post itself. If you’re interested, leave a comment and I may do a post on it. After all, I’m doing this post because of a comment that was left.

There are only three reasons to use getAppData in ImageNow LearnMode for Lawson.

  1. You don’t own Design Studio
  2. You’re trying to link on a Detail screen with many lines and you don’t know which line to link (PO64 comes to mind)
  3. You don’t know what you’re doing

My opinion is that #1 is the only valid reason and even that is tenuous. As far as I know, you can customize any Lawson form and Portal will render it for you, Design Studio just provides a development environment. You can create customized form xml without Design Studio in the same way that you can write Java code without an IDE, it’s just a lot easier when you have a development environment.

Let me restate the problem: There is data on a Lawson form that ImageNow IE LearnMode cannot read. This data is on the form, but it is on a tab that is not the default. Therefore, this data is not available to ImageNow until the tab is selected. Once it is selected, it is always available until the user leaves the screen. Make Sense?

There are three relatively easy ways to do this, two using Lawson Design Studio, that I am aware of. Each has it’s pros and cons.

  1. Have the user click on the tab in question after they load the form (and any subsequent time they access it if they go to another form). This is probably the least desirable as you rely on the users to remember to do this. If you’re linking into Custom Properties in ImageNow, chances are good that you’ll wind up with a lot of “default” values.
  2. Create a new field (or fields) that is hidden from the user. Any event or field change, update the field(s). If the field(s) is created on the ‘header’ section of the form, it is always visible to ImageNow and can be linked on. The downside is that you have to control updating your fields on both Form events and on changes to the field itself. This can be especially tricky if the field you need is auto-populated by Lawson as a default (like a name or description).
  3. Activate the tab and then activate the original tab. This is the automated version of #1. I have tested this, but never used it in a production setting, so I can’t guarantee it works. Aside from that, it’s the easiest to implement. However, if you need data from four or five different tabs, you may find it easier to just create the fields as in #2.

Keep in mind, if you need data that is not on the form you are linking, then you MUST use option #2.

Enough of the jibber-jabber, how about some code? All of these examples will use AP20. For our pretend requirements, I need ImageNow to link the “Cash Code” that is located on the “Payment” tab and is the third tab on the AP20 (aka not the default).

Option 3.

function FORM_OnAfterFrameworkInit()
{
		lawForm.setActiveTab("TF0-2");
		lawForm.setActiveTab("TF0-0");
}

Here we activate tab 3 (using the tab name) and then re-activate the default tab.
Add this as early in the event hierarchy on a form load as you can. I believe the earliest you can add it to be the OnAfterFrameworkInit, as demonstrated here. The XML for the form has been loaded, the initial DOM has been built, and any script actions CAN affect DOM elements. Keep in mind that data is not available at this point, even if you have an initial form action set.

Option 2.
First create a empty text field. In this case, I created “text80”.

FIELD_NAME = "text80";  //Global field declaration - Note this is the Name on the field Properties
//Hide field
function FORM_OnInit()
{
	var fElem=lawForm.getFormElement(FIELD_NAME);
	fElem.style.visibility = "hidden";
}

//Populate our field every time an action is performed
function FORM_OnAfterTransaction(data)
{
	var strCashCode = data.getElementsByTagName("_f96")[0].firstChild.nodeValue;  //Pull our data out of AGS return
	lawForm.setFormValue(FIELD_NAME,strCashCode,0); //Update field to be cashcode
}

//Update the field if the user changes it.  This means we can point ImageNow at a single field and not miss any updates
function TEXT_OnBlur(id, row)
{
	if (id == "text38") //ID of the cash code field
		lawForm.setFormValue(FIELD_NAME,lawForm.getFormValue(id,row),row);
	return true;
}

The OnInit event hides our element as the form is loaded. This could be done by directly manipulating the XML that constructs the form. I don’t like to manipulate the XML if I can help it, not that I’m above it, but bad things can happen. I’ve been there, don’t let it happen to you.
The FORM_OnAfterTransaction event is triggered after any event on the form (Inquire, Add, etc). It may not be appropriate to evaluate this on every action, but when in doubt, do it always.
The TEXT_OnBlur event is executed as the text loses focus. Basically, this is after a user changes the field directly. THIS IS IMPORTANT. If the user changes the data in the field, we need to capture it because they may not execute a Lawson action prior to performing the ImageNow link, which means it won’t be trapped by the FORM_OnAfterTransaction event.

All this event talk makes me think I might need to do a primer on the Lawson Portal Event hierarchy. But I digress.

Regarding my previous statement about why you would use getAppData, I’ve basically addressed #1 and #3. #2 is a little trickier, but not much. (Sorry for the confusion, but WordPress doesn’t seem to support non-numbered ordered lists, so it’s difficult to differentiate the lists.) I would propose that you basically use the second #2 and have the users put an X (select) in FC detail field. You would then use the TEXT_OnBlur for the FC field as a trigger. When the even is triggered, have it write the data to fields at the header record that ImageNow can read. This solves problems like linking from AP90 or GL90. Yes, if the user enters X in multiple fields, only the last row where an X was entered would be linked. This is a training issue. You can program around it, but it has to stop somewhere.

Thanks to Matt from Perceptive for the question.

HTH

Advertisement

Everything you need to know about Lawson Comments

I have a love/hate relationship with Lawson comments. They are an awesome feature, but implemented in such a way that you’d think somebody made it hard on purpose. This post is going to be about how to deal with comments (mostly in large quantities). This is everything that you “need” to know, not everything you “want” to know. Probably once a week, I get asked “Can you query for comments?”. The answer is Yes*. The other question I get a lot is: “Can you upload comments?”. Again, the answer is Yes**.

*No, you cannot query them out through DME.
**No, you cannot use MS Add-ins.

Uploading Comments
Let’s first talk about adding comments to Lawson so that we have something to query out. There is only one way to add comments to Lawson, and that’s through the writeattach.exe cgi program. How you choose to implement it is up to you, but I’m going to explain how to do it through Lawson ProcessFlow. You can add comments via either GET or POST, but I prefer the POST because I have more control over how the comments get added and I don’t have to do a lot of encoding work.

The basic XML for the comments looks like:

  <ATTACHXML>
    <_AESC>IE</_AESC> 
    <_ANAM>Comment Title</_ANAM> 
    <_ATXT>
      <![CDATA[ 
       Comment Text is here
      ]]> 
    </_ATXT>
    <_OUT>XML</_OUT> 
    <_PDL>PRODUCTLINE</_PDL> 
    <_FN>FILENAME</_FN> 
    <_IN>INDEXNAME</_IN> 
    <K1>KEY1VALUE</K1> 
    <K2>KEY2VALUE</K2> 
    <K3>KEY3VALUE</K3> 
    <K4 /> 
    <_ATYP>C</_ATYP> 
    <_AUDT>COMMENTTYPE</_AUDT> 
    <_USCH /> 
    <_DATA>TRUE</_DATA> 
    <_ATTR>TRUE</_ATTR> 
    <_AOBJ>TRUE</_AOBJ> 
    <_OPM>M</_OPM> 
    <_ECODE>FALSE</_ECODE> 
  </ATTACHXML>

The URL for my WebRun node is: cgi-lawson/writeattach.exe and I send the XML above as a POST string. In order to figure out what to put in the tags, I used dbdef. In order to add comments you must follow these steps. First, verify that the file allows attachments. Second, find the index name and what fields go in the index value fields (you can add more as needed – but the values should be the actual value like 0001 instead of 1). The only thing that I don’t have a good way to get is the value for is the _AUDT tag. This is the comment type, so for forms that allow more than one comment type, this value will change. I’m sure there’s some method to figure this out, I just don’t know what it is. I generally use Fiddler, add a comment, then check the calls.

Note: If you do use process flow, you have to be careful with the CDATA nodes. Process flow will interpret the <! as the beginning of a process flow variable and try to replace it. As a result, if you need the CDATA nodes, you should build the post string either in the XML node or an Assign node and then put that variable name in the WebRun.

Querying comments
There are two ways to query comments. One is using SQL (my personal preference, but not always applicable) and the other is using cgi calls.

For the SQL version, it’s important to talk about the table structure used to actually store the data. For each table that allows attachments/comments, there are two tables to store the comments, a “Header” and a “Detail”. The “Header” contains the name of the comment as well as the start of the comment. The “Detail” table contains the rest of the comment.

For a table that allows comments, the comment tables are always named “L_<Table_Type><Table_Prefix>”. Consequently, comments for the APINVOICE table (prefix is API) would be named L_HAPI and L_DAPI. The H and the D indicate Header and detail.

The relationship between the base table (APINVOICE) and the Header comment table (L_HAPI) is:

APINVOICE.L_INDEX = L_HAPI.L_INDEX
'APINVOICE' = L_HAPI.TABLE_NAME

The relationship between the Header (L_HAPI) and detail comment table (L_DAPI) is:

L_HAPI.L_INDEX = L_DAPI.L_INDEX
L_HAPI.ATCHNBR = L_DAPI.ATCHNBR

The data relationships are:

APINVOICE 1:M L_HAPI 1:M L_DAPI

This gets us data that looks something like this (I’m not displaying duplicated data as the query results would actually be; this is to aid understanding of the relationships):

Invoice L_INDEX ATCHNBR Comment Name Seq Comment
122344 zzzz 1 Comment 1 1 some comment detail
2 More comment detail
2 Comment 2 1 some comment detail

The OBJECT field of the Header and Detail tables contains the comments. For the Header table, the OBJECT field actually contains more than the comment. Technically, it’s comma-delimited and each of the first three “field”s has it’s name as part of it. The fourth field is the beginning of comment itself. However, the data is clearly space padded, so I would strongly urge you to delimit based on places, not on commas, if for not other reason than your comment could contain commas and then you’re in trouble. If you’re wondering, the actual comment data begins at character 96. The “Detail” table has a record for any comment that is longer than 416 characters. Each Detail OBJECT field is 1024 characters, so for each comment that is longer than that, there will be an additional record. You must put all of these records back together to form the full comment.

Here is what a query for invoice comments might look like (Oracle Version).

SELECT API.COMPANY, API.VENDOR, API.INVOICE, H.L_INDEX, H.ATCHNBR, H.R_NAME, H.OBJECT, D.SEQNBR, D.OBJECT
FROM LAWSON.APINVOICE API
LEFT OUTER JOIN LAWSON.L_HAPI H
ON API.L_INDEX = H.L_INDEX
  AND H.FILENAME = 'APINVOICE'
LEFT OUTER JOIN LAWSON.L_DAPI D
ON H.L_INDEX = D.L_INDEX
  AND H.ATCHNBR = D.ATCHNBR
WHERE API.INVOICE = '123456' 
ORDER BY D.SEQNBR

You should always make sure that you OUTER JOIN your comment tables to your base tables as the comment tables are never required. Because you may return multiple detail records for each base record, you will have to deal with “putting them back together”. My personal preference is to use the Oracle Hierarchy queries (which I posted on here). However, I have been known to use LEAD and LAG when I have a clearly defined recordset. If you are using Crystal, you can use the Hierarchal functions there as well.

Non-SQL Version
Now that you’ve seen what it takes to create a comment and what the data looks like in the tables, it’s time to discuss the non-SQL ways to get the data. There are two options, ListAttachments and getattachrec.exe programs.

The shortcut method (version 9.0.1) is to use ListAttachment:

/lawson-ios/action/ListAttachments?&dataArea=<PRODUCTLINE>&fileName=<TABLENAME>&indexName=<INDEX>&K1=<Key1Value>&K2=<Key2Value>&K3=<Key3Value>&K4=<Key4Value>&K5=<Key5Value>&outType=XML

You follow the same basic rules to build this URL as you would to build the writeattach.exe. The primary difference is that you do not need to give explicit values (like 0001 for company).
A response from the APINVOICE comments might look like this:

<?xml version="1.0" encoding="ISO-8859-1" ?> 
<ACTION actionName="LISTATTACHMENTS">
    <LIST numRecords="1" maxRecordCount="500" hasMoreRecords="false" status="pass">
        <MSG /> 
        <ATTACHMENT attachmentNbr="zz" indexName="APISET1" attachmentSize="680" createDate="20100112" modifiedDate="20100112" createUser="lawsonuser" modifiedUser="lawsonuser" dataArea="PROD" K5="9999" K4="0" K3="3694823" K2="20840" K1="120" createTime="074725" modifiedTime="074725" attachmentCategory="commentAtch" attachmentType="A" lIndex="yEXb" fileName="APINVOICE" status="pass">
            <MSG /> 
            <ATTACHMENT-NAME>
                <![CDATA[ Attachment name]]> 
            </ATTACHMENT-NAME>
            <ATTACHMENT-TEXT>
                <![CDATA[ Attachment data ]]> 
            </ATTACHMENT-TEXT>
        </ATTACHMENT>
    </LIST>
</ACTION>

As for the getattachrec.exe, I don’t have a reliable method of building these, it’s tedious no matter how you do it. Once you figure out how to get your comment (like the Invoice example above), you might be able to replicate it without having to go through the entire process every time, but you’ll have to use trial and error. The biggest issue you’ll face is that some tables allow for multiple comment types, and you won’t know which ones (if any) exist.

My preferred method to build the URLs the first time around is to either put Lawson in debug mode or use Fiddler. There are three main calls that you will need to focus on.
1) Drill from a Lawson screen on our record (like AP90.1)
2) Based on the data from #1, construct our next URL to get the comment header
3) Based on the data from #2, construct our next URL to get the comment detail

1) Here’s an example of the IDA (Drill) URL. This is for the same invoice as above.

/servlet/Router/Drill/Erp?_OUT=XML&keyUsage=PARAM&_TYP=CMT&_PDL=PROD&_SYS=AP&_TKN=AP90.1&_KNB=50&01=120&08=CHPV&AVi=%20%20%20%2020840&50=3694823&AQ=9999&03=%20%20%20%2020840&_LKN=50&_RECSTOGET=0

This gets us output like this:

<?xml version="1.0" encoding="ISO-8859-1" ?> 
<IDARETURN productline="PROD" title="">
<COLUMNS /> 
<FINDNEXT /> 
<PREVPAGE /> 
<NEXTPAGE /> 
<BASEURL /> 
<LASTUSED /> 
<LINES count="4">
  <LINE>
    <IDACALL type="CMT">
      <![CDATA[ cgi-lawson/getattachrec.exe?_AUDT=A&_IN=APISET1&K1=120&K2=20840&_FN=APINVOICE&K3=3694823&K4=0&_ATYP=C&K5=9999&_TYP=CMT&_OPM=C&_OUT=XML&_ATTR=TRUE&_DRIL=TRUE&_AOBJ=TRUE&_PDL=PROD&_ON=Invoice+Note%2FReport%2FCheck+Comments  ]]> 
    </IDACALL>
    <ATTACHMENTCALL type="CMT">
      <![CDATA[ lawson-ios/action/ListAttachments?attachmentType=A&indexName=APISET1&K1=120&K2=20840&fileName=APINVOICE&K3=3694823&K4=0&attachmentCategory=C&K5=9999&drillType=CMT&outType=XML&dataArea=PROD&objName=Invoice+Note%2FReport%2FCheck+Comments  ]]> 
    </ATTACHMENTCALL>
    <COLS>
      <COL>
        <![CDATA[ Invoice Note/Report/Check Comments  ]]> 
      </COL>
    </COLS>
    <KEYFLDS /> 
    <REQFLDS /> 
  </LINE>
  <LINE>
    <IDACALL type="CMT">
      <![CDATA[ cgi-lawson/getattachrec.exe?_AUDT=N&_IN=APISET1&K1=120&K2=20840&_FN=APINVOICE&K3=3694823&K4=0&_ATYP=C&K5=9999&_TYP=CMT&_OPM=C&_OUT=XML&_ATTR=TRUE&_DRIL=TRUE&_AOBJ=TRUE&_PDL=PROD&_ON=Invoice+Notes  ]]> 
    </IDACALL>
    <COLS>
      <COL>
        <![CDATA[ Invoice Notes  ]]> 
      </COL>
    </COLS>
    <KEYFLDS /> 
    <REQFLDS /> 
  </LINE>
  <LINE>
    <IDACALL type="CMT">
      <![CDATA[ cgi-lawson/getattachrec.exe?_AUDT=D&_IN=APISET1&K1=120&K2=20840&_FN=APINVOICE&K3=3694823&K4=0&_ATYP=C&K5=9999&_TYP=CMT&_OPM=C&_OUT=XML&_ATTR=TRUE&_DRIL=TRUE&_AOBJ=TRUE&_PDL=PROD&_ON=Invoice+Report+Comments  ]]> 
    </IDACALL>
    <COLS>
      <COL>
        <![CDATA[ Invoice Report Comments  ]]> 
      </COL>
    </COLS>
    <KEYFLDS /> 
    <REQFLDS /> 
  </LINE>
  <LINE>
    <IDACALL type="CMT">
      <![CDATA[ cgi-lawson/getattachrec.exe?_AUDT=C&_IN=APISET1&K1=120&K2=20840&_FN=APINVOICE&K3=3694823&K4=0&_ATYP=C&K5=9999&_TYP=CMT&_OPM=C&_OUT=XML&_ATTR=TRUE&_DRIL=TRUE&_AOBJ=TRUE&_PDL=PROD&_ON=Invoice+Check+Comments  ]]> 
    </IDACALL>
    <COLS>
      <COL>
        <![CDATA[ Invoice Check Comments  ]]> 
      </COL>
    </COLS>
    <KEYFLDS /> 
    <REQFLDS /> 
  </LINE>
</LINES>
</IDARETURN>

2) Run each URL that you find in the IDACALL nodes to try and get a comment header. Running one of these URL’s (the Invoice Note/Report/Check Comments one) will give us something like this:

<?xml version="1.0" encoding="ISO-8859-1" ?> 
<Report cgidir="/cgi-lawson/" executable="getattachrec.exe" productline="PROD" filename="APINVOICE" token="Token" keynbr="KeyNbr">
  <QueryBase exepath="/cgi-lawson/writeattach.exe">
    <![CDATA[ _OUT=XML&_PDL=PROD&_FN=APINVOICE&_IN=APISET1&  ]]> 
  </QueryBase>
  <WinTitle>
    <![CDATA[ Invoice Note/Report/Check Comments  ]]> 
  </WinTitle>
  <DrillAround>
    <DrillData>
      <DrillType>Comment</DrillType> 
      <DrillName>
        <![CDATA[ Invoice Note/Report/Check Comments  ]]> 
      </DrillName>
      <DrillUserType>A</DrillUserType> 
      <DrillScheme>
        <![CDATA[ none  ]]> 
      </DrillScheme>
      <OneOnly>F</OneOnly> 
      <NoDetail>F</NoDetail> 
      <NoDrill>F</NoDrill> 
      <DisplayOrder>A</DisplayOrder> 
    </DrillData>
    <DrillData>
      <DrillType>Comment</DrillType> 
      <DrillName>
        <![CDATA[ Invoice Notes  ]]> 
      </DrillName>
      <DrillUserType>N</DrillUserType> 
      <DrillScheme>
        <![CDATA[ none  ]]> 
      </DrillScheme>
      <OneOnly>F</OneOnly> 
      <NoDetail>F</NoDetail> 
      <NoDrill>F</NoDrill> 
      <DisplayOrder>A</DisplayOrder> 
    </DrillData>
    <DrillData>
      <DrillType>Comment</DrillType> 
      <DrillName>
        <![CDATA[ Invoice Report Comments  ]]> 
      </DrillName>
      <DrillUserType>D</DrillUserType> 
      <DrillScheme>
        <![CDATA[ none  ]]> 
      </DrillScheme>
      <OneOnly>F</OneOnly> 
      <NoDetail>F</NoDetail> 
      <NoDrill>F</NoDrill> 
      <DisplayOrder>A</DisplayOrder> 
    </DrillData>
    <DrillData>
      <DrillType>Comment</DrillType> 
      <DrillName>
        <![CDATA[ Invoice Check Comments  ]]> 
      </DrillName>
      <DrillUserType>C</DrillUserType> 
      <DrillScheme>
        <![CDATA[ none  ]]> 
      </DrillScheme>
      <OneOnly>F</OneOnly> 
      <NoDetail>F</NoDetail> 
      <NoDrill>F</NoDrill> 
      <DisplayOrder>A</DisplayOrder> 
    </DrillData>
  </DrillAround>
  <ParentRec>
    <QueryVal>
      <![CDATA[ _AK=yEXb  ]]> 
    </QueryVal>
    <RecAtt Action="Add">
      <AttType>C</AttType> 
      <UsrType>A</UsrType> 
      <AttName>
        <![CDATA[ Add Comment  ]]> 
      </AttName>
      <QueryVal>
        <![CDATA[ K1=0120&K2=++++20840&K3=3694823&K4=000&K5=9999&_ATYP=C&_AUDT=A&_USCH=none&_DATA=TRUE&_OPM=M&  ]]> 
      </QueryVal>
    </RecAtt>
    <RecAtt Action="">
      <CrtDate>
        <![CDATA[ 01/12/2010  ]]> 
      </CrtDate>
      <CrtTime>
        <![CDATA[ 07:47:25  ]]> 
      </CrtTime>
      <ModDate>
        <![CDATA[ 01/12/2010  ]]> 
      </ModDate>
      <ModTime>
        <![CDATA[ 07:47:25  ]]> 
      </ModTime>
      <AttSize>619</AttSize> 
      <HdrSize>95</HdrSize> 
      <AttName>
        <![CDATA[ Comment Name  ]]> 
      </AttName>
      <AttType>C</AttType> 
      <UsrType>A</UsrType> 
      <AttCreator>lawsonuser</AttCreator> 
      <AttModifier>lawsonuser</AttModifier> 
      <QueryVal>
        <![CDATA[ K1=0120&K2=++++20840&K3=3694823&K4=000&K5=9999&_ATYP=C&_AUDT=A&_KS=zz&_OPM=A&_DATA=TRUE&  ]]> 
      </QueryVal>
    </RecAtt>
  </ParentRec>
  <ErrMsg ErrNbr="0" ErrStat="MSG">
    <![CDATA[ Success  ]]> 
  </ErrMsg>
</Report>

3) This time, we have to put the URL together (I’ve highlighted the applicable lines). Take the value of the cgi-dir and the executable attributes from the Report node. Add the CDATA value from the QueryBase node. Then add the CDATA value from the QueryVal node that is in the RecAtt node with an action attribute of “” (the one at the bottom). That gives us:

/cgi-lawson/getattachrec.exe?_OUT=XML&_PDL=PROD&_FN=APINVOICE&_IN=APISET1&K1=0120&K2=++++20840&K3=3694823&K4=000&K5=9999&_ATYP=C&_AUDT=A&_KS=zz&_OPM=A&_DATA=TRUE&

The result of which is our comment detail, and it looks like this:

<?xml version="1.0" encoding="ISO-8859-1" ?> 
<Report cgidir="/cgi-lawson/" executable="getattachrec.exe" productline="PROD" filename="APINVOICE" token="Token" keynbr="KeyNbr">
  <QueryBase exepath="/cgi-lawson/getattachrec.exe">
    <![CDATA[ _OUT=XML&_PDL=PROD&_FN=APINVOICE&_IN=APISET1&  ]]> 
  </QueryBase>
  <WinTitle>
    <![CDATA[ <NULL>  ]]> 
  </WinTitle>
  <ParentRec>
    <QueryVal>
      <![CDATA[ _AK=yEXb  ]]> 
    </QueryVal>
    <RecAtt Action="">
      <AttName>
        <![CDATA[ Comment Name  ]]> 
      </AttName>
      <AttData>
        <![CDATA[ Comment Text%0A  ]]> 
      </AttData>
      <QueryVal>
        <![CDATA[ K1=0120&K2=++++20840&K3=3694823&K4=000&K5=9999&_ATYP=C&_AUDT=A&_KS=zz&_OPM=A&_DATA=TRUE&  ]]> 
      </QueryVal>
      </RecAtt>
  </ParentRec>
  <ErrMsg ErrNbr="0" ErrStat="MSG">
    <![CDATA[ Success  ]]> 
  </ErrMsg>
</Report>

The comment data is in the AttData node. Wasn’t that easy?
Note: Newlines in the AttData node will be represented by %0A.

As for which of the three methods you use, that’s up to you and your requirements. Chances are, you’ll wind up using a combination. I like running a SQL query to get the list of comments, but actually use the ListAttachments to retrieve the data. This saves on the heartache of trying to reconstruct the comment from the SQL and it limits how much work we have to do because we’ll only try to retrieve comments for those records that actually have them.

Comments and Portal
You can add a Comment popup to Design Studio and custom portal pages. This is especially useful when the users are not actually on the forms they might want to view comments for. An example of this might be in an inbasket view. You could give the users a link to view the comments on an invoice without having to leave the inbasket.
The function is:

top.portalObj.drill.doAttachment(window, "lawformRestoreCallback", idaCall, 'CMT');

The idaCall is a URL and is the same as we built in #1 above. You should leave everything else the same.

Anything else you “need” to know? Leave a comment and I’ll answer if I can.

HTH

Lawson sso part 2

Previously, I posted about Lawson sso servlet here: Lawson SSO Servlet

Mostly that post was to talk about using the LOGIN action to achieve some level of automation/integration. There are other actions as well, though probably not as useful.

The base URL is: http://lawsonserver/sso/SSOServlet?_action=

The three actions that I’m aware of are:

  1. LOGIN – Discussed previously. Used to authenticate with Lawson
  2. LOGOUT – Used to void a session. I have some question as to whether it can be used on other user names
  3. PING – Used to check the status of the session

Each action has additional parameters that can be used with it.

  • &_ssoUser = User to perform the action on. Required for LOGIN. Might work on LOGOUT. Appears to have no effect on PING.
  • &_ssoPass = Password of user. Required for LOGIN. Not used in others.
  • &_ssoUpdateSession = Reset the time remaining of the session. Can be used in PING. Works for LOGIN, but unnecessary technically. Has no effect on LOGOUT.
  • &_ssoOrigUrl = URL to redirect to after authenticated. Can be used with LOGIN or PING. Does not work with LOGOUT. I don’t understand why it wouldn’t work with LOGOUT especially if it works with PING. I wouldn’t recommend using it with PING unless you really mean to.
  • &_ssoAuthUrl = I don’t know what this does.

Examples:

LOGIN: http://lawsonserver/sso/SSOServlet?_action=LOGIN&_ssoUser=user&_ssoPass=password&_ssoUpdateSession=TRUE&_ssoOrigUrl=<URLtoRedirectTo>
LOGOUT: http://lawsonserver/sso/SSOServlet?_action=LOGOUT
PING: http://lawsonserver/sso/SSOServlet?_action=PING&_ssoUpdateSession=TRUE

HTH

Lawson – Impersonating a user

Impersonating a user – what does that mean exactly? In this case, I’m not talking about logging in as a user, but instead I’m talking of taking on certain attributes of theirs – like employee id. Quite frankly, I think it’s a security hole, but it’s also insanely useful when you’re troubleshooting.

Here’s the business case: You’ve developed a custom ESS (Employee Self Service) page (or made modifications to a Lawson delivered one). When you run it, everything works fine, but when QA tests, it bombs. Several questions immediately come up – since it runs fine for you, it’s probably not the code per se, but is it an un-handled exception or a security issue? You’ve got full security rights, QA is testing under an ESS account (restricted). To add to that, it’s related to benefits and you’re using your employee id (tied to your account via the employee service) and QA is testing with a dummy employee. So where to begin?

The easiest thing is to test under your ID (full security) and impersonate the other employee. If it works, security is the issue and you can start hunting that. If it doesn’t, it’s the employee setup (or how your code handles the employee). But how do you impersonate an employee?

The answer is that you use the Lawson delivered portalWnd.oUserProfile.setAttribute() method to update the value of the selected attribute for the current session. Once you log out (or refresh-F5) in Portal, the value reverts. So if you need to become another employee, you issue this command:

portalWnd.oUserProfile.setAttribute("employee","12345");

This will make Portal think that I am employee 12345 for the duration of my session so I can test away.

I’ve developed a simple .htm page that is saved in the /lawson/portal directory that the support and development staff have as a bookmark. The sole purpose of the page is to update your profile to be like someone else’s. The basic premise is that it reads the Profile servlet to determine all the attributes assigned to me. It then builds a page with attribute:value pairs. I simply change the value I want and click change. Simple right?

Here’s the real treat. I’m going to call today “Full Code Friday” and just give you the code. (note: this doesn’t work for everything – like RSS, and you can’t make yourself a Portal Admin this way, but most other things work).

UpdateProfile.htm :

<html>
<head>
<script language="javascript" src="../portal/servenv.js"></script>
<script>
var portalWnd=null;

function buildpage()
{
    // open in new window? or within portal?
    if (window.opener && window.opener.lawsonPortal)
        portalWnd = window.opener;
    else
        portalWnd = envFindObjectWindow("lawsonPortal");

    //Base form for display
    var str = '<form><table>';
    str += '<tr><td><h4>Attribute</h4></td><td>'+
             '<h4>Value</h4></td><td><h4>Updated</h4></td></tr>';

    try
    {
        //Make profile call and read XML response
        //Populate attribute name/value to form
        var oXML = portalWnd.httpRequest("/servlet/Profile?_PATH=/lawson/portal");
        var oDS = new portalWnd.DataStorage(oXML);
        var z = oDS.getElementsByTagName("ATTR");
        for(i=0;i<z.length;i++)
        {
            var strName = z[i].attributes.getNamedItem("name").value;
            var strValue = z[i].attributes.getNamedItem("value").value;

            str += '<tr><td><strong><label>' + strName + '</label></strong></td>'
                 + "<td><input type='text' id='" + strName + "' value='" + strValue 
                 + "' onChange='updatecheck(\"" + strName + "\",\"true\")' /></td>"
                 + "<td><input type='checkbox' name='update' id='C" + strName + "' /></td></tr>";
        }

        str += "</table></form>"
        document.getElementById("disp").innerHTML = str;  //Update page
        //alert(str);
    }
    catch(err)
    {
        alert(err.message);
    }
}

function updatecheck(varID,varDir)
{
    varID = "C" + varID;
    document.getElementById(varID).checked = varDir;
}

function updateprofile()
{
    var checks=document.getElementsByName("update");
    for (x=0;x<checks.length;x++)
    {
        var varName = checks[x].id;
        if (document.getElementById(varName).checked)
        {
            portalWnd.oUserProfile.setAttribute(varName.substr(1),document.getElementById(varName.substr(1)).value);
            updatecheck(varName.substr(1),false);
        }
    }
    alert("Update Complete");
}
</script>
</head>
<body onload="buildpage()">
    <span id="disp"></span>
    <button onClick="updateprofile()">Update Profile</button>
</body>
</html>

HTH – Happy Debugging.

Lawson AGS Caching

If you’ve done much work at all with Lawson AGS calls, you’ve no doubt noticed the _CACHE property.  Ever wonder what it does?

Simply put, if you set the _CACHE property to true, you can make subsequent AGS calls to update a single field without having to pass back all of the values on a form.  This is extremely useful for forms that have a lot of fields and you just want to change one or two.  Something like GL10 (GL Company setup) that has hundreds of fields, but you only want to change the company description.

Note: this is not necessary for all forms because some forms implicitly only require the fields you are changing (like PA52).

So how to use the _CACHE property?  First you inquire on the form with caching, get the _TRANSID that is returned to you, and then pass back the fields that you want to change.

In this example, we’re going to be using the PO25.6 screen.  For those unfamiliar with this screen, it is the Vendor Agreement Line form where we can maintain the lines of an Agreement (Vendor contract).  There are 126 detail fields on the screen.   All we want to do is execute a change on the line.  We don’t even want to change anything *.

Inquire:
_PDL=PROD&_TKN=PO25.6&_EVT=CHG&FC=I&_CACHE=TRUE&_DTLROWS=FALSE&_LFN=TRUE&_INITDTL=TRUE&PVN-PROCURE-GROUP=PROC&PVN-VEN-AGRMT-REF=GP2MS01111&PVN-LINE-NBRr0=44

Response looks like:

<?xml version="1.0" encoding="ISO-8859-1" ?> 
<XPO25.6>
<PO25.6>
<_PDL>PROD</_PDL> 
<_TKN>PO25.6</_TKN> 
<_CACHE>TRUE</_CACHE> 
<_TRANSID>1280852223366.24</_TRANSID>
<_LFN>TRUE</_LFN> 
<TC>PO25.6</TC> 
<FC>I</FC> 
<PVN-PROCURE-GROUP>PROC</PVN-PROCURE-GROUP> 
<PCG-PROC-GRP-DESC>PROCUREMENT GROUP</PCG-PROC-GRP-DESC>
...
<PO25.6>
<XPO25.6>

Change Call (to make a change to line 44 with no value changes)
_PDL=PROD&_TKN=PO25.6&_EVT=CHG&FC=C&_DTLROWS=FALSE&PVN-PROCURE-GROUP=PROC&PVN-VEN-AGRMT-REF=GP2MS01111&LINE-FCr0=C&PT-PVN-LINE-NBR=44&_TRANSID=1280852223366.24

Response:

<?xml version="1.0" encoding="ISO-8859-1" ?> 
<XPO25.6>
<PO25.6>
<_PDL>PROD</_PDL> 
<_TKN>PO25.6</_TKN> 
<_TRANSID>1280852223366.24</_TRANSID> 
<_PRE>TRUE</_PRE> 
<_f0>PO25.6</_f0> 
<_f1>C</_f1> 
<_f2>PROC</_f2> 
<_f3>PROCUREMENT GROUP</_f3> 
<_f4 /> 
<_f5 /> 
<_f6>L</_f6> 
<_f7>LINE</_f7> 
<_f8>GP2MS01111</_f8> 
...
<_f855>PROCGP2MS01111</_f855> 
<Message>Change Complete - Continue</Message> 
<MsgNbr>000</MsgNbr> 
<StatusNbr>001</StatusNbr> 
<FldNbr>_f0</FldNbr> 
</PO25.6>
</XPO25.6>

Wasn’t that easy?

*We actually use this as part of a process flow that runs every day.  We do this because we’re on 9.0 apps and when a change is made to the date on the agreement header, it doesn’t propagate to the lines.  All that’s necessary is to go to the PO25.6 and make a change on the line without changing any data (you can’t change the date anyway, it’s a calculated field), but the users seem to find this too tedious.  Imagine that.

UPDATE — Part 2 is here
HTH

Lawson SSO servlet

If you want to retrieve data from Lawson without having to log in, you can use the SSO servlet. Keep in mind that you will still have to authenticate, but you won’t be re-directed to the log in screen.

This is extremely helpful when integrating Lawson with other applications. I use this technique in ImageNow to lookup information in Lawson. I use a DME call as opposed to an SQL statement because there are conditions on the table that I want to take advantage of.

The basic URL is:

http://<server>/sso/SSOServlet?&_ssoUpdateSession=TRUE&_action=&_ssoUser=&_ssoPass=&_ssoOrigUrl=

This will send an authentication request to the Lawson server and authenticate with the username and password provided. Once authentication is complete, it will execute the url that appears after _ssoOrigUrl. You MUST escape the characters in the _ssoOrigUrl.

A full URL would look like this:

http://lawson.mycompany.com/sso/SSOServlet?&_ssoUpdateSession=TRUE&_action=&_ssoUser=test&_ssoPass=test&_ssoOrigUrl=/servlet/Router/Data/Erp?FILE=PURCHORDER%26XKEYS=FALSE%26INDEX=PCRSET1%26XCOUNT=FALSE%26MAX=1%26XIDA=FALSE%26KEY=10%3DCAPT%3D9500135%3D%26FIELD=VENDOR%26PROD=PROD%26COND=MATCHABLE%26XRELS=FALSE%26OUT=XML%26XCOLS=TRUE

This looks up the vendor number for an unmatched PO.

HTH

Lawson Design Studio : Manipulating Form Elements

Manipulating Form Elements – what does this mean? Basically, it’s changing the HTML of the page. Lawson provides several functions in Design Studio (DS) to set the value of an element, but what happens when you want to change the element itself? A good example of this might be on a screen with a date, like AP20. If a user keys the invoice date in the future, you may want to change the text to red so it stands out. But how?

At the end of the day, Lawson Portal is HTML and Javascript. Highly convoluted, but it still follows the same rules as any other web page. In order to perform actions on the form elements, we have to manipulate the DOM directly. (see this primer if you’re unfamiliar with DOM). The design studio (and all portal forms) are actually an XML document that the Portal renders into HTML. This is what gets most people because they forget that there is an intermediate step. What this means is that in order to access a document element via the DOM, you have to know the real name or ID of the element, not the one that is assigned in DS (very often they are the same, but not always). In my experience, when they are different it is a reference to the element type prepended to the DS id (but don’t quote me on that). A drop down list with the id “_l197” would have the id of “VALUES_l197” in the DOM. The best way to find the actual ID is to use a browser that has debugging, like IE8.

Let’s get to the fun stuff. I’m going to give an example that we actually have in production, so it’s going to cover a lot and be fairly involved. With any luck, I might pass on a couple of new things and not confuse anyone.

The form in this case is PA42 – Job requisition. The business requirement is to have a drop down list of the recruiters names that users can select from. When it is selected, it should populate the contact fields. Luckily, HR tracks recruiters with a User Field (50) that they set to ‘Y’ if the employee is a recruiter.

High level overview:

  1. Make DME call to get list of Recruiters
  2. Populate List
  3. add function to the OnInit() event of the form

Please note that the code comments have been added to aid understanding.

function DropDownFill()
{
  //DME call to get Recruiters
  var strDME = "?PROD=" + portalWnd.oUserProfile.getAttribute("ProductLine") +
    "&FILE=HREMPUSF&FIELD=EMPLOYEE.EMPLOYEE;EMPLOYEE.FIRST-NAME;EMPLOYEE.LAST-NAME;" +
    "&INDEX=HEUSET3&KEY=50=Y=10==&XCOLS=TRUE&XKEYS=FALSE&XRELS=TRUE&XCOUNT=FALSE" +
    "&XIDA=FALSE&OUT=XML&MAX=1000&SORTASC=EMPLOYEE.LAST-NAME";

  //Make DME call and return to Portal storage
  var httpDME = portalWnd.httpRequest(portalWnd.DMEPath + strDME);
  var dsDME = new portalWnd.DataStorage(httpDME);

  //We use try/catch here in case call returns no results.  
  //If the "record" element doesn't exist, we don't want a js error on the page.
  try {
    var Recs = dsDME.document.getElementsByTagName("RECORD");
    if (Recs.length == 0)
    {
      alert("No records");
      return false;
    }
  }
  catch(err)
  {
    alert("System Error. " + err.message);
    return false;
  }

  //Read DME result set and populate to list
  for (var i=0; i&lt;Recs.length; i++)
  {
    arrCOL = Recs[i].getElementsByTagName("COL");
    var strName = arrCOL[1].firstChild.nodeValue + " " + 
      arrCOL[2].firstChild.nodeValue;
    //Get our list element
    var List = document.getElementById("VALUES_l172");
    //create a new element to populate our values to, 
    //then append the span element to the list
    var span = document.createElement("span");
    span.setAttribute("text", strName); // long description
    span.setAttribute("tran", arrCOL[0].firstChild.nodeValue); // transaction value
    span.setAttribute("disp", strName); // display value
    List.appendChild(span);
  }
}

To update the page when a user selects a value in the list, create an onBlur event for the values element and use the Lawson provided setFormValue, to update the contact information.

function VALUES_OnBlur(id, row)
{
  if (lawForm.getDataValueById("select2") != "")
  lawForm.setFormValue("text102",lawForm.getDataValueById("select2"));

  return true;
}

HTH

Searching in Javascript arrays

I have a problem.  Namely, that I work with Lawson Portal which requires IE.  There are times when writing a custom HTML or Design Studio page when I store data in Arrays and then have to look up data from them later.   Since IE doesn’t support the .indexOf for Arrays, how to find the info I need?  When dealing with large lists of data, using loops just isn’t feasible, especially if I’m going to use the Array a lot.  I could prototype the indexOf feature, but I have an easier way (depending on the data).

An example of this is what I’m working on currently.  Our Finance department doesn’t want to give security access to the Lawson AP90 screen (Invoice lookup) to everyone in the enterprise because there are sensitive invoices (Legal bills, Employee re-imbursements, etc.), and there’s no good way to secure them.  It’s a legitimate concern, but the problem is people need invoice information to manage and they shouldn’t have to wait on AP or Finance to get their information.

Luckily, our Finance department maintains attributes on each Accounting unit for Manager, VP, etc. that contains the user name. (For those of you who are not Lawson users, an Accounting unit is roughly equivalent to a department code). The most efficient solution is to only let users see invoices that are related to their department by creating an object that acts like an array of Accounting units when the page loads, and then comparing the Accounting Unit(s) for the requested invoice to the Object to determine whether to display or not.  So this brings us to the question of “How do I search a javascript Array effectively?”.

The answer is use an Associative Array — which javascript does not support (technically). What we actually do is to assign properties to javascript’s Object.
(If you want to prototype the indexOf, see this link.)

Say we have a list of values like:  100000,100001,115000,950000.

Normally we would do something like:

var arrAU = new Array(100000,100001,115000,950000);
function lookupAU(strAU) {
  for (var i=0;i<arrAU.length;i++)
  {
    if(arrAU[i] == strAU)
      return true;
  }
    return false;
}

But what happens when we hit the VP or President level and there are hundreds or thousands of Accounting Units?

Instead we set the Object property to the Accounting Unit and set the value to some small value (because the value is irrelevant to us).

var arrAU = "";
arrAU['100000'] = true;
arrAU['100001'] = true;
arrAU['115000'] = true;
arrAU['950000'] = true;

function lookupAU(strAU) {
  if (arrAU[strAU] != undefined)
    return true;
  else
    return false;
}

So the answer to how to search arrays is that we don’t use arrays. The result is that we don’t waste the resources on the loop, and we get right to what we want in an IE supported manner. Please keep in mind that the .length property will not return the length because this is not an Array.

HTH

For those of you who use Lawson and would like to know more about the full process, feel free to leave a comment and I’ll be happy to share.