Maximo

 View Only

Enforcing Qualifications in Maximo

By Steven Shull posted Thu December 15, 2022 07:39 PM

  

NOTE: This is provided as-is without any official support from IBM. 

Core Maximo has had qualifications that can be associated to labor records before I even used the product. Out of box though, it's not really used to ensure that the Labor record is qualified to perform the maintenance. We enhanced qualifications in Transportation & HS&E/O&G to enable compliance but neither really quite addressed the use case well in my opinion. Inspired by a customer and the following idea (https://ibm-ai-apps.ideas.ibm.com/ideas/SCHEDULE-I-27), I set out to figure out how I could make qualifications enforceable in core Maximo.

HS&E has the ability to associate qualifications to assignments and planned/actual labor but it's a 1:1 mapping to qualifications. Work orders might need to require multiple qualifications. When you add additional planned labor you'd also need to remember to add the qualifications.

Transportation has the ability to associate multiple qualifications and it's done at a higher level than planned labor/assignments. Unfortunately, it requires that qualifications are tied to the WO tasks. For most organizations, the qualifications don't change between tasks on a work order so tracking that granularly is overkill. On Job Plans, we required that the job task referenced an ORGID to associate a qualification. This makes sense since Qualifications are at an Organization level, but that limits an organization's ability to re-use a Job Plan across organizations if they wanted to enforce qualifications. 

In both products, we ask the administrator to configure what should happen at a global level. O&G/HS&E it's enabled or disabled. When enabled you can't pick a labor record that doesn't match the qualification. While this makes sense, it's possible someone who wasn't qualified performed the work. Preventing them from being able to record their time means that you don't have accurate information in the system.

Transportation provides qualification options, but it allows for unqualified labor to perform the work even if set to ERROR. Our description on the MAXVAR for Actual Labor Qualification validation explains it pretty well "Specifies whether, at the time the work order is completed, the system should display a warning message, an error message, or take no action (NONE) if actual labor does not meet the qualification requirements.". Essentially, we only display the message when you try to complete the work. 

Before I get into the technical "how", I wanted to discuss what I did from a functional perspective. 

On Job Plans, Job Tasks, WO, & WO tasks I enabled a user to associate multiple qualifications to that record. For each of these objects we have a new persistent field (ISMQUALIFICATION) that will display NONE, MULTIPLE, or the name of the qualification if only one qualification is associated. 

When you open the dialog, it'll vary a bit based on whether you're on the Job Plan/Job Task or Work Order/WO Task.  Since Job Plan can be at a System Level, we allow the user to provide the ORGID when there is no ORGID on the JOBPLAN. They can provide entries for each organization when the job plan is at a System level. We'll evaluate when the job plan is applied to copy over only the relevant Qualifications for that organization. When an ORGID does exist on the JOBPLAN we'll set the ORGID and make it read-only.  


On Work Order/Tasks, we need to evaluate these qualifications against Assignments, Planned Labor, & Actual Labor so we add some additional non-persistent fields for tracking that compliance. We also support re-evaluating this compliance in case certifications may have been added/revoked since the system evaluated that record. 



Exception type is a key difference in our approach over the previous Industry solutions. For every qualification on every WO/Task, you are in control of how we should handle it. If you say that the exception type is ERROR, we'll prevent the user from assigning, planning, or recording actual work without that qualification. This allows you to block when the requirement is absolutely critical. If you choose to utilize WARNING, we'll allow it but display a message to the user when the record is saved. If you choose SILENT, we won't display anything to the user. For both WARNING & SILENT we track that there was a discrepancy on the WPLABOR/ASSIGNMENT/LABTRANS record in a new attribute we added (ISMQUALIFICATIONMET).

The ISMQUALIFICATIONMET attribute will display NA when no qualification is required or qualification can't be evaluated (such as a planned labor at the craft level without a laborcode). It will display ALL if the labor met all qualifications required for the record. It will display PARTIAL if they met some of the qualifications but not all of them. And it will display NONE if they met zero of the qualifications required for that record. 


I wanted to make sure a customer could extend our capability into new scenarios that we haven't thought about yet. For example, one of the feedback items I heard working with my customer is that the user executing the status change to complete might be required to have the qualification. To support this, I added two system properties (ismwoqual.validateCurrentUser & ismwoqual.validateCurrentUser.exceptionLevel) and a non-persistent attribute (ISMQUALVALIDATECURUSER) to the WORKORDER/WOACTIVITY objects. If you ensure these system properties are configured and set the non-persistent attribute to true via another automation script, we'll evaluate the qualifications in context of that user record. 


If you're interested in deploying this in your own environment, I've uploaded a DBC file for the schema changes here: https://community.ibm.com/community/user/asset-facilities/viewdocument/maximo-qualifications-dbc-file?CommunityKey=ed77c224-45e2-47b0-b574-cc31496f9a41&tab=librarydocuments

I added three new Menus that needs to be added to the MENUS.xml (Export System XML from Application Designer)

	<menu id="ISMJPQUALREQ" image="btnicon_matchlabortowork.gif" label="Qualifications">
		<menuitem event="ismviewjpqualreq" id="ismjpqualreq_0" image="btnicon_matchlabortowork.gif" label="Manage Qualifications"/>
	</menu>
	<menu id="ISMWOQUALREQ" image="btnicon_matchlabortowork.gif" label="Qualifications">
		<menuitem event="ismviewwoqualreq" id="ismwoqualreq_0" image="btnicon_matchlabortowork.gif" label="Manage Qualifications"/>
	</menu>
	<menu id="ISMWOTASKQUALREQ" image="btnicon_matchlabortowork.gif" label="Qualifications">
		<menuitem event="ismviewwotaskqualreq" id="ismwotaskqualreq_0" image="btnicon_matchlabortowork.gif" label="Manage Qualifications"/>
	</menu>


I added three dialogs to the LIBRARY.xml (Export System XML from Application Designer)

	<dialog id="ismviewjpqualreq" label="Qualification Requirements">
		<table id="ismviewjpqualreq_table" relationship="ISMJPQUALREQ">
			<tablebody id="ismviewjpqualreq_table_tb1">
				<tablecol dataattribute="orgid" id="ismviewjpqualreq_table_tb1_tc1" lookup="org"/>
				<tablecol applink="qual" dataattribute="qualificationid" id="ismviewjpqualreq_table_tb1_tc2" lookup="QUALS" menutype="normal"/>
				<tablecol dataattribute="exceptiontype" id="ismviewjpqualreq_table_tb1_tc5" lookup="valuelist"/>
				<tablecol id="ismviewjpqualreq_table_tb1_tc6" mxevent="toggledeleterow" mxevent_desc="Mark Row for Delete" mxevent_icon="btn_garbage.gif" type="event"/>
			</tablebody>
			<buttongroup id="ismviewjpqualreq_table_bg1">
				<pushbutton default="true" id="ismviewjpqualreq_table_bg1_bt1" label="New Row" mxevent="addrow"/>
			</buttongroup>
		</table>
		<buttongroup id="ismviewjpqualreq_bg">
			<pushbutton default="true" id="ismviewjpqualreq_bg_bt1" label="Save" mxevent="dialogok"/>
			<pushbutton id="ismviewjpqualreq_bg_bt2" label="Cancel" mxevent="dialogcancel"/>
		</buttongroup>
	</dialog>

	<dialog id="ismviewwoqualreq" label="Qualification Requirements">
		<table id="ismviewwoqualreq_table" relationship="ISMWOQUALREQ">
			<tablebody id="ismviewwoqualreq_table_tb1">
				<tablecol applink="qual" dataattribute="qualificationid" id="ismviewwoqualreq_table_tb1_tc1" lookup="QUALS" menutype="normal"/>
				<tablecol dataattribute="metassignments" id="ismviewwoqualreq_table_tb1_tc2" inputmode="readonly"/>
				<tablecol dataattribute="metplans" id="ismviewwoqualreq_table_tb1_tc3" inputmode="readonly"/>
				<tablecol dataattribute="metactuals" id="ismviewwoqualreq_table_tb1_tc4" inputmode="readonly"/>
				<tablecol dataattribute="exceptiontype" id="ismviewwoqualreq_table_tb1_tc5" lookup="valuelist"/>
				<tablecol id="ismviewwoqualreq_table_tb1_tc6" mxevent="toggledeleterow" mxevent_desc="Mark Row for Delete" mxevent_icon="btn_garbage.gif" type="event"/>
			</tablebody>
			<buttongroup id="ismviewwoqualreq_table_bg1">
				<pushbutton default="true" id="ismviewwoqualreq_table_bg1_bt1" label="New Row" mxevent="addrow"/>
			</buttongroup>
		</table>
		<buttongroup id="ismviewwoqualreq_bg">
			<pushbutton default="true" id="ismviewwoqualreq_bg_bt1" label="Save" mxevent="dialogok"/>
			<pushbutton id="ismviewwoqualreq_bg_bt2" label="Validate Qualifications" mxevent="ismqualval"/>
			<pushbutton id="ismviewwoqualreq_bg_bt3" label="Cancel" mxevent="dialogcancel"/>
		</buttongroup>
	</dialog>

	<dialog id="ismviewwotaskqualreq" label="Qualification Requirements">
		<table id="ismviewwotaskqualreq_table" relationship="ISMWOQUALTASKPARENT">
			<tablebody id="ismviewwotaskqualreq_table_tb1">
				<tablecol applink="qual" dataattribute="qualificationid" id="ismviewwotaskqualreq_table_tb1_tc1" lookup="QUALS" menutype="normal"/>
				<tablecol dataattribute="metassignments" id="ismviewwotaskqualreq_table_tb1_tc2" inputmode="readonly"/>
				<tablecol dataattribute="metplans" id="ismviewwotaskqualreq_table_tb1_tc3" inputmode="readonly"/>
				<tablecol dataattribute="metactuals" id="ismviewwotaskqualreq_table_tb1_tc4" inputmode="readonly"/>
				<tablecol dataattribute="exceptiontype" id="ismviewwotaskqualreq_table_tb1_tc5" lookup="valuelist"/>
				<tablecol dataattribute="wonum" id="ismviewwotaskqualreq_table_tb1_tc6" inputmode="readonly"/>
				<tablecol id="ismviewwotaskqualreq_table_tb1_tc7" mxevent="toggledeleterow" mxevent_desc="Mark Row for Delete" mxevent_icon="btn_garbage.gif" type="event"/>
			</tablebody>
			<buttongroup id="ismviewwotaskqualreq_table_bg1">
				<pushbutton default="true" id="ismviewwotaskqualreq_table_bg1_bt1" label="New Row" mxevent="addrow"/>
			</buttongroup>
		</table>
		<buttongroup id="ismviewwotaskqualreq_bg">
			<pushbutton default="true" id="ismviewwotaskqualreq_bg_bt1" label="Save" mxevent="dialogok"/>
			<pushbutton id="ismviewwotaskqualreq_bg_bt2" label="Validate Qualifications" mxevent="ismqualval"/>
			<pushbutton id="ismviewwotaskqualreq_bg_bt3" label="Cancel" mxevent="dialogcancel"/>
		</buttongroup>
	</dialog>


You'll need to add the ISMQUALIFICATION along with the appropriate menu to the applications (JOBPLAN/WOTRACK) for each of the objects you plan to support (JOBPLAN, JOBTASK, WORKORDER, and/or WOACTIVITY).

Main WORKORDER
WOACTIVITY (Tasks)



JOBPLAN


Optionally, if you want to display the ISMQUALIFICATIONMET you can add that to WOTRACK for Planned Labor, Assignments, & Actual Labor.


We have one signature option that needs to be added to the WO application (IE WOTRACK) that you added the ISMQUALIFICATION field. Please make sure to grant this to any user you want to be able to execute ad-hoc validation of the qualifications.

Option: ISMQUALVAL
Description: Validate Qualifications
Prerequisites: SAVE
Advanced Signature Options (expand section at the bottom): This is an action that must be invoked by user in the UI

 

We have seven messages that need to be configured in Database Configuration. For simplicity I've only displayed the text to make it easier to copy.

Group: ismwoqual
Key: curUserMissingQual
Prefix: BMXZZ
Suffix: E
Value: Qualification verification was enforced on this work order or task and the current user does not meet the required qualification {0}.

Group: ismwoqual
Key: existingRecordRequiredQuals
Prefix: BMXZZ
Suffix: E
Value: Existing records are missing qualification {0}, such as labor {1} on the {2} object. This qualification needs to be removed or labor records updated before this qualification can be added.

Group: ismwoqual
Key: missRequiredQuals
Prefix: BMXZZ
Suffix: E
Value: Qualification {0} is required for labor {1} to be saved on {2}.

Group: ismwoqual
Key: missWarnQuals
Prefix: BMXZZ
Suffix: W
Value: Qualification {0} is missing for labor {1} on the {2} record. Please review to ensure the appropriate qualifications and labor were chosen.

Group: ismwoqual
Key: woQualNoAdd
Prefix: BMXZZ
Suffix: E
Value: Qualifications cannot be added to a WO past approval.

Group: ismwoqual
Key: woQualNoDelete
Prefix: BMXZZ
Suffix: E
Value: Qualifications cannot be removed from a WO after approval.

Group: ismwoqual
Key: woQualNoDeleteChild
Prefix: BMXZZ
Suffix: E
Value: Qualifications cannot be removed from the task record when they are associated to the parent work order.

We have 19 automation scripts. Please be aware that a few of these (such as ISMWOQUALSTATUS) will be marked as optional with remarks of what they do for you to decide if you want. 

Script 1: ISMJPQUAL.DELETE
Description: Delete Qualifications when Job Plan/Job Task is Deleted
Launch Point 1: Object
Object: JOBTASK
Event: Save
Save options: Delete. Before Save

Launch Point 2: Object
Object: JOBPLAN
Event: Save
Save options: Delete. Before Save

 Source:

if mbo.getString("ISMQUALIFICATION") and mbo.getString("ISMQUALIFICATION")!="NONE":
    qualSet=mbo.getMboSet("ISMJPQUALREQ")
    qualSet.deleteAll(mbo.NOACCESSCHECK)


Script 2: ISMJPQUALREQ.INIT
Description: Set Qualifications as Read-Only
Launch Point: Object
Object: ISMJPQUALREQ
Event: Initialize Value

Source: 

# Only need to handle JOBPLAN because the JOBPLAN object is not read-only. JOBTASK is read-only when it's a historical or ACTIVE job plan
owner=mbo.getOwner()
if owner and owner.isBasedOn("JOBPLAN"):
    mbo.setFlag(mbo.READONLY,owner.isRevisionStatusNotAllowed())


Script 3: ISMJPQUALREQ.NEW
Description: Set Default Values on Job Plan Qualifications
Launch Point: NONE
Source:

owner=mbo.getOwner()
if owner and owner.getName() in ["JOBPLAN","JOBTASK"]:
    mbo.setValue("JPNUM",owner.getString("JPNUM"))
    mbo.setValue("SITEID",owner.getString("SITEID"))
    if owner.getString("ORGID"):
        mbo.setValue("ORGID",owner.getString("ORGID"),mbo.NOACCESSCHECK)
        # Don't allow a user to change the ORGID when the JOBPLAN is at an ORG/SITE level
        mbo.setFieldFlag("ORGID",mbo.READONLY,True)
    else:
        # Job Plan may be at a system level but qualifications require an organization. 
        # Require the user to provide the ORGID
        mbo.setFieldFlag("ORGID",mbo.REQUIRED,True)
    
    if owner.getName()=="JOBTASK":
        mbo.setValue("JOBTASKID",owner.getDouble("JOBTASKID"))
        mbo.setValue("PLUSCJPREVNUM",owner.getDouble("PLUSCJPREVNUM"))
        mbo.setValue("JPTASK",owner.getString("JPTASK"))
    else:
        mbo.setValue("JOBPLANID",owner.getDouble("JOBPLANID"))
        mbo.setValue("PLUSCJPREVNUM",owner.getDouble("PLUSCREVNUM"))


Script 4: ISMJPQUALREQ.SAVE
Description: Set Qualification Field on Save
Launch Point: Object
Object: ISMJPQUALREQ
Event: SAVE
Save Options: Add, Update, Delete. Before Save

Source:

owner=mbo.getOwner()
if owner and owner.getName() in ["JOBPLAN","JOBTASK"]:
    qualSet=mbo.getThisMboSet()
    service.invokeScript("ISMWOQUALLIBRARY","setQualificationField",[owner,qualSet])


Script 5: ISMQUALVAL
Description: Validate Qualifications
Launch Point: ACTION

Source:

# This action retriggers validation of all qualifications. 
# This isn't required except for verifying that labor qualifications haven't changed (added/removed) since we validated
if mbo and mbo.isBasedOn("WORKORDER"):
    mbo.setValue("ISMQUALVALIDATE",True,mbo.NOACCESSCHECK)


Script 6: ISMQUALVALIDATECURUSER
Description: Validate Current User for Qualifications

Launch Point 1: Attribute
Object: WOACTIVITY
Attribute: ISMQUALVALIDATECURUSER
Event: Validate

Launch Point 2: Attribute
Object: WORKORDER
Attribute: ISMQUALVALIDATECURUSER
Event: Validate

Source:

# This script can be used by customer to trigger validation for the current logged in user during any scenario.
# For example, when completing a WO you can require that the user has sufficient requirements. Set ISMQUALVALIDATECURUSER to True to trigger validation 

from psdi.server import MXServer

def showError(qualMbo):
    minExceptionLevel=service.getProperty("ismwoqual.validateCurrentUser.exceptionLevel")
    if minExceptionLevel=="ERROR" and qualMbo.getString("EXCEPTIONTYPE")=="ERROR":
        return True
    elif minExceptionLevel=="WARN" and qualMbo.getString("EXCEPTIONTYPE") in ["ERROR","WARN"]:
        return True
    elif minExceptionLevel=="SILENT":
        return True
    elif not minExceptionLevel:
        return True
    else:
        return False

if mbo.getBoolean("ISMQUALVALIDATECURUSER"):
    qualDict=service.invokeScript("ISMWOQUALLIBRARY","getQualMboDict",[mbo])
    laborSet=mbo.getMboSet("$ISMCURUSERLABOR","LABOR","personid=:&PERSONID& and orgid=:orgid")
    laborMbo=laborSet.moveFirst()
    if laborMbo and len(qualDict)>0:
        workDate=MXServer.getMXServer().getDate()
        for qual in qualDict.keys():
            qualMbo=qualDict[qual]
            found=service.invokeScript("ISMWOQUALLIBRARY","checkLaborQual",[laborMbo,qualMbo,workDate])
            if not found and showError(qualMbo):
                service.error("ismwoqual","curUserMissingQual",[qualMbo.getString("QUALIFICATIONID")])
    # Check if no labor record was found and qualifications are required
    elif not laborMbo and len(qualDict)>0:
        for qual in qualDict.keys():
            qualMbo=qualDict[qual]
            if showError(qualMbo):
                service.error("ismwoqual","curUserMissingQual",[qualMbo.getString("QUALIFICATIONID")])



Script 7: ISMWOQUAL.DELETE
Description: Delete Qualifications when WO/Task is Deleted

Launch Point 1: Object
Object: WOACTIVITY
Event: SAVE
Save Options: Delete. Before Save

Launch Point 2: Object
Object: WORKORDER
Event: SAVE
Save Options: Delete. Before Save

Source:

if mbo.getString("ISMQUALIFICATION") and mbo.getString("ISMQUALIFICATION")!="NONE":
    qualSet=mbo.getMboSet("ISMWOQUALREQ")
    qualSet.deleteAll(mbo.NOACCESSCHECK)



Script 8: ISMWOQUALJPCROSS
Description: Crossover Qualifications from Job Plan/Job Task to WO

Launch Point 1: Attribute
Object: WOACTIVITY
Attribute: JOBTASKID
Event: Run Action

Launch Point 2: Attribute
Object: WORKORDER
Attribute: JPNUM
Event: Run Action

Source:

recordMbo=None
if mbo.getString("JPNUM"):
    recordMbo=mbo.getMboSet("JOBPLAN").getMbo(0)
elif mbo.getDouble("JOBTASKID"):
    recordMbo=mbo.getMboSet("JOBTASK").getMbo(0)
# Ensure that we have a qualification by checking our field before getting the qualification set
if recordMbo and recordMbo.getString("ISMQUALIFICATION") and recordMbo.getString("ISMQUALIFICATION")!="NONE":
    woQualSet=mbo.getMboSet("ISMWOQUALREQ")
    
    # Handle JPNUM sometimes getting set multiple times by checking that we haven't already copied qualifications
    if woQualSet.getSize()==0:
        jobQualSet=recordMbo.getMboSet("ISMJPQUALREQ")
        jobQualMbo=jobQualSet.moveFirst()
        while jobQualMbo:
            # Since job plans can be at a system level and we support qualifications per ORG, make sure this matches our ORG before copying
            if jobQualMbo.getString("ORGID")==mbo.getString("ORGID"):
                woQualMbo=woQualSet.add()
                woQualMbo.setValue("QUALIFICATIONID",jobQualMbo.getString("QUALIFICATIONID"),jobQualMbo.NOACCESSCHECK)
                woQualMbo.setValue("EXCEPTIONTYPE",jobQualMbo.getString("EXCEPTIONTYPE"),jobQualMbo.NOACCESSCHECK)
            jobQualMbo=jobQualSet.moveNext()



Script 9: ISMWOQUALLIBRARY
Description: Library Scripts for WO Qualifications
IMPORTANT: You must check the checkbox for "Allow Invoking Script Functions". This allows us to call the individual methods inside this script like we need.
Launch Point: NONE

Source:

from psdi.server import MXServer
from psdi.util import MXApplicationException

assignmentRelationship="SHOWASSIGNMENT"
planLaborRelationship="SHOWPLANLABOR"
labtransRelationship="SHOWACTUALLABOR"

def getWorkDate(recordMbo,woMbo):
    if recordMbo.getName()=="ASSIGNMENT" and recordMbo.getDate("SCHEDULEDATE"):
        return recordMbo.getDate("SCHEDULEDATE")
    elif recordMbo.getName()=="LABTRANS":
        return recordMbo.getDate("STARTDATE")
    # ASSIGNMENT & WPLABOR Utilize the following WO fields in same order if not handled above
    elif woMbo.getDate("SCHEDSTART"):
        return woMbo.getDate("SCHEDSTART")
    elif woMbo.getDate("TARGSTARTDATE"):
        return woMbo.getDate("TARGSTARTDATE")
    else:
        return MXServer.getMXServer().getDate()

def checkQualForSet(attributename,qualMbo,woMbo,service):
    # This is used to set the non-persistent attributes for a particular qualification whether it's been met. 
    # This should not throw an error because it's during initialization
    qualMbo.setValue(attributename,True,qualMbo.NOACCESSCHECK)
    recordSet=None
    if attributename=="METACTUALS":
        recordSet=woMbo.getMboSet(labtransRelationship)
    elif attributename=="METPLANS":
        recordSet=woMbo.getMboSet(planLaborRelationship)
    elif attributename=="METASSIGNMENTS":
        recordSet=woMbo.getMboSet(assignmentRelationship)
    
    recordMbo=recordSet.moveFirst()
    while recordMbo:
        if recordMbo.getString("LABORCODE") and not checkQualForMbo(qualMbo,recordMbo,woMbo):
            qualMbo.setValue(attributename,False,qualMbo.NOACCESSCHECK)
            break
        recordMbo=recordSet.moveNext()

def checkMboSetsForQuals(woMbo,service):
    qualDict=getQualMboDict(woMbo)
    checkMboSetForQuals(woMbo.getMboSet(assignmentRelationship),woMbo,service,qualDict)
    checkMboSetForQuals(woMbo.getMboSet(planLaborRelationship),woMbo,service,qualDict)
    validateLabor=service.getProperty("ismwoqual.validate.labtrans")
    if validateLabor and validateLabor=="1":
        checkMboSetForQuals(woMbo.getMboSet(labtransRelationship),woMbo,service,qualDict)

def buildQualDict(woMbo,qualDict,qualSet):
    qualMbo=qualSet.moveFirst()
    while qualMbo:
        if qualMbo.getString("QUALIFICATIONID") not in qualDict.keys() and not qualMbo.toBeDeleted():
            qualDict[qualMbo.getString("QUALIFICATIONID")]=qualMbo

        qualMbo=qualSet.moveNext()
    return qualDict

def getQualMboDict(woMbo):
    qualDict={}
    qualSet=None
    if woMbo.getBoolean("ISTASK"):
        qualSet=woMbo.getMboSet("ISMWOQUALTASKPARENT")
    else:
        qualSet=woMbo.getMboSet("ISMWOQUALREQ")
    
    qualDict=buildQualDict(woMbo,qualDict,qualSet)
    
    return qualDict

def checkMboSetForQuals(recordSet,woMbo,service,qualDict):
    counter=0
    recordMbo=recordSet.getMbo(counter)
    while recordMbo:
        if not recordMbo.toBeDeleted():
            checkMboForQuals(recordMbo,woMbo,service,qualDict)
        counter+=1
        recordMbo=recordSet.getMbo(counter)

# Used for WPLABOR/ASSIGNMENT/LABTRANS Evaluation against all qualifications
def checkMboForQuals(recordMbo,woMbo,service,qualDict):
    if not recordMbo.getString("LABORCODE"):
        # Since a labor record isn't referenced, we can't check qualification
        recordMbo.setValue("ISMQUALIFICATIONMET","NA",recordMbo.NOACCESSCHECK)
    else:
        # Since qualifications are 1:many, we need to track when some match 
        allMatch=True
        someMatch=False    
        for qualification in qualDict.keys():
            qualMbo=qualDict[qualification]
            # Don't evaluate qualifications about to be deleted
            if not qualMbo.toBeDeleted():
                matches=checkQualForMbo(qualMbo,recordMbo,woMbo)
                if matches:
                    someMatch=True
                else:
                    allMatch=False
                    handleMissingQualification(woMbo,qualMbo,recordMbo,service)
            
        # If there are no qualifications set to NA
        if len(qualDict)==0:
            recordMbo.setValue("ISMQUALIFICATIONMET","NA",recordMbo.NOACCESSCHECK)
        elif someMatch and not allMatch:
            recordMbo.setValue("ISMQUALIFICATIONMET","PARTIAL",recordMbo.NOACCESSCHECK)
        elif allMatch:
            recordMbo.setValue("ISMQUALIFICATIONMET","ALL",recordMbo.NOACCESSCHECK)
        else:
            recordMbo.setValue("ISMQUALIFICATIONMET","NONE",recordMbo.NOACCESSCHECK)

def handleMissingQualification(woMbo,qualMbo,recordMbo,service):
    params=[qualMbo.getString("QUALIFICATIONID"),recordMbo.getString("LABORCODE"),recordMbo.getName()]
    
    # Error/Warn/Silent Message Handling
    if qualMbo.getString("EXCEPTIONTYPE")=="ERROR":
        if qualMbo.toBeAdded():
            # Utilize a different, more descriptive error message, when a qualification is being added
            service.error("ismwoqual","existingRecordRequiredQuals",params)
        else:
            service.error("ismwoqual","missRequiredQuals",params)
    elif qualMbo.getString("EXCEPTIONTYPE")=="WARN":
        mxe=MXApplicationException("ismwoqual","missWarnQuals",params)
        recordMbo.getThisMboSet().addWarning(mxe)


def checkLaborQual(laborMbo,qualMbo,workDate):
    laborQualSet=laborMbo.getMboSet("LABORQUAL")
    laborQualMbo=laborQualSet.moveFirst()
    while laborQualMbo:
        if laborQualMbo.getString("QUALIFICATIONID")==qualMbo.getString("QUALIFICATIONID"):
            if laborQualMbo.getInternalStatus()=="ACTIVE" and (not laborQualMbo.getDate("EFFDATE") or laborQualMbo.getDate("EFFDATE").before(workDate) or laborQualMbo.getDate("EFFDATE").equals(workDate)) and (not laborQualMbo.getDate("ENDDATE") or laborQualMbo.getDate("ENDDATE").after(workDate)):
                return True
            else:
                return False
        laborQualMbo=laborQualSet.moveNext()
    return False

def checkQualForMbo(qualMbo,recordMbo,woMbo):
    # Assignment, WPLABOR, & LABTRANS All have a LABOR relationship OOTB. 
    # If added to additional objects more intelligence might be needed here 
    laborSet=recordMbo.getMboSet("LABOR")
    # Reset so that changes to the labor record are reflected
    laborSet.reset()
    laborMbo=laborSet.getMbo(0)
    return checkLaborQual(laborMbo,qualMbo,getWorkDate(recordMbo,woMbo))
    

def setQualificationField(woJpMbo,qualSet):
    qualList=[]
    counter=0
    while qualSet.getMbo(counter):
        qualMbo=qualSet.getMbo(counter)
        if qualMbo.getString("QUALIFICATIONID") and not qualMbo.toBeDeleted():
            qualList.append(qualMbo.getString("QUALIFICATIONID"))
            
        counter+=1
    if len(qualList)==1:
        woJpMbo.setValue("ISMQUALIFICATION",qualList[0],woJpMbo.NOACCESSCHECK)
    elif len(qualList)==0:
        woJpMbo.setValue("ISMQUALIFICATION","NONE",woJpMbo.NOACCESSCHECK)
    elif len(qualList)>1:
        woJpMbo.setValue("ISMQUALIFICATION","MULTIPLE",woJpMbo.NOACCESSCHECK)



Script 10: ISMWOQUALMETACTUAL

OPTIONAL: This script populates some non-persistent attributes on ISMWOQUALREQ to make it easier to see which qualifications are met on all planned labor, assignment, or labor transaction records. It has no other functional impact.

Description: Populate non-persistent attributes when being viewed in qualification dialog
Launch Point: Attribute
Object: ISMWOQUALREQ
Attribute: METACTUALS
Event: Initialize Value

Source:

owner=mbo.getOwner()
if owner and owner.isBasedOn("WORKORDER"):
    service.invokeScript("ISMWOQUALLIBRARY","checkQualForSet",["METACTUALS",mbo,owner,service])
    service.invokeScript("ISMWOQUALLIBRARY","checkQualForSet",["METASSIGNMENTS",mbo,owner,service])
    service.invokeScript("ISMWOQUALLIBRARY","checkQualForSet",["METPLANS",mbo,owner,service])



Script 11: ISMWOQUALREQ.CANADD
Description: Restrict Adding new Qualifications
Launch Point: Object
Object: ISMWOQUALREQ
Event: Allow Object Creation

Source:

owner=mboset.getOwner()
# Only allow adding new qualifications pre-approval
if owner and owner.isBasedOn("WORKORDER") and owner.getString("FIRSTAPPRSTATUS"):
    service.error("ismwoqual","woQualNoAdd")



Script 12: ISMWOQUALREQ.CANDELETE
Description: Restrict Deleting Qualifications
Launch Point: Object
Object: ISMWOQUALREQ
Event: Allow Object Deletion

Source:

owner=mbo.getOwner()
if owner and owner.isBasedOn("WORKORDER") and owner.getString("FIRSTAPPRSTATUS"):
    service.error("ismwoqual","woQualNoDelete")
# Prevent a qualification from the parent being deleted on the task
if owner and owner.isBasedOn("WORKORDER") and owner.getString("WONUM")!=mbo.getString("WONUM"):
    service.error("ismwoqual","woQualNoDeleteChild")



Script 13: ISMWOQUALREQ.NEW
Description: Default Values on New Qualifications
Launch Point: NONE

Source:

owner=mbo.getOwner()
if owner and owner.isBasedOn("WORKORDER"):
    mbo.setValue("WONUM",owner.getString("WONUM"),mbo.NOACCESSCHECK)
    mbo.setValue("SITEID",owner.getString("SITEID"),mbo.NOACCESSCHECK)
    mbo.setValue("ORGID",owner.getString("ORGID"),mbo.NOACCESSCHECK)



Script 14: ISMWOQUALREQ.SAVE
Description: Validate WO Qualifications on Save
Launch Point: Object
Object: ISMWOQUALREQ
Event: SAVE
Save Options: Add, Update, Delete. Before Save

Source:

owner=mbo.getOwner()
if owner and owner.isBasedOn("WORKORDER"):
    qualSet=mbo.getThisMboSet()
    owner.setValue("ISMQUALVALIDATE",True,owner.NOACCESSCHECK)
    # Set flag on WO for Qualification
    service.invokeScript("ISMWOQUALLIBRARY","setQualificationField",[owner,qualSet])



Script 15: ISMWOQUALSTATUS

OPTIONAL: This script triggers the current user validation when the WO is being completed and the system property is enabled.

Description: Validate Current User Qualifications on WO Status Changes
Launch Point 1: Attribute
Object: WORKORDER
Attribute: STATUS
Event: Validate

Launch Point 2: Attribute
Object: WOACTIVITY
Attribute: STATUS
Event: Validate

Source:

if interactive and mbo.getInternalStatus() in ["COMP"] and service.getProperty("ismwoqual.validateCurrentUser")=="1":
    mbo.setValue("ISMQUALVALIDATECURUSER",True,mbo.NOACCESSCHECK)



Script 16: ISMWOQUALVALIDATE
Description: Validate WO Qualifications

Launch Point 1: Attribute
Object: WORKORDER
Attribute: ISMQUALVALIDATE
Event: Validate

Launch Point 2: Attribute
Object: WOACTIVITY
Attribute: ISMQUALVALIDATE
Event: Validate

Source:

if mbo.getBoolean("ISMQUALVALIDATE"):
    service.invokeScript("ISMWOQUALLIBRARY","checkMboSetsForQuals",[mbo,service])



Script 17: JOBPLAN.DUPLICATE
Description: ISM Copy Qualifications on Duplication/Revision of JOBPLAN
Launch Point: NONE

Source:

from psdi.server import MXServer
def copyQualifications(jobplanMbo,oldQualSet,newQualSet,isRevision):
    oldQualMbo=oldQualSet.moveFirst()
    while oldQualMbo:
        newQualMbo=newQualSet.add()
        newQualMbo.setValue("ORGID",oldQualMbo.getString("ORGID"),newQualMbo.NOACCESSCHECK)
        newQualMbo.setValue("QUALIFICATIONID",oldQualMbo.getString("QUALIFICATIONID"),newQualMbo.NOACCESSCHECK)
        newQualMbo.setValue("EXCEPTIONTYPE",oldQualMbo.getString("EXCEPTIONTYPE"),newQualMbo.NOACCESSCHECK)
        if isRevision:
            # The framework hasn't set the new revision yet so we have to set it
            newQualMbo.setValue("PLUSCJPREVNUM",jobplanMbo.getNextRevNum(),newQualMbo.NOACCESSCHECK)
        oldQualMbo=oldQualSet.moveNext()    

if mbo.getString("ISMQUALIFICATION") and mbo.getString("ISMQUALIFICATION")!="NONE":
    newQualSet=dupmbo.getMboSet("ISMJPQUALREQ")
    oldQualSet=mbo.getMboSet("ISMJPQUALREQ")
    isRevision=MXServer.getBulletinBoard().isPosted("jobplan.REVISEJOBPLAN",mbo.getUserInfo())
    copyQualifications(mbo,oldQualSet,newQualSet,isRevision)



Script 18: JOBTASK.DUPLICATE
Description: ISM Copy Qualifications on Duplication/Revision of JOBTASK
Launch Point: NONE

Source:

from psdi.server import MXServer
def copyQualifications(jobplanMbo,oldQualSet,newQualSet,isRevision):
    oldQualMbo=oldQualSet.moveFirst()
    while oldQualMbo:
        newQualMbo=newQualSet.add()
        newQualMbo.setValue("ORGID",oldQualMbo.getString("ORGID"),newQualMbo.NOACCESSCHECK)
        newQualMbo.setValue("QUALIFICATIONID",oldQualMbo.getString("QUALIFICATIONID"),newQualMbo.NOACCESSCHECK)
        newQualMbo.setValue("EXCEPTIONTYPE",oldQualMbo.getString("EXCEPTIONTYPE"),newQualMbo.NOACCESSCHECK)
        if isRevision:
            # The framework hasn't set the new revision yet so we have to set it
            newQualMbo.setValue("PLUSCJPREVNUM",jobplanMbo.getNextRevNum(),newQualMbo.NOACCESSCHECK)    
        oldQualMbo=oldQualSet.moveNext()    

# The task lookup on JOBLABOR utilizes a MBO (JPTASKLOOKUP) that calls the duplicate. 
# Only try to evaluate when we are still dealing with a job task record
if mbo.getString("ISMQUALIFICATION") and mbo.getString("ISMQUALIFICATION")!="NONE" and dupmbo.getName()=="JOBTASK":
    newQualSet=dupmbo.getMboSet("ISMJPQUALREQ")
    oldQualSet=mbo.getMboSet("ISMJPQUALREQ")
    isRevision=MXServer.getBulletinBoard().isPosted("jobplan.REVISEJOBPLAN")
    owner=mbo.getOwner()
    # This should always be true which is why we don't have an else
    if owner and owner.getName()=="JOBPLAN":
        copyQualifications(owner,oldQualSet,newQualSet,isRevision)

Script 19: ISMQUALRECORD.SAVE

Description: Validate Transaction Record (WPLABOR, ASSIGNMENT, LABTRANS, etc.) Prior to Save for Qualifications
Launch Point 1: Object
Object: ASSIGNMENT
Event: SAVE
Save Options: Add/Update/Delete. Before Save

Launch Point 2: Object
Object: LABTRANS
Event: SAVE
Save Options: Add/Update/Delete. Before Save

Launch Point 3: Object
Object: WPLABOR
Event: SAVE
Save Options: Add/Update/Delete. Before Save

Source:

owner=mbo.getOwner()
if owner and owner.isBasedOn("WORKORDER"):
    if mbo.getName()=="ASSIGNMENT" and not mbo.getThisMboSet().getRelationName():
        # Assignment was done utilizing the WMASSIGNMENT view (EX: Assignment Manager) which requires special processing
        qualDict=service.invokeScript("ISMWOQUALLIBRARY","getQualMboDict",[owner])
        service.invokeScript("ISMWOQUALLIBRARY","checkMboForQuals",[mbo,owner,service,qualDict])
    elif mbo.getName()=="LABTRANS" and service.getProperty("ismwoqual.validate.labtrans")=="0":
        # Skip, do nothing because we're disabled
        service.log("Skipped labor validation due to property disabled")
    else:
        owner.setValue("ISMQUALVALIDATE",True,owner.NOACCESSCHECK)
elif owner and owner.isBasedOn("LABORCRAFTRATE") and mbo.getName()=="ASSIGNMENT":
    # Assignment was modified in the availability dialog of assignment manager
    woMbo=mbo.getMboSet("WORKORDER").getMbo(0)
    qualDict=service.invokeScript("ISMWOQUALLIBRARY","getQualMboDict",[woMbo])
    service.invokeScript("ISMWOQUALLIBRARY","checkMboForQuals",[mbo,woMbo,service,qualDict])




#MaximoIntegrationandScripting
#Maximo
#AssetandFacilitiesManagement
1 comment
50 views

Permalink

Comments

Fri March 15, 2024 09:54 AM

Steven this is really great work.  Thank you for sharing this with the community!!