Friday, February 13, 2015

Use an automationscript in a Condition node of a Workflow

Goal:
Use an automationscript in Maximo to calculate the outcome of a condition node in Maximo. 

Example:
I have a record with zero or more child records. e.g. a  Workorder with Task workorders. I also have a workflow and I want the Workorder only to be able to go in to the workflow if the Tasks all have the same Location.

Problem:
Whith the condition nodes in Workflow you can set an Expression. But how can I loop through task workorders within an expression? That would make a difficult expression. Looping through records (and doing a lot of other stuff) is much easier in an automation script. How can I use the best of both worlds?

Solution:
So I need to call an automationscript from a workflow. First I set up my basic workflow. For more information about that, see one of my previous posts, with information on setting up a basic workflow.
http://sometimesiliketopretend.blogspot.com/2014/10/maximo-75-my-first-workflow.html

My workflow:




When starting the workflow, the check is performed, and when not satisfactory a message is shown to the user and the workflow is stopped.

The condtion now needs to call my automation script.

Create the script on a Custom Condition Launchpoint:



I want the script to run on the workorder object:



Name the script:



Paste in the source code for the script and click the [Create] button







Source code script:

print "Start CHECKTASKLOC script";

from psdi.mbo import Mbo;
from psdi.server import MXServer;

'''
Date: 20150211
Author: G.N. Zomerdijk

Purpose of script:
Check if the location on all tasks are the same

The script will be run from a condition node in the Workflow

Custom Condition Launchpoint: WOFLOW on WORKORDER object

Useage in Workflow:
Condition Node with title: CHECKTASKLOC:WOFLOW   That is the <script name>:<launchpoint name>
Expression:   com.ibm.tivoli.maximo.script.ScriptCustomCondition
Custom Class: Y

For reference when we want to use it in a Condition:
Create the following condition:
Condition: TEST
Type: CLASS
Expression: CHECKTASKLOC:WOFLOW 
Condition class: com.ibm.tivoli.maximo.script.ScriptCustomCondition

Info:
Expression should hold launch point information: <script name>:<launchpoint name>

evalresult is returned, is an internal variable, no need to define it as variable in automationscript application.
'''

#set values for used variables
evalresult = True;
loc = "";

#Loop through the PRLINES
tasksFromWOSet = mbo.getMboSet("WOACTIVITY")
tasksFromWO = tasksFromWOSet.moveFirst()

while tasksFromWO is not None:    
    if (loc != "" and loc != tasksFromWO.getString("LOCATION")):
        evalresult = False;
        break;
    else:
        loc = tasksFromWO.getString("LOCATION");

    #get the next task
    tasksFromWO = tasksFromWOSet.moveNext();

print "evalresult: " + str(evalresult);

print "End CHECKTASKLOC  script";


The script returns a True or a False. This is done by setting the evalresult variable. See the code above for more detailed information.

Now in the workflow we can run the script by modifying the Condition Node.

Condition Node with title: CHECKTASKLOC:WOFLOW   That is the <script name>:<launchpoint name>
Expression: com.ibm.tivoli.maximo.script.ScriptCustomCondition

Custom Class: Y




After clicking OK I get:



Click OK in the error and Click OK again in the condition node. Do not know why the error occurs, but the second time around it is validated. Maybe because the Custom Class is checked, and only at the second run the Expression is validated as a Custom Class.

The workflow looks like this now:


Save the Workflow.

Next I associate the Workflow with the Workorder (WOTRACK) application




I only add support for the WOTRACK application, so deselect the rest of the application and click [OK]



Next Validate, Enable and Activate the process:








Create a workorder, with 3 task workorders.




On one of the tasks change the location from FOORBAR to FOOBAR2 and save the workorder.



Now I want to route the workorder in the workflow, but the button is grayed out... 



I go back to the workflow and go in the Select Action menu to the "Edit Workflow GO Buttons" 

There I add the Process Name to the existing Application row.



I save the workflow and open my workorder again.
Still grayed out... I decide to now logout maximo and log back-in.
Open the workorder again and the button is now available!



And now I get the message:


I change location on the second task to match the other tasks, save the workorder and hit the workflow button again.


Now the record is routed in the workflow: 


GNZ


Reference:
For more info about automationscripting I used the pdf from this page: https://www.ibm.com/developerworks/community/blogs/a9ba1efe-b731-4317-9724-a181d6155e3a/entry/scripting_with_maximo6?lang=en

For more information about workflow I used this "Workflow Implementation Guide": http://publib.boulder.ibm.com/infocenter/tivihelp/v49r1/topic/com.ibm.mbs.doc/pdfs/pdf_mbs_workflow.pdf

Saturday, February 7, 2015

WP App part 4 - "the Manual Automatic" - Saving and Loading images from and to a byte array

Goal:
Create the functionality to use images from the phone / take image with my phone for my Windows Phone App 'The Manual Automatic" 


Previous parts:


First setup in part 1:
http://sometimesiliketopretend.blogspot.com/2014/09/windows-phone-app-part-1-manual.html

The setup of the DataModel in part 2:
http://sometimesiliketopretend.blogspot.com/2014/09/wp-app-part-2-manual-automatic-setting.html

Using the FilePicker in part 3:

http://sometimesiliketopretend.blogspot.com/2014/11/wp-app-part-3-manual-automatic-use.html

It has been a little while since I posted about my project of developing the Manual Automatic app for Windows Phone. But a lot of work has been done since then. So now first I will show how I managed to store images in my Json datamodel.


Part 4 - Saving and Loading images in JSON

I want the images to be stored and not just referenced if all possible. That way it isn't lost if you delete the picture from your camarea library, because you might not want to have all those pictures of pages in your library.

So it would be great if the image can be stored in json.


Step 1 - Converting the image to a byte array and saving it
I want to store the picture in a array of ytes, so in my datamodel it looks like this:

    public class Manual
    {
        public int ID { get; set; }
        public string ManualName { get; set; }
        public string Description { get; set; }
        public DateTime Created { get; set; }
        public DateTime Changed { get; set; }
        public byte[] Image { get; set; }
    }

I have a byte array named Image.
After selecting the image with the FilePicker (see previous part) the WriteableBitmap originalBitmap variabele and the byte[] buffer variabele are filled. I used the originalBitmap to bind it to the Image control on the XAML.

private async void OnFilesOpenPicked(IReadOnlyList<StorageFile> files)
{
    // Load picked file
    if (files.Count > 0)
    {
        // Check if image and pick first file to show
        var imageFile = files.FirstOrDefault(f => SupportedImageFileTypes.Contains(f.FileType.ToLower()));
        if (imageFile != null)
        {
            using (var stream = await imageFile.OpenReadAsync())
            {
                originalBitmap = await new WriteableBitmap(1, 1).FromStream(stream);
                RandomAccessStreamReference rasr = RandomAccessStreamReference.CreateFromStream(stream);
                var streamWithContent = await rasr.OpenReadAsync();
                buffer = new byte[streamWithContent.Size];
                await streamWithContent.ReadAsync(buffer.AsBuffer(), (uint)streamWithContent.Size, InputStreamOptions.None);
            }
                    
            Image.Source = originalBitmap;

        }

    }
}


The buffer needs to be stored.
Since that allready is a byte[] I can simply store it in my DataModel.

So on the saveButton click event I do some checks and then call

App.DataModel.AddManual(manualNameTextBox.Text, manualDescriptionTextBox.Text, buffer);


There I set the buffer to the manual.Image ad call the sveManualDataAsync(); function

manual.ManualName = manualname;
manual.Description = description;
manual.Created = DateTime.Now;
manual.Changed = DateTime.Now;
manual.Image = buffer;

_manuals.Add(manual);


await saveManualDataAsync();

There the data will be save, see the previous post for more details.


Step 2 - Loading the Image and converting it back from byte[] to WritableBitmap.

From the Main page, where the user can search for records, they can click on the results and then it neds to open on a seperate page. On that page I want to show detailed record information and also the image. So there the byte{} needs to be converted back to an image.


On the Main page I use the code below to generate results;

private void genResult(DataModel.Manual manual)
        {
            TextBlock NameText = new TextBlock();
            NameText.Text = "Manual Name: " + manual.ManualName;
            NameText.FontSize = 22;
            //Store the manual in the Tag so we can pass it along
            NameText.Tag = manual;
            NameText.TextTrimming = TextTrimming.CharacterEllipsis;        
            NameText.Tapped += new TappedEventHandler(NameClickedEvent);
            
            
            TextBlock DescText = new TextBlock();
            DescText.Text = "Description: " + manual.Description;
            DescText.FontSize = 12;
            DescText.TextWrapping = TextWrapping.WrapWholeWords;

            TextBlock CreateText = new TextBlock();
            CreateText.Text = "Created on: " + manual.Created;
            CreateText.FontSize = 12;
            
            TextBlock IDText = new TextBlock();
            IDText.Text = "ID: " + manual.ID;
            IDText.FontSize = 12;

            TextBlock ImageStringText = new TextBlock();
            ImageStringText.Text = "ImageString: " + manual.ImageString;
            ImageStringText.FontSize = 12;

            
            resultStack.Children.Add(NameText);
            resultStack.Children.Add(DescText);
            resultStack.Children.Add(CreateText);
            resultStack.Children.Add(IDText);
            resultStack.Children.Add(ImageStringText);


        }

In the NameClickedEvent I navigate to the RecordPage

private void NameClickedEvent(object sender, TappedRoutedEventArgs e)
        {
            //Get the manual from the textblock tag and pass it along to the ReordPage
            TextBlock textBlock = sender as TextBlock;
            DataModel.Manual manual = textBlock.Tag as DataModel.Manual;

            Frame.Navigate(typeof(RecordPage), manual);
        }

On the RecordPage in the OnNavigatedTo I do the following:

 protected async override void OnNavigatedTo(NavigationEventArgs e)
        {
            //get the vars and get the one manual
            var manual = (DataModel.Manual)e.Parameter;
            
            
            //should not occur that the passed manual is empty but check anyway
            if (manual != null)
            {
                ManualNameTextBlock.Text = manual.ManualName.ToString();
                CreatedTextBlock.Text = CreatedTextBlock.Text + manual.Created.ToString();
                DescriptionTextBlock.Text = manual.Description.ToString();

                if (manual.Image != null)
                {
                    MemoryStream _stream = new MemoryStream(manual.Image);
                    ManualImage.Source = await new WriteableBitmap(1, 1).FromStream(_stream);
                }

                //Show a blank Image if no image is returned
                if (ManualImage.Source == null)
                    ManualImage.Source = ImageFromRelativePath(this, "Assets/empty.png");  
               
            }
            else
            {
                //show a message that searchterm is required
                string messageText = "Something went wrong, please go back and re-select a Manual";
                MessageDialog msgBox = new MessageDialog(messageText);

                await msgBox.ShowAsync();
            }


        }

So the byte[] is converted back to in WriteableBitmap and set to the source of the Image.

But an error occurs when I do just that:




An exception of type 'System.Exception' occurred in mscorlib.ni.dll but was not handled in user code

Additional information: The component cannot be found. (Exception from HRESULT: 0x88982F50)


If there is a handler for this exception, the program may be safely continued


I have no idea why this happens, and after searchig online for hours I still can't get it to work.
I use the same loading image from byte[] method to show the image after picked by the file picker and no error occurs. But when first storing it in the json file and the retreiving it messes it up...

At some point it worked, but after a few mote tests it broke again... Sounds stupid, but I cannot find a way to reproduce the corect working again... Something must be off.


After testing and searching I realize that this maybe isn't the best way to store images. In one of my tests I crate maybe 10 records with images and the json files grew to 11MB. So I van only imagine how big the files becomes after a while...
This will not be the correct way I gues

So i decide to not store the images in the json file. I now want to store a copy of the images in a folder in the Localfolder of the app. 

Once that is done I will post about it in a new blog post.

GNZ