This started as a customer idea (https://ibm-ai-apps.ideas.ibm.com/ideas/MASM-I-543). The customer requirement was to support activating or completing a PM based on another PM. While the Maximo for Transportation industry solution offers this functionality, adopting the industry solution may not be appropriate for all organizations. With this functionality being relatively isolated and useful for organizations outside of the Transportation industry we set out to figure out how difficult this would be to implement with just some configuration and automation scripts.
NOTE: This is provided as-is without any official support from IBM. This was not written to support meter based PMs. There may be scenarios even with time based PMs that are not handled properly so this should be tested thoroughly prior to implementing in your environment.
Schema changes:
I've posted a DBC file that can be utilized to deploy these in your Maximo environment. You can download the file here: https://community.ibm.com/community/user/iot/viewdocument/pm-relationship-dbc-file?CommunityKey=ed77c224-45e2-47b0-b574-cc31496f9a41. You can utilize this to determine what to add even if you don't want to go through the DBC deployment process itself.
I added two new objects (ISMPMREL & ISMPMHIST), 1 new attribute on a core object (ISMMAXCOUNT on PM), two domains (ISMINACTIVE & ISMPMREL), a couple of relationships between the objects. The ISMPMREL object is where we will store our mapping between PMs and which action should be taken. The ISMPMHIST will be utilized for storing data when a PM is completed so we can easily see which WO was used for recording PM completion since we now support the PM being claimed by another.
Automation Scripts:
We have 8 automation scripts in total. Most of these are very simple scripts with the exception of the ISMPMCOMPLETE script which contains most of our logic. All scripts were written in jython language.
Script: ISMPM.CANDELETE
Launch Point: Object
Object: PM
Event: Allow Object Deletion
Source:
relatedPMs=mbo.getMboSet("$ISMPMRELDELETE","ISMPMREL","relpm = :pmnum and pmnum != :pmnum and siteid = :siteid")
# Throw error if a record is returned
if not relatedPMs.notExist():
service.error("ismpm","deleteSecondaryPM",[mbo.getString("PMNUM")])
Purpose: This script is to avoid a PM being deleted when it's referred to on our new custom object.
Script: ISMPM.LASTCOMPDATE.INIT
Launch Point: Attribute
Object: PM
Attribute: LASTCOMPDATE
Event: Initialize Value
Source:
if mbo.getOwner() and mbo.getOwner().isBasedOn("WORKORDER"):
# Set the flag for SAMEVALUEVALIDATION to trigger validation even if the value is the same
mbo.setFieldFlag("LASTCOMPDATE",mbo.SAMEVALUEVALIDATION,True)
Purpose: This script adds a MboConstant that triggers our validate event for recording PM completion. Otherwise if multiple PMs are completed in the same day only 1 completion would be recorded in our PM history table.
Script: ISMPM.PMCOUNTER
Launch Point: Attribute
Object: PM
Attribute: PMCOUNTER
Event: Run Action
Source:
if mbo.getInt("ISMMAXCOUNT")>0 and mbo.getInt("PMCOUNTER")>=mbo.getInt("ISMMAXCOUNT"):
# Mark INACTIVE so it doesn't generate again
mbo.setValue("STATUS","INACTIVE",mbo.NOACCESSCHECK)
Purpose: This script supports setting a maximum number of executions for a PM before it's deactivated. Sometimes the PMs might only need to run a set number of times and this will deactivate the PM after the the counter exceeds the value defined in our ISMMAXCOUNT attribute.
Script: ISMPMCOMPLETE
Launch Point: Attribute
Object: PM
Attribute: LASTCOMPDATE
Event: Validate
Source:
def getTopOwner():
# The WOStatusHandler gets the PM from the WO as a child.
# This allows us to get the WO # and some additional information to record in the PM history table
# Since our script may cause this script to fire for additional PMs, we need to get all the way back to the WO
ownerMbo=mbo
while ownerMbo and ownerMbo.getOwner():
ownerMbo=ownerMbo.getOwner()
return ownerMbo
def recordPMHistory(pmMbo,woMbo):
# This functions a bit differently than Transportation where a history record is created first and then updated.
# We are currently only recording it upon completion for efficiency & simplicity
if woMbo and woMbo.isBasedOn("WORKORDER"):
pmhistSet=pmMbo.getMboSet("ISMPMHIST")
pmhistMbo=pmhistSet.add(mbo.NOACCESSCHECK)
pmhistMbo.setValue("PMNUM",pmMbo.getString("PMNUM"),mbo.NOACCESSCHECK)
pmhistMbo.setValue("SITEID",pmMbo.getString("SITEID"),mbo.NOACCESSCHECK)
pmhistMbo.setValue("LASTCOMPDATE",woMbo.getDate("STATUSDATE"),mbo.NOACCESSCHECK)
pmhistMbo.setValue("DATECOMP",woMbo.getDate("ACTFINISH"),mbo.NOACCESSCHECK)
pmhistMbo.setValue("DATEDUE",pmMbo.getDate("NEXTDATE"),mbo.NOACCESSCHECK)
if woMbo.getString("PMNUM")!=pmMbo.getString("PMNUM"):
pmhistMbo.setValue("INACTIVECODE","CLAIMED",mbo.NOACCESSCHECK)
pmhistMbo.setValue("PMCLAIMEDBY",woMbo.getString("PMNUM"),mbo.NOACCESSCHECK)
pmhistMbo.setValue("WOCLAIMEDBY",woMbo.getString("WONUM"),mbo.NOACCESSCHECK)
else:
pmhistMbo.setValue("WONUM",woMbo.getString("WONUM"),mbo.NOACCESSCHECK)
def relPMComplete(relPMMbo,woMbo):
# Increment the PM counter since this has been claimed
relPMMbo.setValue("PMCOUNTER",relPMMbo.getInt("PMCOUNTER")+1,relPMMbo.NOACCESSCHECK)
if not relPMMbo.getDate("LASTSTARTDATE") or woMbo.getDate("ACTFINISH").after(relPMMbo.getDate("LASTSTARTDATE")):
relPMMbo.setValue("LASTSTARTDATE",woMbo.getDate("ACTFINISH"),relPMMbo.NOACCESSCHECK)
if not relPMMbo.getDate("LASTCOMPDATE") or woMbo.getDate("ACTFINISH").after(relPMMbo.getDate("LASTCOMPDATE")):
relPMMbo.setValue("LASTCOMPDATE",woMbo.getDate("ACTFINISH"),relPMMbo.NOACCESSCHECK)
else:
# This ensures our script still fires on completion when multiple WOs get completed on same day
relPMMbo.setFieldFlag("LASTCOMPDATE",relPMMbo.SAMEVALUEVALIDATION,True)
# Set LASTCOMPDATE equal to itself to trigger the PM history and cascading rules but not modify the value
relPMMbo.setValue("LASTCOMPDATE",relPMMbo.getDate("LASTCOMPDATE"),relPMMbo.NOACCESSCHECK)
# Update Work Order Dates
relPMMbo.setNextDueDate()
def pmrelAction(pmrelMbo,woMbo):
actiontype=pmrelMbo.getString("ACTIONTYPE")
relatedPMMbo=pmrelMbo.getMboSet("ISMRELPM").moveFirst()
if relatedPMMbo:
if actiontype=="ACTIVATE" and not relatedPMMbo.getInternalStatus()=="ACTIVE":
relatedPMMbo.setValue("STATUS","ACTIVE",mbo.NOACCESSCHECK)
# Set the dates for LASTSTARTDATE/LASTCOMPDATE based on the PM being completed. This ensures our dates start from this completion date
relatedPMMbo.setValue("LASTSTARTDATE",woMbo.getString("ACTFINISH"),relatedPMMbo.NOACCESSCHECK)
relatedPMMbo.setValue("LASTCOMPDATE",woMbo.getString("ACTFINISH"),relatedPMMbo.NOACCESSCHECK)
relatedPMMbo.setNextDueDate()
elif actiontype=="INACTIVATE" and relatedPMMbo.getInternalStatus()=="ACTIVE":
relatedPMMbo.setValue("STATUS","INACTIVE",mbo.NOACCESSCHECK)
elif actiontype=="COMPLETE" and relatedPMMbo.getInternalStatus()=="ACTIVE":
relPMComplete(relatedPMMbo,woMbo)
else:
service.error("ismpm","relPMMissing",[pmrelMbo.getString("PMNUM"),pmrelMbo.getString("RELPM")])
ownerMbo=getTopOwner()
# LASTCOMPDATE can be updated in scenarios (such as reinstating a PM) where we should do nothing. Verify the WO was completed prior to recording history or updating records
if ownerMbo and ownerMbo.isBasedOn("WORKORDER") and ownerMbo.getInternalStatus() in ["COMP","CLOSE"]:
recordPMHistory(mbo,ownerMbo)
pmrelSet=mbo.getMboSet("ISMPMREL")
pmrelMbo=pmrelSet.moveFirst()
while pmrelMbo:
pmrelAction(pmrelMbo,ownerMbo)
pmrelMbo=pmrelSet.moveNext()
Purpose: This script executes the majority of the logic for this enhancement. This records the history of the PM completion in our custom ISMPMHIST table. This looks for any defined rules we have and executes the update to the PM (makes it active, marks it complete, etc.).
Script: ISMPMHIST.INIT
Launch Point: Object
Object: ISMPMHIST
Event: Initialize Value
Source:
mbo.setFlag(mbo.READONLY,True)
Purpose: This script marks our history records as read-only. This could be achieved in other ways (such as a Global Data Restriction).
Script: ISMPMREL.NEW
Launch Point: NONE
Source:
owner=mbo.getOwner()
if owner and owner.isBasedOn("PM"):
mbo.setValue("PMNUM",owner.getString("PMNUM"))
mbo.setValue("SITEID",owner.getString("SITEID"))
Purpose: This defaults values on our custom object based on our source PM.
Script: ISMPMREL.RELPM.GETLIST
Launch Point: Attribute
Object Name: ISMPMREL
Attribute: RELPM
Event: Retrieve List
Source:
relationObject="PM"
# Need to ensure that we don't have circular references (where PM1 references PM2 and PM2 references PM1)
relationWhere="pmnum=:relpm and siteid=:siteid and pmnum!=:pmnum and not exists(SELECT 1 FROM ismpmrel WHERE pmnum=:relpm and siteid=:siteid and relpm=:pmnum)"
srcKeys=["pmnum"]
targetKeys=["relpm"]
listWhere="siteid=:siteid and pmnum!=:pmnum and not exists(SELECT 1 FROM ismpmrel WHERE ismpmrel.pmnum=pm.pmnum and ismpmrel.siteid=:siteid and ismpmrel.relpm=:pmnum)"
Purpose: This supports our lookup for related PMs.
Script: ISMPMREL.VALIDATE
Launch Point: Object
Object Name: ISMPMREL
Event: Validate Application
Source:
if not mbo.getString("ACTIONTYPE"):
service.error("ismpm","missingRequiredField",[mbo.getMboValue("ACTIONTYPE").getColumnTitle()])
if not mbo.getString("RELPM"):
service.error("ismpm","missingRequiredField",[mbo.getMboValue("RELPM").getColumnTitle()])
Purpose: This adds some error handling prior to save to ensure we have all the necessary fields for the PM relationship
Messages:
I added three new messages to support error handling in our automation scripts.
Group: ismpm
Key: deleteSecondaryPM
Prefix: BMXZZ
Suffix: E
Value: You cannot delete the PM {0} because it is referenced as a related PM on PM relationships. Please delete the PM relationships prior to deleting the PM.
Group: ismpm
Key: missingRequiredField
Prefix: BMXZZ
Suffix: E
Value: The attribute {0} is required. Please provide a value.
Group: ismpm
Key: relPMMissing
Prefix: BMXZZ
Suffix: E
Value: PM {0} has a related PM {1} that no longer exists. Please delete the relationship.
Screen Changes:
These can be placed as desired in the PM application. I created a separate tab for viewing the PM history, added the PM relationship table to the main tab in my environment, and put the maximum counter on the Frequency tab. The XML snippets to add to the application are as follows:
PM relationship table
<table id="ismpmrel" label="PM Relationships" relationship="ISMPMREL">
<tablebody id="ismpmrel_tb">
<tablecol dataattribute="ACTIONTYPE" id="ismpmrel_tc1" inputmode="required" lookup="valuelist"/>
<tablecol applink="PM" dataattribute="relpm" id="ismpmrel_tc2" inputmode="required" lookup="PM" menutype="NORMAL"/>
<tablecol dataattribute="ISMRELPM.DESCRIPTION" id="ismpmrel_tc3" inputmode="readonly"/>
<tablecol id="ismpmrel_tc4" mxevent="toggledeleterow" mxevent_desc="Mark Row for Delete" mxevent_icon="btn_garbage.gif" type="event"/>
</tablebody>
<buttongroup align="right" id="ismpmrel_bg">
<pushbutton id="ismpmrel_bg_btn1" label="New Row" mxevent="addrow"/>
</buttongroup>
<tabledetails id="ismpmrel_details"/>
</table>
PM history tab
<tab id="ismpmhist_tab" label="PM History">
<section border="true" height="100" id="ismpmhist_sec">
<sectionrow id="ismpmhist_sec_r1">
<sectioncol id="ismpmhist_sec_r1_c1">
<section id="ismpmhist_sec_r1_c1_sec">
<multiparttextbox dataattribute="pmnum" descdataattribute="description" desclookup="longdesc" id="ismpmhist_sec_r1_c1_txt1"/>
</section>
</sectioncol>
<sectioncol id="ismpmhist_sec_r1_c2">
<section id="ismpmhist_sec_r1_c2_sec">
<textbox dataattribute="siteid" id="ismpmhist_sec_r1_c2_txt1" inputmode="readonly"/>
</section>
</sectioncol>
<sectioncol id="ismpmhist_sec_r1_c3">
<section id="ismpmhist_sec_r1_c3_sec">
<textbox dataattribute="status" id="ismpmhist_sec_r1_c3_txt1" inputmode="readonly"/>
</section>
</sectioncol>
</sectionrow>
<sectionrow id="ismpmhist_sec_r2">
<sectioncol id="ismpmhist_sec_r2_c1">
<table id="ismpmhist_sec_r2_c1_table1" label="PM History Records" orderby="datecreated desc" relationship="ISMPMHIST">
<tablebody id="ismpmhist_sec_r2_c1_tb1">
<tablecol dataattribute="datedue" id="ismpmhist_sec_r2_c1_tb1_tc1" lookup="datelookup"/>
<tablecol dataattribute="datecreated" id="ismpmhist_sec_r2_c1_tb1_tc2" lookup="datelookup"/>
<tablecol dataattribute="DATECOMP" id="ismpmhist_sec_r2_c1_tb1_tc3" lookup="datelookup"/>
<tablecol applink="WOTRACK" dataattribute="wonum" id="ismpmhist_sec_r2_c1_tb1_tc4" menutype="NORMAL"/>
<tablecol dataattribute="INACTIVECODE" id="ismpmhist_sec_r2_c1_tb1_tc5" lookup="valuelist"/>
<tablecol applink="PM" dataattribute="PMCLAIMEDBY" id="ismpmhist_sec_r2_c1_tb1_tc6" lookup="PM" menutype="NORMAL"/>
<tablecol applink="WOTRACK" dataattribute="WOCLAIMEDBY" id="ismpmhist_sec_r2_c1_tb1_tc7" menutype="NORMAL"/>
</tablebody>
<tabledetails id="ismpmhist_sec_r2_c1_details"/>
</table>
</sectioncol>
</sectionrow>
</section>
</tab>
Maximum Count
<textbox dataattribute="ISMMAXCOUNT" id="ismmaxcounttxt"/>
Utilizing the functionality:
1) Go to the PM application
2) Add a new relationship with a specific action to take on another PM.

3) Once that PM is completed, including records that may have been generated already, the related PMs will be modified as defined. All PMs (with or without a relationship) will also start to record history.
If interested in utilizing the max counter, just set that value equal to the maximum number of times you want the PM to run before it gets deactivated. Then when a PM is generated that takes it beyond that number it will deactivate itself automatically.
#AssetandFacilitiesManagement#Maximo#MaximoIntegrationandScripting