cancel
Showing results for 
Search instead for 
Did you mean: 
cancel
Showing results for 
Search instead for 
Did you mean: 

ThingWorx Navigate is now Windchill Navigate Learn More

Translate the entire conversation x

How to Automate an Excel-Based Form with PDF Generation in ThingWorx?

MA8731174
16-Pearl

How to Automate an Excel-Based Form with PDF Generation in ThingWorx?

Hello Community,

I’m currently got a new  use case where we have an existing Excel form that includes multiple text fields and checkboxes. The goal is to digitally fill out this form and then generate a PDF from it — all within a ThingWorx-based environment.

Here’s the idea I’m considering:

  • Convert the Excel structure into an HTML representation (including form fields and styling).

  • Open this HTML form dynamically in the browser via ThingWorx.

  • Allow users to fill in the fields (text, checkboxes, etc.).

  • Finally, generate and save the completed form as a PDF.

My question is:
Is this workflow achievable directly in ThingWorx?
Has anyone implemented something similar using ThingWorx + HTML + PDF generation?
 What are the best practices  I can consider for this?

 

Note: We have around 80 plus excel files of customer which they normally fill out and save as pdf. I cannot make it static checklist in vuforia studio because there can be in the future 100 plus excels which needs to be automate. So basically its just digitalization of checklist which needs to be filled and saved as pdf thats it. 

 

Any input, examples, or suggestions would be greatly appreciated!

Thanks in advance,

In my this post of mine , i was successfully able to open HTML in browser from thingworx
https://community.ptc.com/t5/ThingWorx-Developers/Generate-a-Pre-Filled-HTML-Template-in-Browser-for-PDF-Export-Is/m-p/1018231#M69880

ACCEPTED SOLUTION

Accepted Solutions

Hello @MA8731174,

 

I would look in a different direction if I were you. I'd rather hack something like this:

 

  1. When the user needs to fill in a form, copy the corresponding template-123.xlsx in OneDrive using Microsoft Office APIs
  2. Share this file, so that anyone with a link can edit it. Get that link using another API.
  3. Open this link in a standalone browser tab, so that the user can fill in the actual Excel form in a web version of Office 365
  4. Let them click some button in ThingWorx UI (e.g. "Complete") once they filled it and closed that Office tab
  5. Call another Office 365 API to generate a PDF out of an XSLT, download that PDF

In other words, I would delegate to MS Office for all that Excel filling and PDF generation. This all assumes that all of that is exposed via REST APIs, and it is easy to use it, which is a very big assumption. It probably isn't easy at all.

 

/ Constantine


Vilia (my company) | GitHub | LinkedIn

View solution in original post

17 REPLIES 17

Hello @MA8731174,

 

I would look in a different direction if I were you. I'd rather hack something like this:

 

  1. When the user needs to fill in a form, copy the corresponding template-123.xlsx in OneDrive using Microsoft Office APIs
  2. Share this file, so that anyone with a link can edit it. Get that link using another API.
  3. Open this link in a standalone browser tab, so that the user can fill in the actual Excel form in a web version of Office 365
  4. Let them click some button in ThingWorx UI (e.g. "Complete") once they filled it and closed that Office tab
  5. Call another Office 365 API to generate a PDF out of an XSLT, download that PDF

In other words, I would delegate to MS Office for all that Excel filling and PDF generation. This all assumes that all of that is exposed via REST APIs, and it is easy to use it, which is a very big assumption. It probably isn't easy at all.

 

/ Constantine


Vilia (my company) | GitHub | LinkedIn

Out of curiosity I did a quick check, and it seems like all those REST APIs are indeed available:

/ Constantine


Vilia (my company) | GitHub | LinkedIn

Thank @Constantine  for your feedback but for this process would you please also explain where should i call these apis? you meant with MS Azure ? I am a bit confused about the process to implement as i have never used MS Office APIs before. would you elaborate a bit how should i start implementing this approach?

The flow would be like this:

  • The user clicks "Fill claim" button. This triggers a ThingWorx service.
  • In that ThingWorx service, you call Microsoft Office 365 APIs 1 and 2 to create an empty form and share it by link. You return that link from the service into the mashup.
    • Note that you'd need to authenticate against MS Office 365 somehow -- this might not be trivial, as they likely use some OAuth and you'd need to create some service account in your AD, generate and store a refresh token, etc. etc.
  • The mashup takes that URL and passes it to a Navigation function of type "Modal popup", for example.
  • This popup loads an Office 365 spreadsheet (check that its CORS policy supports IFRAMES! If it doesn't -- you'd need to use "New tab" navigation tab).
  • The user fills in the spreadsheet and clicks some "Finish" button in your wrapper mashup.
  • This button triggers another ThingWorx service, which calls API #3 and downloads the PDF from Office 365.

 

Here I make a bunch of important assumptions:

 

  • That you have an Office 365 subscription (for example, your company's one),
  • That your IT will allow and help you to create a service account in the corresponding ActiveDirectory, so that you can use it to authenticate those REST calls,
  • That your IT policy allows sharing OneDrive items with anyone by link,
  • That it doesn't violate Microsoft EULA.

 

On a side note, regardless of the approach you take, I think it's a pretty complex feature. You are likely looking at several weeks of implementation work.

 

/ Constantine


Vilia (my company) | GitHub | LinkedIn

Thank you so much @Constantine for your detailed recommendation – your suggested approach is working well so far, and I’m making good progress with the implementation.

I’m not quite finished yet, but I’ll be sure to update you once everything is working or if I run into any further questions.

 

Really appreciate your support and guidance!

 

No problem! Don't want to sound like an ass, but I'll be positively surprised if it works ))

 

Looking forward for an update.


Vilia (my company) | GitHub | LinkedIn

MA8731174_0-1754460883170.png

 

As you can see above when i click on template button which triggers navigation and i can see the excel file in mashup after doing copy file and generating link all in the background.. now i cannot find any event in navigate with which i can close this navigation with button from mashup. What i have found so far is that i can close the popup which is chrome itself popup and then this popupCancelled event get called but i would like may be something on mashup may be to close the popup of excel. what do you think about it?

MA8731174_1-1754460969622.png

 

I don't think there's much you can do about it. First of all, Office's CORS policy won't allow you to embed this form in your mashup. Second, you can't open / close windows from mashup Expressions, as they explicitly block "window" object.

 

The only solution I can think of would be to craft a simple UI extension, which would allow doing something like this:

 

  • "Open" service --> this.openedWindow = window.open(this.getProperty('url'));
  • "Close" service --> this.openedWindow.close();

It's literally a ten lines of code extension. If you've never done ThingWorx UI extensions, you can either go through Help, or use something like this as a quickstart: https://github.com/vilia-fr/twx-infotable-to-json-ext

 

/ Constantine


Vilia (my company) | GitHub | LinkedIn

Just wanted to give a quick update regarding the process:

I click the button in the mashup to open the Excel file (via link), make some changes, and then close the window. On closing, I trigger the PDF generation service to create, save in repository and display the PDF on the mashup.

However, I noticed an issue: approximately once in every 15 tries, the changes made in the Excel file do not appear in the generated PDF. To resolve this, I have to reopen the Excel file—then the changes are visible quickly in excel which i did before, and when I close it again, the PDF reflects them correctly.

This inconsistency is a bit of a blocker for me at the moment. I also tried adding pause(2000) before calling the PDF service, but the issue still occurs occasionally. I feel like its not about pdf service , its about excel saving the content.... 

I am testing it with a very basic Excel file where I only change the value in one cell before closing, and this is the behaviour. I hope i would find a way here...

Try something -- when you reproduce this behavior, before opening this spreadsheet in the browser again, try to open it in another browser, or in the Private mode. See if you can still see your changes. Maybe they stay in your browser's local storage before being synced to the backend. In this case that's something you'd need to google for, e.g. "how to flush Office 365 changes to OneDrive" -- there might be some frontend trick to do this.


Vilia (my company) | GitHub | LinkedIn

I did some testing using two different browsers — I opened the mashup in Chrome and had the same file open in Edge. In both cases, changes made in one tab were instantly visible in the other, so it was clearly not a browser cache issue.

Afterward, I tried using the lastModifiedDateTime parameter provided by the Graph API with the file metadata. I compared that value with Date.now() and added a wait loop that only proceeds once the lastModifiedDateTime has changed.

 

},
            "file": {
                "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                "hashes": {
                    "quickXorHash": "rf5vEi9WxaBtqa8W7kKsKpej/Io="
                }
            },
            "fileSystemInfo": {
                "createdDateTime": "2025-08-05T08:53:09Z",
                "lastModifiedDateTime": "2025-08-07T09:18:56Z"
            },
            "shared": {
                "scope": "users"
            }

 

 With this approach, everything seems to be working well now. I'm still testing, but so far it's looking stable and reliable.

 

My javascript service in thingworx as below:- which waits untill the lastmodifieddatetime changes for file in onedrive 

 

let lastModifiedDateTime = Things["SmartPDFAssist.APICalls.Controller"].getFilelastModifiedDateTime(); // e.g. "2025-08-07T08:16:12Z"


let lastModified = new Date(lastModifiedDateTime.lastModifiedDateTime);
let now = new Date();
let result;
let diffInMs = now.getTime() - lastModified.getTime();
let diffInSeconds = diffInMs / 1000;


if (Math.abs(diffInSeconds) < 1) {
    executeService();
    
    logger.info("Exact same datetime (to the second).");
    logger.info(now.getTime() - lastModified.getTime());
    result="Exact same datetime (to the second).";
}


else if (diffInSeconds > 5) {
    
    for (let i=0;i<14; i++){
        pause(1000);
        lastModifiedDateTime = Things["SmartPDFAssist.APICalls.Controller"].getFilelastModifiedDateTime();
        logger.info("wait for the " + i + " time");
        if (now.getTime() <= new Date(lastModifiedDateTime.lastModifiedDateTime).getTime()) {
            break;
        }
    }
    
    executeService();
    
    logger.info("Last modified time is older than 5 seconds.");
    result ="Last modified time is older than 5 seconds.";
    
}


else if (diffInSeconds > 0 && diffInSeconds <= 5) {
      //pause(6000);
    executeService();
   
    logger.info("Last modified time is less than 5 seconds behind current time.");
   result ="Last modified time is less than 5 seconds behind current time.";
}


else   if(diffInSeconds < 0) {
       
   executeService();
    logger.info(" Last modified time is in the future compared to now.");
   result = " Last modified time is in the future compared to now.";
 
}

else {

result = "no condition worked";
}






function executeService() {
    let pdfFile = Things["SmartPDFAssist.APICalls.Controller"].saveOneDriveFileAsPDF();
    Things["SmartPDFAssist.FileRepository"].SaveBinary({path:"/test/files/testfile.pdf",content:pdfFile});
    
}

 

@MA8731174 First of all, congratulations! I am positively surprised it worked at the end, well done!

 

Second, keep in mind that this approach is very sensitive to your local clock being correct. Make sure you have an NTP service ("Windows Time Service") running on your server, so that your clock is synchronized!

 

Third, this logic can be streamlined:

let elapsed = function() {
    return now.getTime() - new Date(Things["SmartPDFAssist.APICalls.Controller"].getFilelastModifiedDateTime().lastModifiedDateTime).getTime();
}

while ((ms = elapsed()) > 1000) {
    // The last modification is older than 1s, which means OneDrive hasn't saved the file yet
    if (ms > 15000) {
        throw new Error("FATAL: OneDrive didn't save the file within 15 seconds - please retry");
    }
    pause(500);  // Let's wait a bit longer... The delay should be < 1000ms to avoid infinite loops.
}

 

/ Constantine


Vilia (my company) | GitHub | LinkedIn

Thank you so much for continuously supporting! Glad to be part of such a great helping community . So as you said the time approach can be tricky i have found another thing which is

 

eTag: The etag represents the overall state of an item (metadata and content)

cTag: The ctag specifically tracks changes to the item's content. 

 

eTag and cTag we also get back as we get lastmodifieddatetime so i just try to use them instead ....so on close excel file i get the present eTag and cTag which is in graphAPI saved and when that service completed i call the next service which returns me eTag and cTag again which is after closing the file and i compare them with my on close values and if they are not changed take delay and check again untill they are change.... i have delay upto 45 or 50 seconds.... one thing which i have found very weired that even though eTag and cTag are changed the pdf changes can still take a second to be fully saved in the cloud of DRIVE ... even though i already had 2 seconds delay before we get our pdf still it came once wrong...so i just put another 2 seconds delay before calling the pdf service as you can see below.... 

 

I am doubting a bit on my approach now. i dont know either it is right or can cause problems later.... i have tested now with this code now pdf is being generated right with latest changes.... i have tested it around 50 times now.. what do you think about it?

 

 

// ========= Pre-close snapshot (baseline) =========
// Grab the current tags *right when the user closes the Excel window*.
// We will wait until OneDrive shows a version newer than this.
var baseMeta = Things["SmartPDFAssist.Data.Controller"].getSourceFileCTag({ folderName: "", fileName: "" });
// Expecting baseMeta to include eTag / cTag (and optionally hashes.quickXorHash)
var baseETag = baseMeta.eTag;
var baseCTag = baseMeta.cTag;
var baseHash = (baseMeta.hashes && baseMeta.hashes.quickXorHash) ? baseMeta.hashes.quickXorHash : undefined;

logger.info("Baseline -> eTag: " + baseETag + " | cTag: " + baseCTag + " | hash: " + (baseHash || "-"));

// ========= Backoff schedule (balanced correctness vs speed) =========
// Total wait ~60–70s max. Adjust if you see longer commits in practice.
var waits = [1000, 1000, 2000, 3000, 5000, 8000, 13000, 15000];

var ready = false;
var meta, curHash;

// ========= Poll until tags/hash change from baseline =========
for (var i = 0; i < waits.length; i++) {
pause(waits[i]); // wait before checking again to let OneDrive commit/version-bump

meta = Things["SmartPDFAssist.Data.Controller"].getSourceFileCTag({ folderName: "", fileName: "" });
curHash = (meta.hashes && meta.hashes.quickXorHash) ? meta.hashes.quickXorHash : undefined;

var changed =
(meta.eTag !== baseETag) ||
(meta.cTag !== baseCTag) ||
(!!baseHash && !!curHash && curHash !== baseHash); // use content hash if both sides available

logger.info(
"Wait " + (i + 1) + "/" + waits.length + " (" + waits[i] + " ms) -> " +
"eTag: " + meta.eTag + " (base " + baseETag + "), " +
"cTag: " + meta.cTag + " (base " + baseCTag + "), " +
"hash: " + (curHash || "-") + " (base " + (baseHash || "-") + "), " +
"changed=" + changed
);

if (changed) {
// small settle time even after version bump to avoid race with export
pause(2000);
ready = true;
break;
}
}

// ========= Generate PDF =========
if (ready) {
executeService();
logger.info("PDF generated after OneDrive version bump was detected.");
} else {
logger.warn("Timed out waiting for OneDrive version bump (~>60s). Generating PDF anyway.");
// IF no change has been done that means the cTag eTag would not change then we show the old pdf already.
executeService();
}

// ========= Helper =========
function executeService() {
pause(2000);
var pdfFile = Things["SmartPDFAssist.APICalls.Controller"].saveOneDriveFileAsPDF();
Things["SmartPDFAssist.FileRepository"].SaveBinary({
path: "/test/files/testfile.pdf",
content: pdfFile
});
}

 

If we take a step back... do you need a PDF immediately? Maybe it's enough to store a "promise" in your database and generate PDF on the fly at the moment when you need to read it? Lazy PDF generation of sorts. If you don't clean up those spreadsheets in OneDrive, you should be able to generate PDF anytime later, even in one or two years... Taking it even further, you don't even need to store those PDFs in ThingWorx, you can always generate them anew when you need them.


Vilia (my company) | GitHub | LinkedIn

Another wild idea -- ThingWorx mashups are just JSONs, which you can generate yourself, if you want to. You might be able to create some script (e.g. an Excel macro) that would convert your spreadsheet into a mashup for you. This assumes you know how to program Office macros well.


Vilia (my company) | GitHub | LinkedIn
Rocko
19-Tanzanite
(To:MA8731174)

Just an idea, if you have Analytics installed, you might want to look into the Analytics Manager which comes with an Excel Connector. This could get you the Excel populated and computed, but for the last step - is rendering an Excel as a PDF really something you want your IIOT platform to perform? 

https://support.ptc.com/help/thingworx/analytics/r10/en/#page/analytics/analysis_services/AnalysisServices_WorkingWithProviders.html#

 

Hello @MA8731174,

 

It looks like you have some responses from some community members. If any of these replies helped you solve your question please mark the appropriate reply as the Accepted Solution. 
Or if you were able to figure out the issue, please help to document it and mark the reply as your accepted solution. This will surely help other community members who may have a similar question.


Thanks,
Vivek N.
Community Moderation Team.

Announcements


Top Tags