Saturday, October 25, 2014

How to activate a custom menu item

In the GESAWI function, Buttons/menus tab, you can add custom menu items to any Sage X3 window like this:


After you validate the window (and the entry transactions, if it is an object managed with entry transactions) you will see your custom menu item when you open the window, but it will always be grayed out. That is the default. To activate it, you need a bit of custom programming.

In the specific processing file for your window you must handle the SETBOUT action and put the following code in the action handler:

$SETBOUT
CHMEN += "O"
Gosub SET_BOUT_SPE From GSAISIE
Return

The principle is this: You add the codes of the menu items that you want to activate to the CHMEN variable and then call the SET_BOUT_SPE standard function. You use only the second letter of the code - in the above screenshot you can see that the actual code of our custom menu item is DO, but we only add O to CHMEN.

You can add more than one code at once. For example, if you have a custom menu item with code DO (as shown above) and another one with code DP, you can add them by saying

CHMEN += "OP"


And, of course, you can have an If statement and only activate a menu item when certain conditions hold.

When adding menu item codes to the CHMEN variable, be careful to use += and not =.

Tuesday, October 21, 2014

How to lock out a user on unsuccessful login

To boost the security of Sage X3 and prevent brute-force attacks, you might want to lock out a user after three unsuccessful login attempts. Here is how to do it, using workflow and a helper table:

First we will create the helper table. We do this in the GESATB function:

Fields tab:

Index tab:


This table will hold the number of unsuccessful login attempts for each user.

Next we need an action that will be triggered by the workflow. We create it in the GESACT function:


Do not forget to mark the Workflow checkbox!

Now we go to the Workflow rules function (GESAWA) and create the following workflow:

General tab:

Recipients tab:
Only one row. All fields should be empty or have the value of "No", except for Type and Recipients.

Action tab:

And the last thing we need to do is create the standard processing file for our ZPASS action. So we go to the editor (function ADOTRT) and create the ZPASSSTD file with this code:

Subprog PASS()
If GERR=6
    If clalev([ZPS])=0 : Local File ZPASS [ZPS] : Endif

    Read [ZPS]ZPS0=GUSER
    If fstat=5
        Raz [ZPS]
        [F:ZPS]USER=GUSER
        [F:ZPS]COUNT=1

        Write [ZPS]
        If fstat
            Infbox("Error while writing [ZPS], fstat="+num$(fstat))
        Endif
    Elsif fstat
        Infbox("Error while reading [ZPS], fstat="+num$(fstat))
    Else
        If [F:ZPS]COUNT>=3
            If clalev([ZAUS])=0 : Local File AUTILIS [ZAUS] : Endif
            Update [ZAUS] Where USR=GUSER With ENAFLG=1

            Delete [ZPS] Where USER=GUSER       
        Else
            [F:ZPS]COUNT+=1

            Rewrite [ZPS]
            If fstat
                Infbox("Error while writing [ZPS], fstat="+num$(fstat))
            Endif
        Endif
    Endif
Elsif GERR=0
    If clalev([ZPS])=0 : Local File ZPASS [ZPS] : Endif
    Delete [ZPS] Where USER=GUSER       
Endif
End

And here is how it all works:

The workflow that we created is of type Miscellaneous and has the CON event code. This means that the workflow will be triggered on each connection to the application. The GERR global variable will be set to 6 if the provided connection password is incorrect (you might need to debug in order to see which is the right value of GERR - my Sage X3 documentation says that GERR is set to 1 when the password is incorrect, but in my case it turned out to be actually 6). The workflow runs the ZPASS action, which means that the code we placed in the ZPASSSTD file is executed. This code uses our ZPASS helper table to check if the user has three consecutive unsuccessful login attempts due to incorrect password, and if so, locks the user out by deactivating his record in the AUTILIS table (setting the ENAFLG field to 1).

Once locked out, the user cannot connect to X3 even with his correct password. An administrator will have to re-enable his record in the GESAUS function. And if the administrator locks himself out, he will have to change his record in the AUTILIS table (set the ENAFLG field to 2) directly in the database.

Tuesday, October 14, 2014

How to manually unlock a code file

When you open a code file in the Sage X3 internal editor (function ADOTRT), Sage X3 locks the file to prevent simultaneous modification. When you close the file, the lock is released. But if you experience an unexpected exit from the system while you have a file opened in the editor (for example, due to power failure), the file lock might not be released. In this case, when you open the file again, you will see a warning message and the file will be in read-only mode.

In such situations, to release the lock manually you have to delete a temporary file that X3 creates to mark the code file as locked. This temporary file has the same name as your code file and an extension of 'LCKsrc'. For example, if you open the SPEPOH code file, X3 will create a temporary file called 'SPEPOH.LCKsrc'.

The temporary file is created in the same directory as its corresponding code file. This is usually the TRT subdirectory of your X3 folder. To release the lock, all you have to do is delete the temporary file (be careful not to delete the .src and .adx files of the same name!)

Saturday, October 11, 2014

How to start a manual workflow programmatically

A manual workflow is usually started - you guessed it - manually, using the SAIWRKMAN function. But if you need to start a manual workflow programmatically, here is one way do it:

Local Char PARAM(GLONAWA)(1..3)
PARAM(1)="XXX"
PARAM(2)=""
PARAM(3)="1"

SAVGSERVEUR=GSERVEUR
GSERVEUR=2

Gosub INIT From SAIWRKMAN
Gosub CONTROLE From SAIWRKMAN
Gosub EXEC From SAIWRKMAN

GSERVEUR=SAVGSERVEUR

In the above code, XXX should be replaced by the name of the manual workflow that you want to run.

Keep in mind that depending on where you use it, this might change the values of some of your significant global variables and/or modify the masks and files by default. If such errors occur, you should program around them. For example, you can save the values of the affected global variables before running the above code and restore them afterwards.

Thursday, October 9, 2014

What are the GLON variables and how to use them

Every data type with alphanumeric internal type has an assotiated length. Let's take for example the Product reference data type ITM. Suppose it has a length of 20. So if you want to create a varibale that will take values of type ITM, you might write this:

Local Char WITMREF(20)

But like almost everything in X3, the length of an alphanumeric data type can be personalized. Also, it might change between versions of the software. If at some point in the future the length of the ITM data type changes to a value greater than 20, the above code will break.

So you might think of giving the variable a crazy upper limit:

Local Char WITMREF(200)

This will work for all practical purpoces, but it is a waste of resource, especially when you have a better alternative. This alternative comes in the form of the GLON variables.

The GLON variables are global variables maintained internally by X3. The name of each one starts with GLON and ends with the abbreviation of the corresponding alphanumeric data type. So for the ITM data type the corresponding GLON variable is GLONITM. The value of each GLON variable is the length of the corresponding data type. This value is updated on data type validation, so even if you customize your data type lengths or they change between versions, the GLON variable is guaranteed to hold the correct value.

And here is the proper way to define an ITM data type variable:

Local Char WITMREF(GLONITM)

Replacing ITM with another data type abbreviation, this will work for all other alphanumeric data types as well.

Saturday, October 4, 2014

How to impose custom password validation rules

If you want to create advanced password validation logic, Sage X3 has an entry point dedicated to this. To show how it works, we will create a rule that will reject a password not containing at least one digit.

The entry point we are going to use is called CTLPASSE and is located in the PASSE process. So first you need to go to your Entry points function (GESAPE) and declare the PASSE process like this:


ZCTLPASS is the name of the file that will contain the entry point code. Of course, you can give it another name. Here is the code for this file:

$ACTION
Case ACTION
    When "CTLPASSE" : Gosub CTLPASSE
Endcase
Return

$CTLPASSE
If !pat(PASSE, "*#*")
    Infbox("Error: The password must contain at least one digit.")
    GPE=1
Endif
Return

In the entry point, we are passed the new password in the PASSE variable. To reject the new password, we must set GPE to 1. In the above example, we do this when the password does not contain at least one digit. We check for this condition by pattern matching using the pat instruction. This is just a simple check, but the checks can be as elaborated as you want, so you can create any number of advanced password validation rules.

You might need to logout of X3 and login again before the entry point becomes active.

Friday, October 3, 2014

How to find the window a screen belongs to

In any opened X3 screen you can find the name of the screen by simply entering any of its fields and pressing F6. This pops up the field info, part of which is the screen name. But how about the window this screen belongs to? Probably there is some offical method to do this, but as I am not aware of it, here is a hacky but effective way to find the window name.

When you have your screen opened, in the top menu you select ? > Diagnostic help > Window description. This opens the XML description of the window. You are only interested in the beginning of the second line of the XML. For example, in the Products function (GESITM) the file starts with:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<WIND NAM="OITM"    ... ... ...


So the very first attribute of the very first tag is the window name you are looking for.

Of course, for a standard object management function like GESITM you already know that the window name is most probably going to be "O" plus the object code. But the above technique might prove useful in case you need the window name of a screen that does not conform to this convention.

Thursday, October 2, 2014

How to implement a birthday reminder - Pt. 2

In the first part of this tutorial we created the screen, window and actions that we need for our birthday reminder. Now we continue with a bit of custom 4GL programming.

In the actions that we created (ZBRD and ZBRDAY) we have standard processing files. Let's write the code for these files.

We create the file ZBRDSTD (standard processing file for the ZBRD action) and put the following code in it:

$ACTION
Case ACTION
    When "OUVRE" : Gosub OUVRE
    When "DEBUT" : Gosub DEBUT
Endcase
Return


$OUVRE
If clalev([AIN])=0 : Local File CONTACTCRM [AIN] : Endif
Return


$DEBUT
For [AIN] Where day(CNTBIR)=day(date$) & month(CNTBIR)=month(date$)
    [M:ZBRD]CNTFNA([M:ZBRD]NBLIG)=[F:AIN]CNTFNA
    [M:ZBRD]CNTLNA([M:ZBRD]NBLIG)=[F:AIN]CNTLNA

    [M:ZBRD]NBLIG+=1
Next

Affzo [M:ZBRD]
Return

In the opening OUVRE action we make sure that the CONTACTCRM table is opened. That is the Contacts (relationship) table. Then, in DEBUT we loop through the records in this table for whom the day and month of their birthday (CNTBIR field) is equal to today's day and month (date$ always contains today's date). After that we display the whole screen with the Affzo instruction.

Next we create the file ZBRDAYSTD (standard processing file for the ZBRDAY action) and put the following code in it:

Subprog BRDAY()
If clalev([AIN])=0 : Local File CONTACTCRM [AIN] : Endif

Filter [AIN] Where day(CNTBIR)=day(date$) & month(CNTBIR)=month(date$)

If rowcount([AIN])>0
    Filter [AIN]

    Local Char SAVACT(GLONACT)
    Local Integer FLGEXE
    SAVACT=GACTION
    Local Char VALBOUT(250), PARBOUT(250)(1..20)
    FLGEXE=1
    GACTION="ZBRD"
    Call SAISIE_CHAR(VALBOUT, PARBOUT, "ZBRD", "ZBRDSTD", "") From GSAISIE
    GACTION=SAVACT
Else
    Filter [AIN]
Endif
End

In this file we filter the CONTACTCRM table to check if we have some contacts whose birthday is today. If we do, we setup and use the SAISIE_CHAR function to call the ZBRD action. As you remember, we cannot use an action that displays a window in a workflow, so we use this two-step procedure - we have an action that is going to popup the birthday reminder on the screen and another action to call the first one. So that is what we just programmed.

The last thing left to do is setup the workflow. So we go to the Workflow rules function (GESAWA) and create and validate the following workflow:

General tab:

Recipients tab: Create only one line, leave all fields empty or with the value of No, except for Type and Recipients:

Action tab:

As you can see, the workflow has Event type: Miscellaneous and Event code: CON. This means that it is going to be executed whenever a user connects to X3. And we have checked the Trigger action checkbox, so the workflow will run the action that we specify in the Action tab - which is our ZBRDAY action, which is going to run our ZBRD action, which is going to display the birthday reminder popup. Maybe a bit complicated... but it works like a charm!

So now when we login to X3, if there are business contacts whose birthday is today, we will see something like this:


Of course, other fields can be added as well - for example, a phone number, so that we can call the contact right away and say Happy birthday!