Direct Print URL and File Based Attachments

I was working with a customer recently that was having problems with the Direct Print with Attachments function from a list of work orders in Maximo. In Maximo 7.6, they would navigate to the Work Order Tracking application, filter for a list of work orders, and run the Direct Print with Attachments option against those work orders. After upgrading to MAS 9.0, they were receiving errors from the system when attempting to do so under certain conditions.

The main trigger for the error they were seeing was the presence of a URL-based attachment on one of the work orders being printed. Without a URL-based attachment on any work order in the batch, the Direct Print with Attachments routine worked just fine. The problem was that most work orders had a safety data sheet attached that referenced an online PDF document that would get updated frequently.

We went through the standard process of raising an IBM support case, but unfortunately timelines were in danger of being impacted and we needed a workaround solution. We decided to create an Automation Script that could get called from the REST API in Postman that could generate the reports.

The Automation Script accepts two query parameters:

  • where: This represents a where clause against the WORKORDER table that can be applied to filter the list of work orders down to just those wanting to be printed.
  • max: A numeric value that represents the maximum number of records to process in a given print run. This protects against a where clause that may return more records than expected.

Here is the script. Please note that there is not a Launch Point associated with the script as it is currently meant to be called from the REST API. Also, this is meant to be a temporary script until the underlying issue is resolved from IBM.

# ------------------
# This script will generate a single PDF file for direct 
# printing a work order or a group of work orders.
# 
# The script can either be called from:
# 1. the REST API via: /oslc/script/WODIRECTPRINT, or 
# 2. a JSP file. If using this method, please be sure 
#    to set the parameters as part of the script context.
# 
# Parameters:
# - where: The where clause of the work orders to pull.
# - max: How many work orders do we want to limit 
#   the PDF attachments to? Default is -1 which does 
#   not apply a limit.
# 
# @author Alex Walter alex@a3jgroup.com
# @date   09 JUL 2025
# ------------------
"""
{
	'autoscript': 'WODIRECTPRINT',
	'description': 'Direct Print Work Orders',
	'scriptlanguage': 'python',
	'version': '1.0.0',
	'loglevel': 'DEBUG',
	'active': true
}
"""
from com.groupdocs.conversion import Converter
from com.groupdocs.conversion.licensing import License
from com.groupdocs.conversion.options.convert import PdfConvertOptions
from com.ibm.tivoli.maximo.oslc.provider import AttachmentStorage
from com.ibm.tivoli.maximo.report.birt.runtime import ReportParameterData, ReportRuntimeTempLocation
from java.io import ByteArrayInputStream, File
from java.lang import System
from java.util import HashMap, Vector
from java.util.concurrent.atomic import AtomicInteger
from javax.imageio import ImageIO
from psdi.app.report import PathAndReportFlag, ReportUtil
from psdi.iface.router import HTTPHandler
from psdi.iface.util import XMLUtils
from psdi.server import MXServer
from psdi.util.logging import MXLoggerFactory

# -----------
# Convert Word and Excel documents to PDF
# -----------
def convertByGroupDocs(sourceFilePath, targetFilePath):
	converter = Converter(sourceFilePath)
	converter.convert(targetFilePath, PdfConvertOptions())
	converter.close()

# -----------
# Get the file extension from a file path
# -----------
def getFileExtension(path):
	# Find the last occurrence of '.' in the path
	lastDotIndex = path.rfind('.')
	# If a dot is found and it's not the last character, return the extension
	if lastDotIndex != -1 and lastDotIndex < len(path) - 1:
		return path[lastDotIndex + 1:]
	# Return empty string if no extension is found
	return ""

# -----------
# Get a byte array from a given URL
# -----------
def getByteArrayforUrl(docpath):
	properties = HashMap()
	properties.put("HTTPMETHOD", "GET")
	properties.put("URL", docpath)
	return HTTPHandler().invoke(properties, None)

# -----------
# For a given work order, loop over all printable 
# attachments and convert them to PDF.
# -----------
def convertAllAttachmentsToPDF(baseMbo):
	global listOfFilesVector, outputFolder, docIndex, logger

	# Set license key of PDF converter
	converter = None
	groupdocLicense = License()
	groupdocLicense.setLicense(ReportRuntimeTempLocation.getGroupdocsLicLocation() + File.separator + "GroupDocs.Conversion.lic")

	# Get the set of attachments for this WO
	doclinksSet = baseMbo.getMboSet("DOCLINKS")
	doclinks = doclinksSet.moveFirst()
	while doclinks:
		if doclinks.getBoolean("PRINTTHRULINK"):
			# Increment document index
			docIndex = docIndex + 1

			# What kind of file are we looking at?
			isListItem = False
			isImageFile = False
			isTextFile = False
			isPdfFile = False

			# Make a temporary file name
			groupDocConvertedFileName = "File" + str(docIndex)

			# Get the document URL and see what type of file we are working with
			attachUrl = doclinks.getString("urlname")
			fileExtension = getFileExtension(attachUrl)
			attchUrlUpperCase = attachUrl.upper()
			if attchUrlUpperCase.endswith(".XLS") or attchUrlUpperCase.endswith(".XLSX"):
				isListItem = True
			elif attchUrlUpperCase.endswith(".PNG") or attchUrlUpperCase.endswith(".JPG") or attchUrlUpperCase.endswith(".GIF") or attchUrlUpperCase.endswith(".JPEG"):
				isImageFile = True
			elif attchUrlUpperCase.endswith(".TXT") or attchUrlUpperCase.endswith(".CSV"):
				isTextFile = True
			elif attchUrlUpperCase.endswith(".PDF"):
				isPdfFile = True
			
			if logger.isDebugEnabled():
				logger.debug("attach url:" + attachUrl)
				logger.debug("file extension:" + fileExtension)
				logger.debug("isListItem:" + str(isListItem))
				logger.debug("isImageFile:" + str(isImageFile))
				logger.debug("isTextFile:" + str(isTextFile))
				logger.debug("isPdfFile:" + str(isPdfFile))
			
			try:
				# Get the file data from URL or COS Storage
				fileData = None
				try:
					urlType = doclinks.getString("URLTYPE")
					if urlType == "URL":
						if logger.isDebugEnabled():
							logger.debug("urlType: [" + urlType + "] [" + doclinks.getString("weburl") + "]")

						# Get the file data from URL
						docpath = doclinks.getString("weburl")
						fileData = getByteArrayforUrl(docpath)
					else:
						if logger.isDebugEnabled():
							logger.debug("urlType: [" + urlType + "]")
						
						# Get the file data from COS
						fileData = AttachmentStorage.getInstance().getAttachment(doclinks)
				except Exception as fe:
					logger.error("URL attachment " + attachUrl + " might be missing")
					logger.error("Error: " + str(fe))
				
				# Convert file data to PDF (if necessary)
				if fileData:
					# Set the final path for PDF of attachment
					finalFilePath = outputFolder + File.separator + groupDocConvertedFileName + ".pdf"
					if logger.isDebugEnabled():
						logger.debug("finalFilePath: " + finalFilePath)

					if isImageFile:
						# Handle images
						stream = ByteArrayInputStream(fileData)
						img = ImageIO.read(stream)

						if logger.isDebugEnabled():
							logger.debug("convertImgToPDF: [" + groupDocConvertedFileName + "] [" + fileExtension + "]")
						
						ReportUtil.convertImgToPDF(outputFolder, groupDocConvertedFileName, img, stream, fileExtension, logger)

					elif isTextFile:
						# Handle text-based files
						stream = ByteArrayInputStream(fileData)
						
						if logger.isDebugEnabled():
							logger.debug("convertTextToPDF: [" + groupDocConvertedFileName + "] [" + fileExtension + "]")
						
						ReportUtil.convertTextToPDF(outputFolder, groupDocConvertedFileName, stream, fileExtension, logger)

					elif isPdfFile:
						# Handle PDFs (just write to disk)
						XMLUtils.writeBytesToXMLFile(fileData, finalFilePath, False)
					else:
						# Handle Word, Excel, and other document types
						tempFileName = outputFolder + File.separator + "tempFile." + fileExtension
						XMLUtils.writeBytesToXMLFile(fileData, tempFileName, False)
						convertByGroupDocs(tempFileName, finalFilePath)
					
					# Add to our vector of attachments to be concatenated
					pathAndReportFlag = PathAndReportFlag()
					pathAndReportFlag.filePath = finalFilePath
					pathAndReportFlag.isReport = False
					listOfFilesVector.add(pathAndReportFlag)

					if logger.isDebugEnabled():
						logger.debug("ReportUtil file path of attached file is: " + doclinks.getString("urlname"))
				else:
					logger.error("URL attachment " + attachUrl + " does not have file data")

			except Exception as e:
				logger.error("ReportUtils.convertAllPrintableAttachmentsOfThisMboToPDF error occured while converting attachment to pdf; probable problamatic url=" + doclinks.getString("urlname"))
				logger.error("e.getMessage(): " + str(e))
		
		# Move to the next attachment
		doclinks = doclinksSet.moveNext()
	
	# Return the document index
	return docIndex

# Create a logger
logger = MXLoggerFactory.getLogger("maximo.script." + service.getScriptName())
if logger.isDebugEnabled():
	logger.debug("Starting " + service.getScriptName() + " script")

# Test to see if we're coming from the REST API or not
try:
	request
except:
	request = None
try:
	where
except:
	where = "1=2"
try:
	maxRecords
except:
	maxRecords = -1

# Initialize Variables
if request:
	userInfo = request.getUserInfo()
	if request.getQueryParam("where"):
		where = request.getQueryParam("where")
	if request.getQueryParam("max"):
		maxRecords = request.getIntegerQueryParam("max")

# Report information
reportDesignName = "woprint.rptdesign"
reportApp = "WOTRACK"

# Output logging information
if logger.isDebugEnabled():
	logger.debug("where: " + where)
	logger.debug("maxRecords: " + str(maxRecords))
	logger.debug("ReportName: " + reportDesignName)

# Construct file paths and names
atomicIntegerForFilepath = AtomicInteger()
nextNumber = atomicIntegerForFilepath.incrementAndGet()
folderNameTemp = str(System.currentTimeMillis()) + "_" + str(nextNumber)
outputFolder = ReportRuntimeTempLocation.getGroupdocsLocation() + File.separator + folderNameTemp + "O"
woDirectPrintFileName = "woDirectPrint.pdf"
woDirectPrintFullPath = outputFolder + File.separator + woDirectPrintFileName
fileDir = File(outputFolder)
fileDir.mkdirs()

# Output logging information
if logger.isDebugEnabled():
	logger.debug("outputFolder: " + outputFolder)
	logger.debug("woDirectPrintFullPath: " + woDirectPrintFullPath)

# Call PDF generator
reportAdminService = MXServer.getMXServer().lookup("BIRTREPORT")
listOfFilesVector = Vector()

mboSet = None
reportData = None
try:
	# Get a set of Work Orders based on the where clause
	mboSet = MXServer.getMXServer().getMboSet("WORKORDER", userInfo)
	mboSet.setWhere(where)
	mbo = mboSet.moveFirst()
	
	docIndex = 0
	countIndex = 0
	while mbo:
		countIndex += 1
		woDetailsFileName = "woDetails-" + mbo.getString("WONUM") + ".pdf"
		woDetailsFullPath = outputFolder + File.separator + woDetailsFileName

		# Generate the PDF report
		parameterData = ReportParameterData()
		parameterData.addParameter("appname", reportApp)
		parameterData.addParameter("where", "(workorderid = " + str(mbo.getLong("WORKORDERID")) + ")")
		report = reportAdminService.runReport(userInfo, reportDesignName, reportApp, parameterData, woDetailsFileName, "pdf")

		# Write the report to the server
		XMLUtils.writeBytesToXMLFile(report, woDetailsFullPath, False)
		if logger.isDebugEnabled():
			logger.debug("PDF Report written for work order " + mbo.getString("WONUM"))

		# Add the report to the list of files vector
		docIndex += 1
		pathAndReportFlag = PathAndReportFlag()
		pathAndReportFlag.filePath = woDetailsFullPath
		pathAndReportFlag.isReport = True
		listOfFilesVector.add(pathAndReportFlag)

		# Generate PDFs for all printable WO attachments
		# docIndex = ReportUtil.convertAllPrintableAttachmentsOfThisMboToPDF(mbo, listOfFilesVector, outputFolder, docIndex, logger)
		docIndex = convertAllAttachmentsToPDF(mbo)

		# Break the loop if max records has been reached
		if maxRecords > -1 and countIndex > maxRecords:
			if logger.isDebugEnabled():
				logger.debug("Breaking out of record loop at " + str(countIndex) + " index")
			break
		
		# Move to the next work order in the set
		mbo = mboSet.moveNext()

	# Concat all of the PDF files together and cleanup
	ReportUtil.concatAttachmentPDFFiles(outputFolder, woDirectPrintFullPath, listOfFilesVector, logger)
	reportAdminService.deleteNeededFileFromAttachmentTempDirectory(outputFolder, listOfFilesVector, logger)

	# Read the final PDF into byte array
	reportData = XMLUtils.readXMLFileToBytes(woDirectPrintFullPath)

finally:
	# Clean up the set
	if mboSet:
		try:
			mboSet.cleanup()
			mboSet.close()
		except:
			logger.error("Failed closing mboSet")

# Send the response
if request:
	responseHeaders.put("content-type", "application/pdf")
	responseBody = reportData

if logger.isDebugEnabled():
	logger.debug("Finished " + service.getScriptName() + " script")

About

A3J Group is a leading solutions and consulting firm based in Tampa, Florida, specializing in Enterprise Asset Management (EAM) with a focus on fully developed Computerized Maintenance Management System (CMMS) solutions like IBM Maximo.

In addition to our suite of proprietary products—including MxMobile, our powerful mobile solution for Maximo—we provide traditional consulting services that help organizations plan, implement, optimize, and support their EAM strategies. Whether clients need hands-on configuration, strategic guidance, or ongoing system support, our team delivers the expertise to drive long-term success.

With over 50 years of combined experience, our specialized team is dedicated to our core mission: to serve as excellent stewards of our clients’ trust and investments, while driving measurable results through tailored, high-impact solutions and intelligent consulting.

Visit us at www.a3jgroup.com. Or drop us a line at info@a3jgroup.com.

Related posts

Michael Woline

Technology Project Manager (PMP 2009-18) with 15 plus years’ experience building, migrating, and upgrading enterprise applications including Maximo and Peoplesoft. Industries include banking, insurance, and publishing. Career began as a developer writing code and SQL because solving puzzles and figuring out how things works is fun. For fun I used to kite board on the beach, and have resumed cycling with a goal of riding some of the routes of the Giro d'Italia and Tour De France.

Nick Bonfiglio

Hi, I'm Nick! I am relatively new to Maximo, but I've been working in the software and IT space for a good stretch of my career. Most of that time I’ve spent as a QA Engineer at a SaaS company, so I'm no stranger to the technical side of things. Outside of work, I spend most of my time with family. I enjoy cooking and trying out new recipes whenever I get the chance, most of all I love cooking on a good grill! Living in Florida, I take full advantage of the beach as much as I can. I like to keep things pretty simple just good food, family, and some sun!

Akilah

Hi, I’m Akilah! I work with Maximo and come from a background in Computer Engineering, where I picked experience with SQL, Python, and general IT troubleshooting (aka figuring out why things break and how to fix them). I also served as a communications lead during my senior capstone, which helped me get comfortable collaborating with teams and keeping things moving.

Chad

Chad started his CMMS and EAM solutions consulting career in the late 20th century. The past twenty-three years have primarily been focused on supporting Maximo in Life Sciences, Oil & Gas, Government facilities, and Manufacturing verticals. Chad especially enjoys discussions regarding logistics, inventory management, and process improvement. In his free time, he enjoys hunting, fishing, playing guitar, maintaining his small farm, and spending time with his family.

Mike

Mike is a jack-of-all-trades IT professional with 20+ years in manufacturing, starting as a Help Desk temp and working his way through admin, engineering, management, and architect roles. He loves solving problems and building tools to make work easier—Maximo gives him plenty of chances to do both. In his free time, Mike enjoys woodworking, the outdoors, playing synths and hand drums, reading, and learning. Given his druthers (whatever those are), he'd build a timber-framed cabin on 40+ acres of mixed hardwood forest for his future grandkids to work for free play in.

Shane

Maximo Analyst

With a background in Computer Science and Cybersecurity and a CompTIA Security+ certification, I bring both a technical foundation and a curiosity-driven mindset to everything I do. Since joining the industry, I’ve gained hands-on experience with IBM Maximo, including scripting, BIRT reporting, Maximo customization, configuration, and supporting long-term client projects. I've also attained my MAS8 certification. I’ve had the opportunity to work closely with clients, troubleshoot complex issues, and contribute to impactful system enhancements—all while continuously learning and growing in the field. I approach challenges with determination, thrive in collaborative environments, and enjoy finding creative solutions. Outside of work, I recharge by spending time with family and friends, playing video games, staying active, and experimenting in the kitchen.

Maximo Application Suite

Accelerated Time to Value
Lower Total Cost of Ownership
Scalable, Integrated Platform
Continuous Innovation & Updates

Visual Inspection

Accelerate Defect Detection
Flexibility to Train and Deploy
Increase Uptime 24/7 Real Time Monitoring
Better Worker Safety

Monitor

Enterprise Level Data Aggregation
Scaled Operations
AI Deductions
Advanced Root Cause Analysis

Predict

Condition Based Maintenance
Lower Costs
Better Asset Availability
Reduced Risk

Manage

Comprehensive View of Assets
Understand Resource Attributes, Configuration and Relationships
Drill Down View

Health

Improve Reliability
Performance Insight
Better Planning
Clean Data
Optimization – Data, Costs, Risks

Mobility

Boost Productivity
Safer Workplace
Reduce Knowledge Gaps
Avoid Downtime

Maximo Assist

AI-Powered Troubleshooting & Guidance
Faster Issue Resolution in the Field
Knowledge Capture & Reuse Across Teams
Smarter Decisions with Contextual Insights

Dexter

Maximo Analyst

Dexter began his career in the warehouse field. Developing his skills in inventory control, accounts payable, shipping, and receiving, and procurement. While working at Apex he was learning the Maximo system in and out. Eventually learning it so well he taught everyone how to use it. Eventually becoming a system administrator for the Apex site.

A fun fact about Dexter is that he was an Elvis impersonator for 24 years. He enjoys singing, fishing, golf, and playing pool. He's an avid movie buff, most likely you'll find him watching the latest movie.

Chip

Client Relations Specialist

Chip is new to Maximo but has worked for various software companies through his career in both a sales and client relations role. In his spare time, Chip likes to golf, travel and work on projects around the house, especially those with an artistic touch. He is also up for any type of competition whether that be playing cards, board games or recreational sports.

Sarah

Maximo Analyst

Sarah is a recent graduate starting her asset management career. She counts fast learning and determination as key skills that she’s applying to become an Maximo pro! Sarah’s background in IT Services and studies in computer science have strengthened her natural problem-solving skills and desire to help others through technology. In her free time, Sarah enjoys baking, sewing, reading comic books, and building her own video game.

Vishal

Maximo Analyst

Hey there! I’m a Maximo Developer with hands-on experience working across IBM Maximo 7.5.x, 7.6.x, MAS 8, and MAS 9. My focus is mainly on database administration, system integrations, automation scripting, UI customizations, and Maximo admin tasks like BIRT reporting and migration management. I’ve also dabbled in integration work and enjoy finding smart, efficient solutions to everyday challenges in Maximo.

When I’m not working, you’ll probably find me gaming on my PS5 over the weekend or exploring the latest tech trends to keep myself up to speed.

Xavier Galarza

Maximo Analyst

I'm a motivated and curious professional with hands-on experience across multiple Maximo projects, including leading several initiatives. My goal is to continue deepening my expertise in Maximo so I can guide others in unlocking its full potential. I am also fully certified in MAS8. With a strong background in development and a natural problem-solving mindset, I enjoy tackling challenges and learning from those around me. I'm passionate about continuous improvement—both technically and personally—and thrive when I'm surrounded by talented, knowledgeable peers. Outside of work, I enjoy playing video games, watching sports, and staying active with pickleball.

Courtney LeBlanc

Business Analyst

Courtney is a polished Maximo consultant with over 10 years working in the industry. She has worn many hats including QA Specialist, Trainer, Functional Lead, Project Lead and Business Analyst. A point of great professional pride for Courtney is helping clients meet their needs and realize the power of their investment. Being a part of the process that helps clients learn how to use their system and get the most out of it is a great feeling at the end of the day.

Bob Richardson

VP of Products (Ninja)

Bob leads A3J Group’s operational services and heads up our Maximo Ninja product line. With over 50 successful Maximo projects under his belt and MAS 8 certifications in both functional and technical tracks, Bob brings deep experience and steady leadership to every engagement. He has played a key role in the development of many of A3J Group’s most advanced solutions.
Outside of work, Bob enjoys traveling with his wife, spending time outdoors, and occasionally pondering life’s mysteries from his garage—possibly while thinking about squirrels.

Kelly Nimmo

VP of Products (MxMobile)

Kelly Nimmo leads A3J Group’s product development and serves as a senior analyst and consultant with over 14 years of IBM Maximo experience. She is a co-architect and lead developer of MxMobile, our flagship mobile solution, and holds MAS 8 certifications in both functional and technical disciplines. Known for blending technical depth with practical insight, Kelly plays a key role in designing and delivering innovative Maximo-based tools. Outside of work, she enjoys fishing the Florida waters and teaching soccer skills—like the “around the world”—to her nieces and nephews.

Kim Walter

COO

Kim began her career in Investment Banking in the Biotech and Tech sectors. After leaving banking, she focused on executive strategic development evolving her business building skills. Co-founding A3J Group was a natural fit with the idea of creating a fierce ninja style unit that outperforms all others. In her free time, she is training for her next eventing competition or studying to become a Master of Wine or practicing her tornado kick. She could be with her family hiking or surfing or cooking. But she’s definitely not sitting around doing nothing.

Alex Walter

CIO

Alex brings 18 years of Maximo/EAM consulting experience in Life Sciences, Oil & Gas, Water/Waste Management, Government facilities, and much more. He has been awarded the honor of IBM Champion from 2018-2021, and is the brainchild behind our revolutionary mobile solution MxMobile.

In his free time he buys camping gear, enjoys a good circular saw and spends time with his family hiking, surfing and being outdoors.

Enhancement

• Configuration
• Integration & data loading
• Report improvement
• Screen view & workflow changes

Sustainability

• Report
• Troubleshoot
• Learn
• Teach
• Lead

Change Management

• Advanced systematic improvements
• Tailored procedure implementation
• Script-, report-, and application-level source and document control through Git repository software
• Training on how to document and deploy changes

Monitoring

• Active monitoring of your IBM Maximo system’s heartbeat
• Triggered notifications and support tickets when Maximo is not responding
• Automated Escalations based on the health of various critical background processes.
• Periodic and quarterly scheduled system health checks

System Support & Trouble Shooting

•On Demand Support
•Advanced Troubleshooting
•Access to A3J Group’s Maximo Support Environment to submit trouble tickets
•Directly report bugs, request fixes and enhancements

System Patching & Environment Refreshes

• Tentative, bi-annual feature pack releases from IBM
• Minor patches (defined by the last number in the version model)
,• Quarterly lower environment refreshes

Admin

MxAsk

Create new self-service requests in a wizard-style format, and view the details of their open service requests.

MxApprove

Accept, reject, and reassign assignments, review assigned records, and view your workflow inbox.

MxMobile

A suite of mobile applications designed to integrate with IBM Maximo Asset Management. These apps modernize workflow, expedite inventory processes, reduce time spent on data entry, and more.

Workflow

MxMeter

View and enter meter readings, view trends in graphical format, and create readings while in offline mode.

MxAsset

Scan barcodes and view maintenance facts. Create meter readings, work orders or service requests. Toggle asset downtime.

MxWork

Create new work orders on the go, view existing work orders and attachments, and capture fingertip electronic signatures.
0