Dynamics CRM 365 : Downloading a Word Document Template via a Button on the Ribbon - ms-word

Currently users have to click the ellipses, word templates, and finally quote to download the word template.
To make it easier for our users we would like to have the document download when pressing the "print quote" button on the ribbon.
Is this possible? If so how would I go about doing this? I understand how to edit the ribbon using the ribbon workbench. I need to know how to download a word template using the ribbon.
If the solution is using the ribbon workbench, what command can I enter to get the word template to download?

When you click the templates flyout, it's dynamically populated through an invocation of /AppWebServices/DocumentTemplate.asmx, which returns the XML for the menu.
The flyout for Word Templates in the Incident home page grid looks like this:
<Menu Id="incident|NoRelationship|HomePageGrid|Mscrm.HomepageGrid.incident.WordTemplates.Menu">
<MenuSection Id="incident|NoRelationship|HomePageGrid|Mscrm.HomepageGrid.incident.WordTemplates.Menu.CreateTemplates" Title="Create Word Template" Sequence="10" DisplayMode="Menu16">
<Controls Id="incident|NoRelationship|HomePageGrid|Mscrm.HomepageGrid.incident.WordTemplates.Menu.CreateTemplates.Controls">
<Button Id="incident|NoRelationship|HomePageGrid|Mscrm.HomepageGrid.incident.WordTemplates.Menu.CreateTemplates.Controls.00000000-0000-0000-0000-000000000000" Command="incident|NoRelationship|HomePageGrid|Mscrm.WordTemplate.CreateWordTemplate.Grid" Sequence="10" ToolTipDescription="Create Word Template" Alt="Create Word Template" LabelText="Create Word Template" />
</Controls>
</MenuSection>
<MenuSection Id="incident|NoRelationship|HomePageGrid|Mscrm.HomepageGrid.incident.WordTemplates.Menu.WordTemplates" Title="Word Templates" Sequence="20" DisplayMode="Menu16">
<Controls Id="incident|NoRelationship|HomePageGrid|Mscrm.HomepageGrid.incident.WordTemplates.Menu.WordTemplates.Controls">
<Button Id="incident|NoRelationship|HomePageGrid|Mscrm.HomepageGrid.incident.WordTemplates.Menu.WordTemplates.Controls.9b77c5b0-1033-4741-a01c-afdbdb1c3f22" Command="incident|NoRelationship|HomePageGrid|Mscrm.WordTemplate.TemplatesMenu.Grid" Sequence="10" ToolTipDescription="Case Summary" Alt="Case Summary" LabelText="Case Summary" />
</Controls>
</MenuSection>
</Menu>
I don't have the means to try it out at the moment, but I'd try and "copy" the last <Button>:
<Button Id="incident|NoRelationship|HomePageGrid|Mscrm.HomepageGrid.incident.WordTemplates.Menu.WordTemplates.Controls.9b77c5b0-1033-4741-a01c-afdbdb1c3f22" Command="incident|NoRelationship|HomePageGrid|Mscrm.WordTemplate.TemplatesMenu.Grid" Sequence="10" ToolTipDescription="Case Summary" Alt="Case Summary" LabelText="Case Summary" />

It's possible to do this using only supported features of CRM (of course I'm sure it's also possible to do using unsupported javascript, but I don't have time currently to investigate this). The steps that you should take to achieve the functionality you want:
Create new process of type Action, bound to entity that you want to
create a template for (the reason why I suggest Action here, is that
it can be easily invoked using JavaScript and CRM WebAPI)
In this Action add single step - invoke an Action and choose
built-in action "SetWordTemplate"
Set Properties of this action - choose the template that you need
and dynamically set the target to current entity (using Dynamic
Values assistant) If you never used this action - it simply creates
a given word template and adds it as an annotation to your entity
Now you need to write logic inside your button (I'm assuming you
know how to add a button using Ribbon Workbench or whatever)
Call your action using WebAPI
Find annotation that was just created for your entity with the
attached document
Download the attachment (you can show some prompt for the user or
simply force the download the file, user will have to save it)
Delete the annotation
Maybe not a one-liner, but keeps you in the supported zone...

ExecuteWordMerge = function (wordtemplateid, entitytypecodeint, ids, templatetype, fieldforfilename, filenameoverride) {
try {
Xrm.Page.ui.clearFormNotification("worderror");
var funcpath = Xrm.Page.context.getClientUrl() + "/_grid/print/print_data.aspx";
if (typeof ids !== "object") {
var tids = ids;
ids = new Array();
ids.push(tids);
}
var wordTemplateId = wordtemplateid;//"f1f7b994-543b-e711-8106-c4346bac2908" test data;
var currentEntityTypeCode = entitytypecodeint;//"10063" test data;
var templateType = (templatetype || 9940); //9940 is global and 9941 is personal
var fieldForFileName = (fieldforfilename || "");
var formdata = "exportType=MergeWordTemplate&selectedRecords=" + encodeURIComponent(JSON.stringify(ids)) +
"&associatedentitytypecode=" + currentEntityTypeCode + "&TemplateId=" + wordTemplateId + "&TemplateType=" + templateType;
var req = new XMLHttpRequest();
req.open("POST", funcpath, true);
req.responseType = "arraybuffer";
req.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
req.setRequestHeader("Accept-Language", "en-US,en;q=0.8");
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
req.onreadystatechange = function () {
if (this.readyState == 4) {/* complete */
req.onreadystatechange = null;
if (this.status >= 200 && this.status <= 299) {//200 range okay
var mimetype = (2 === 2) ? "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
var blob = new Blob([req.response], { type: mimetype });
var fileNameTemplate = req.getResponseHeader('content-disposition').split('filename=')[1].replace(/'/g, "");
var dloadurl = URL.createObjectURL(blob);
var filename = (fieldForFileName !== "" && Xrm.Page.getAttribute(fieldForFileName) !== null && Xrm.Page.getAttribute(fieldForFileName).getValue() !== "") ?
Xrm.Page.getAttribute(fieldForFileName).getValue() : fileNameTemplate;
filename = filenameoverride || filename;
//new code, prevent IE errors
if (navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(blob, filename);
return;
}
else if (window.navigator.msSaveBlob) { // for IE browser
window.navigator.msSaveBlob(blob, filename);
return;
}
var a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
a.href = dloadurl;
a.download = filename;
a.click();
URL.revokeObjectURL(dloadurl);
//window.location = dloadurl;//we can use just this instead of creating an anchor but we don't get to the name the file
}
else {
Xrm.Page.ui.setFormNotification("An Error occurred generating the word document, please contact support if the issue persists,code: " + this.status, "ERROR", "worderror");
}
}
};
req.send(formdata);
}
catch (err) {
Xrm.Page.ui.setFormNotification("An Error occurred generating the word document, please contact support if the issue persists. " + err.message, "ERROR", "worderror");
}
}

Just to simplify #TeamEASI.com answer a little here is what I did.
Add a button to the ribbon using XRMToolBox Ribbon Workbench 2016.
Create a JS web resource like the one bellow.
/*
* Author: Matthew Hunt
* File: vsi_DownloadTemplate.js
* Date: 12/20/2017
* Project: CRM USA
* Description: DownloadTemplate() allows the user to download a document template
* via a button on the ribbon.
*
* #param entitytypecode: the type code of the entity. In the ribbon workbench set a
* CRM parameter with value PrimaryEntityTypeCode. ex: 1063
*
* #param templateid: the id for the template you want to download. I had to go to
* the database to find this and pass it as a string parameter in the ribbon workbench.
* For example:
* SELECT DocumentTemplateId, Name FROM dbo.DocumentTemplateBase WHERE Name Like '%Quote%';
* returns something like 4AB391A4-D247-E711-80D3-005056914EA2
* Unforunatly, anytime the template is updated, you'll probably have to get the new id.
*
* #param templatetype: the code for the template type. Pass this value in the ribbon
* workbench as a int param. ex: 9940 is a documenttemplate
*
* #param filename: the resulting name of the file that will be downloaded to the users
* computer. Pass this value in the ribbon workbench as a string param. ex: Quote.docx
*
*/
function DownloadTemplate(entitytypecode, templateid, templatetype, filename){
// retrieve the entity id from the current page
var entityid = new Array();
entityid.push(Xrm.Page.data.entity.getId());
// try and make a request for the document template
try{
// clear the page of any previous errors
Xrm.Page.ui.clearFormNotification("docerror");
// the path that will be used to retrieve the word template
var funcpath = Xrm.Page.context.getClientUrl() + "/_grid/print/print_data.aspx";
// open the request to create the template
var req = new XMLHttpRequest();
req.open("POST", funcpath, true);
req.responseType = "arraybuffer";
req.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
req.setRequestHeader("Accept-Language", "en-US,en;q=0.8");
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
// on completion, run the bellow function
req.onreadystatechange = function () {
// request complete
if (this.readyState == 4) {
req.onreadystatechange = null;
// check if we got back a 200 from the request
if (this.status >= 200 && this.status <= 299) {
// add the download url to an a tag and then click the a tag
// to download the document
var mimetype = (2 === 2) ? "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
var blob = new Blob([req.response], { type: mimetype });
var dloadurl = URL.createObjectURL(blob);
var a = document.createElement("a");
// if ie, because ie sucks
if (navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(blob, filename);
// else a browser that doesn't suck
} else {
document.body.appendChild(a);
a.style = "display: none";
a.href = dloadurl;
a.download = filename;
a.click();
URL.revokeObjectURL(dloadurl);
}
}
};
// compile the data to send with the request
var formdata = "exportType=MergeWordTemplate&selectedRecords=" + encodeURIComponent(JSON.stringify(entityid)) +
"&associatedentitytypecode=" + entitytypecode + "&TemplateId=" + templateid + "&templatetype=" + templatetype;
// make the request to create the template
req.send(formdata);
}catch (err) {
PrintError(err.message);
}
}
/*
* PrintError() is a helper method to display any errors to the user.
*/
function PrintError(msg){
Xrm.Page.ui.setFormNotification("An Error occurred generating the word document, please contact support if the issue persists. " + msg, "ERROR", "docerror");
}
IE fix: .click() giving access denied in IE11
Create a command using XRMToolBox Ribbon Workbench 2016 with the following parameters to execute the JS when the button is clicked.

With the new version of CRM this javascript code, need to be amened to remove the unsupported API, as well some addtional changes to be able to work also with CHROME.
below my working version,
/*
* Author: Matthew Hunt
* Changes: Philippe Guarino
* File: vsi_DownloadTemplate.js
* Date: 22/09/2021
* Project: CRM USA
* Description: DownloadTemplate() allows the user to download a document template
* via a button on the ribbon.
*
* #param entitytypecode: the type code of the entity. In the ribbon workbench set a
* CRM parameter with value PrimaryEntityTypeCode. ex: 1063
*
* #param templateid: the id for the template you want to download. I had to go to
* the database to find this and pass it as a string parameter in the ribbon workbench.
* For example:
* SELECT DocumentTemplateId, Name FROM dbo.DocumentTemplateBase WHERE Name Like '%Quote%';
* returns something like 4AB391A4-D247-E711-80D3-005056914EA2
* Unforunatly, anytime the template is updated, you'll probably have to get the new id.
*
* #param templatetype: the code for the template type. Pass this value in the ribbon
* workbench as a int param. ex: 9940 is a documenttemplate
*
* #param filename: the resulting name of the file that will be downloaded to the users
* computer. Pass this value in the ribbon workbench as a string param. ex: Quote.docx
*
*/
function DownloadTemplate(entitytypecode, templateid, templatetype, filename, formContext)
{
// var formContext = executionContext.getFormContext(); // get formContext
// retrieve the entity id from the current page
var entityid = new Array();
entityid.push(formContext.data.entity.getId());
// try and make a request for the document template
try
{
// clear the page of any previous errors
formContext.ui.clearFormNotification("docerror");
// the path that will be used to retrieve the word template
var globalContext = Xrm.Utility.getGlobalContext();
var funcpath = globalContext.getClientUrl() + "/_grid/print/print_data.aspx";;
// open the request to create the template
var req = new XMLHttpRequest();
req.open("POST", funcpath, true);
req.responseType = "arraybuffer";
req.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
req.setRequestHeader("Accept-Language", "en-US,en;q=0.8");
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
// on completion, run the bellow function
req.onreadystatechange = function ()
{
// request complete
if (this.readyState == 4)
{
req.onreadystatechange = null;
// check if we got back a 200 from the request
if (this.status >= 200 && this.status <= 299)
{
// add the download url to an a tag and then click the a tag
// to download the document
var mimetype = (2 === 2) ? "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
var blob = new Blob([req.response],
{
type: mimetype
});
var dloadurl = (window.URL ? URL : webkitURL).createObjectURL(blob);
var a = document.createElement("a");
// if ie, because ie sucks
if (navigator.msSaveOrOpenBlob)
{
navigator.msSaveOrOpenBlob(blob, filename);
// else a browser that doesn't suck
}
else
{
document.body.appendChild(a);
a.style = "display: none";
a.href = dloadurl;
a.download = filename;
a.click();
URL.revokeObjectURL(dloadurl);
}
}
}
};
// compile the data to send with the request
var formdata = "exportType=MergeWordTemplate&selectedRecords=" + encodeURIComponent(JSON.stringify(entityid)) +
"&associatedentitytypecode=" + entitytypecode + "&TemplateId=" + templateid + "&templatetype=" + templatetype;
// make the request to create the template
req.send(formdata);
}
catch (err)
{
PrintError(err.message);
}
}
/*
* PrintError() is a helper method to display any errors to the user.
*/
function PrintError(msg)
{
Xrm.Page.ui.setFormNotification("An Error occurred generating the word document, please contact support if the issue persists. " + msg, "ERROR", "docerror");
}

To get the entity code, run below query:
SELECT coalesce(OriginalLocalizedName,name) AS DisplayName, Name AS SchemaName, ObjectTypeCode
FROM EntityLogicalView
ORDER BY ObjectTypeCode
and for Template ID:
SELECT DocumentTemplateId, Name FROM dbo.DocumentTemplateBase

Related

sapui5 upload a file in SAPUI5 and send that file in odata service entityset

My requirement is to
step 1 User and choose a file to upload to sapUI5
step 2 on click of "SAVE" button the file should be uploaded,
step 3 save that uploaded file in a odata entityset and send it to backend. This odata service has two entity set, both of the entity sets should go back to backend in one call.
step
I am using UploadCollection control
<UploadCollection
uploadComplete="onUploadComplete"
typeMissmatch="onTypeMissmatch"
fileSizeExceed="onFileSizeExceed"
filenameLengthExceed="onFilenameLengthExceed"
fileDeleted="onFileDeleted"
change="onChangeAttachments"
noDataDescription="Drag & Drop files or use the "+" button for pending upload"
instantUpload="false"
sameFilenameAllowed="false"
multiple="true"
maximumFileSize="10"
maximumFilenameLength="55"
id="UploadSet"/>
on save button i am calling the method
onStartUpload: function (oEvent) {
var oUploadCollection = this.getView().getID
var cFiles = oUploadCollection.getItems().length;
if (cFiles > 0) {
oUploadCollection.upload();
}
},
My question is on method onUploadComplete how to get the content of the file and save it in the odata entityset.
Thanks
Sonal.
I have found a solution for this.
on click of the save button I am using a file reader to read the selected file as binary, than saving the binary in my model filecontent and than sending it to backend
var base64_marker = "data:" + file.type + ";base64,";
var reader = new FileReader();
reader.onload = (function (thefile) {
return function (evt) {
var base64Index = evt.target.result.indexOf(base64_marker) + base64_marker.length; // locate base64 content
var base64 = evt.target.result.substring(base64Index); // get base64 content
var oAttData = that.getModel("newInitiative").getData().rawAttachment[0];
var oFileData = {};
oFileData.ClaimType = "OFFICE";
oFileData.FileContent = base64;
oFileData.Filename = oAttData.name;
oFileData.Filesize = oAttData.size;
oFileData.Mimetype = oAttData.type;
that.getModel("SupplierModel").getProperty("/VendorFileSet").push(oFileData);
};
}.bind(that))(file);
Thanks.

How to create a local empty JSON Model from OData metadata

Is there a way to create a local empty JSON file (with all entities and properties filled in) from the metadata of the oData service? I need this for the create scenario, where I can bind the properties to the view controls. I tried the following code, but did not work. Will appreciate your suggestions.
this.getOwnerComponent().getModel().getMetaModel().getODataEntitySet("EntitySetName");
Error:
Uncaught TypeError: Cannot read property 'getObject' of null
at constructor.h.getODataEntityContainer (ODataMetaModel-dbg.js:692)
at constructor.h.getODataEntitySet (ODataMetaModel-dbg.js:731)
at eval (eval at _prepareCreatePage (ObjectPage.controller.js:74), <anonymous>:1:48)
at f._prepareCreatePage (ObjectPage.controller.js:74)
at f._onObjectPatternMatched (ObjectPage.controller.js:40)
at constructor.b.fireEvent (EventProvider-dbg.js:228)
at constructor.<anonymous>
SAP does very similar stuff in their sample pages: https://sapui5.hana.ondemand.com/#/entity/sap.ui.table.Table/sample/sap.ui.table.sample.OData2/code
Your first part is more or less correct, this is a function to store the meta model in a seperate JSONModel:
function () {
const that = this;
const oModel = this.getOwnerComponent().getModel();
const oMetaModel = oModel.getMetaModel();
oMetaModel.loaded().then(function () {
that.setModel(oMetaModel, "meta");
});
}
The interesting part is how to access stuff:
const sBasePath = "/dataServices/schema/[${namespace}===\'NAME_OF_YOUR_ODATA_SRV\']/entityType/[${name}===\'NameOfYourEntity\']"
const oEntityInformation = oMetaModel.getProperty(sBasePath);
const aKeys = oMetaModel.getProperty(sBasePath + "/key/propertyRef");
const aAllProperties = oMetaModel.getProperty(sBasePath + "/property");
const oSingleProperty = oMetaModel.getProperty(sBasePath + "/property/[${name}===\'NameOfYourProperty\']");
You can even access the model in your XML view:
columns="{
path: 'meta>/dataServices/schema/[${namespace}===\'NAME_OF_YOUR_ODATA_SRV\']/entityType/[${name}===\'NameOfYourEntity\']/property',
factory: '.columnFactory'
}"
Note that NameOfYourEntity has to be the name of the single Entity, so there is no Set at the end.
Not sure, what you want exactly but this script will write the metadata in JSON format in a blob. The save dialog in browser appears after that.
var oDataModel = this.getOwnerComponent().getModel();
oDataModel.attachMetadataLoaded(null, function() {
var oMetaData = oDataModel.getServiceMetadata(); //Read the metadata
var blob = new Blob([JSON.stringify(oMetaData)], {
type: "text/plain;charset=utf-8"
}); //Create a blob with metadata string in JSON format
if (navigator.msSaveBlob) {
return navigator.msSaveBlob(blob, "metadata.json"); //For IE
} else {
//For Chrome and FF we create a Link with a download attribute and trigger the click on it
var sUrl = URL.createObjectURL(blob); //Create the URL for the blob object
var oLink = document.createElement("a"); //Create link element
oLink.download = "metadata.json" //Set download attribute for link
oLink.href = sUrl; //Set href attribute for link
document.body.appendChild(oLink); //Append link to body
oLink.click(); //Click on link
oLink.remove(); //Remove link
}
});

Generate and download file from Word dialog

Is there a way to generate a file with some JSON content and prompt the user with a saveAs dialog.
This is from an open dialog in word.
The object could be like (will be quite a lot bigger in practice)
var obj = {a: 1, b: 2, c: 'qwerty'}
I tried to uri encode and using window.open without any luck.
content = JSON.stringify(obj);
uriContent = "data:application/octet-stream," + encodeURIComponent(content);
newWindow = window.open(uriContent, 'filename');
I did this for XML, but it requires some back-end and front-end work. On the backend, you need some ASPX like this:
string filename = "export.xml";
byte[] data = Convert.FromBase64String(Request.QueryString[0].Replace("\"", ""));
string decodedString = System.Text.Encoding.UTF8.GetString(data);
// set the http content type to "APPLICATION/OCTET-STREAM
Response.ContentType = "APPLICATION/OCTET-STREAM";
System.String disHeader = "Attachment; Filename=\"" + filename + "\"";
Response.AppendHeader("Content-Disposition", disHeader);
Response.Flush();
Response.Write(decodedString);
I called mine "download.aspx." Then on the front-end, you have to use AJAX. This created a form region on the page and the forces a submit of the form to start the download:
// Helper function to load a form and then send the post results to a
// new window so that we can get the download button
function ajax_download(url, data, input_name) {
try {
$('#form-div').append("<form method='GET' action='" +
url + "' target='_blank'>" +
"<input type=hidden name='" + input_name + "' value='" +
JSON.stringify(data) + "'/></form>");
$('#form-div').find('form').submit();
} catch (err) {
showNotification("error", err.description);
}
}
To call it, you simply make this this call from your JavaScript code where you want it:
xml = "<xml><data>12345</data></xml>";
ajax_download('./Download.aspx', btoa(xml), 'xml');
In this case, mine is targeting XML and always creates a file called "export.xml", but you can adjust it as needed.

Send mail with attached PDF to recipient after confirmation

I have to edit a Google Spreadsheet file daily. When I'm finished, I would like to send a message to people, notifying them that I'm done. Attached to that notification mail, I want to send them one specific sheet (called Report), as a PDF.
I found this option which sends email (and is working fine):
function sendEmails() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 2; // First row of data to process
var numRows = 2; // Number of rows to process
// Fetch the range of cells A2:B3
var dataRange = sheet.getRange(startRow, 1, numRows, 2)
// Fetch values for each row in the Range.
var data = dataRange.getValues();
for (i in data) {
var row = data[i];
var emailAddress = row[0]; // First column
var message = row[1]; // Second column
var subject = "Sending emails from a Spreadsheet";
MailApp.sendEmail(emailAddress, subject, message);
}
}
Is there a way to add the specific sheet as a PDF?
A secondary question: How can I create some sort of button in the spreadsheet ("Send now"), to let me easily send this email, and so I don't have to open the script editor every time?
Menu-driven on-demand operation
Within the Google Sheets user interface, a menu item would be a natural way to set this up to be run on-demand.1 A good resource for learning how to do this yourself is Google's Quickstart: Macros, Menus, and Custom Functions.
From that tutorial, here's the code that would add a "Send Report" menu item to your spreadsheet, which would call a sendReport_() function when selected:
/**
* A special function that runs when the spreadsheet is open, used to add a
* custom menu to the spreadsheet.
*/
function onOpen() {
var spreadsheet = SpreadsheetApp.getActive();
var menuItems = [
{name: 'Send Report', functionName: 'sendReport_'}
];
spreadsheet.addMenu('Custom', menuItems);
}
sendReport_() function
Let's assume we have a getPdfBlob() function, that will return a blob suitable for attaching to an email. With that taken care of, here is all sendReport_() needs to do:
// From https://stackoverflow.com/a/37149036/1677912
function sendReport_() {
// Configuration parameters; customize as you wish
var sheetName = "Report";
var subject = "Email subject line";
var recipients = "user1#example.com, user2#example.com";
var htmlMessage = "Greetings,<br/><br/>"
+ "Please find today's report attached as a PDF.<br/><br/>"
+ "Cheers,<br/><br/>Paranoia";
// Get the IDs of the spreadsheet & sheet-of-interest
var ss = SpreadsheetApp.getActive();
var sheetId = ss.getSheetByName(sheetName).getSheetId();
// Retrieve the PDF blob
var pdfBlobArray = getPdfBlobs(ss.getId(),sheetId);
// Send the email + attachment
MailApp.sendEmail(recipients,subject,"Report attached.", {
htmlBody: htmlMessage,
attachments: pdfBlobArray
});
}
getPdfBlobs() utility function
A utility to generate a PDF of a spreadsheet appears in Convert all sheets to PDF with Google Apps Script.
That can be adapted to return a blob containing the PDF of the single sheet you're after.
You must enable the Advanced Drive Service through "Resources > Advanced Drive Services...", and the developer console. (See this for more info.)
Note: There is some grotty customization of the PDF output supported by editing URL parameters that are embedded inside this function.
/**
* Get one or all sheets in a spreadsheet as PDF file blobs.
*
* From: https://stackoverflow.com/a/37149036/1677912
* Adapted from https://stackoverflow.com/a/30492812/1677912
*
* #param {String} optSSId (optional) ID of spreadsheet to export.
* If not provided, script assumes it is
* sheet-bound and opens the active spreadsheet.
* #param {String} optSheetId (optional) ID of single sheet to export.
* If not provided, all sheets will export.
*/
function getPdfBlobs( optSSId, optSheetId ) {
// If a sheet ID was provided, open that sheet, otherwise assume script is
// sheet-bound, and open the active spreadsheet.
var ss = (optSSId) ? SpreadsheetApp.openById(optSSId) : SpreadsheetApp.getActiveSpreadsheet();
// Get URL of spreadsheet, and remove the trailing 'edit'
var url = ss.getUrl().replace(/edit$/,'');
// Get array of all sheets in spreadsheet
var sheets = ss.getSheets();
// Loop through all sheets, generating PDF blobs.
var blobArray = [];
for (var i=0; i<sheets.length; i++) {
var sheet = sheets[i];
// If provided a optSheetId, only save it.
if (optSheetId && optSheetId !== sheet.getSheetId()) continue;
//additional parameters for exporting the sheet as a pdf
var url_ext = 'export?exportFormat=pdf&format=pdf' //export as pdf
+ '&gid=' + sheet.getSheetId() //the sheet's Id
// following parameters are optional...
+ '&size=letter' // paper size
+ '&portrait=true' // orientation, false for landscape
+ '&fitw=true' // fit to width, false for actual size
+ '&sheetnames=false&printtitle=false&pagenumbers=false' //hide optional headers and footers
+ '&gridlines=false' // hide gridlines
+ '&fzr=false'; // do not repeat row headers (frozen rows) on each page
var options = {
headers: {
'Authorization': 'Bearer ' + ScriptApp.getOAuthToken()
}
}
var response = UrlFetchApp.fetch(url + url_ext, options);
var blob = response.getBlob().setName(ss.getName() + ' - ' + sheet.getName() + '.pdf');
// Add blob to our array
blobArray.push(blob);
}
// Return array of PDF blobs
return blobArray;
}
/**
* Dummy function for API authorization only.
* From: https://stackoverflow.com/a/37172203/1677912
*/
function forAuth_() {
DriveApp.getFileById("Just for authorization"); // https://code.google.com/p/google-apps-script-issues/issues/detail?id=3579#c36
}
1This could be further extended to be an add-on, so that the script would not need to be attached to a specific spreadsheet.

Send Email fields not rendering in Sitecore Web Forms For Marketers

I have an issue with the WFFM Send Email Message save action (Sitecore 6.5.0). I'm trying to send an email that includes the form placeholders from the "Insert Field" dropdown in the Send Email editor. Sometimes the fields will render correctly, but most times the email will include the placeholder text instead of the field's actual value.
For example, this is the email that is coming through:
First Name: [First Name]
Last Name: [Last Name]
Email: [Email Address]
Company Name: [Company Name]
Phone Number: [Phone Number]
I think it has to do with the Send Email editor using a rich text editor for the email template, but I've tried adjusting the message's HTML to no avail. This is what the markup looks like: (the <p> tags and labels used to be inline, but that didn't work either)
<p>First Name:
[<label id="{F49F9E49-626F-44DC-8921-023EE6D7948E}">First Name</label>]
</p>
<p>Last Name:
[<label id="{9CE3D48C-59A0-432F-B6F1-3AFD03687C94}">Last Name</label>]
</p>
<p>Email:
[<label id="{E382A37E-9DF5-4AFE-8780-17169E687805}">Email Address</label>]
</p>
<p>Company Name:
[<label id="{9C08AC2A-4128-47F8-A998-12309B381CCD}">Company Name</label>]
</p>
<p>Phone Number:
[<label id="{4B0C5FAC-A08A-4EF2-AD3E-2B7FDF25AFA7}">Phone Number</label>]
</p>
Does anyone know what could be going wrong?
I have encountered this issue before, but was using a custom email action. I managed to fix it by not using the deprecated methods in the SendMail class and instead using the
Sitecore.Form.Core.Pipelines.ProcessMessage namespace's ProcessMessage and ProcessMessageArgs classes.
My use case was a little more complicated than yours, as we were also attaching a PDF brochure to our message (which is why we were using the custom email action in the first place), but here is the code:
public class SendBrochureEmail : SendMail, ISaveAction, ISubmit
{
public new void Execute(ID formId, AdaptedResultList fields, params object[] data)
{
try
{
var formData = new NameValueCollection();
foreach (AdaptedControlResult acr in fields)
{
formData[acr.FieldName] = acr.Value;
}
var senderName = formData["Your Name"];
var emailTo = formData["Recipient Email"];
var recipientName = formData["Recipient Name"];
var documentTitle = formData["Document Title"];
if (documentTitle.IsNullOrEmpty())
{
documentTitle = String.Format("Documents_{0}", DateTime.Now.ToString("MMddyyyy"));
}
Subject = documentTitle;
if (!String.IsNullOrEmpty(emailTo))
{
BaseSession.FromName = senderName;
BaseSession.CatalogTitle = documentTitle;
BaseSession.ToName = recipientName;
var tempUploadPath = Sitecore.Configuration.Settings.GetSetting("TempPdfUploadPath");
var strPdfFilePath =
HttpContext.Current.Server.MapPath(tempUploadPath + Guid.NewGuid().ToString() + ".pdf");
//initialize object to hold WFFM mail/message arguments
var msgArgs = new ProcessMessageArgs(formId, fields, MessageType.Email);
var theDoc = PdfDocumentGenerator.BuildPdfDoc();
theDoc.Save(strPdfFilePath);
theDoc.Clear();
FileInfo fi = null;
FileStream stream = null;
if (File.Exists(strPdfFilePath))
{
fi = new FileInfo(strPdfFilePath);
stream = fi.OpenRead();
//attach the file with the name specified by the user
msgArgs.Attachments.Add(new Attachment(stream, documentTitle + ".pdf", "application/pdf"));
}
//get the email's "from" address setting
var fromEmail = String.Empty;
var fromEmailNode = Sitecore.Configuration.Factory.GetConfigNode(".//sc.variable[#name='fromEmail']");
if (fromEmailNode != null && fromEmailNode.Attributes != null)
{
fromEmail = fromEmailNode.Attributes["value"].Value;
}
//the body of the email, as configured in the "Edit" pane for the Save Action, in Sitecore
msgArgs.Mail.Append(base.Mail);
//The from address, with the sender's name (specified by the user) in the meta
msgArgs.From = senderName + "<" + fromEmail + ">";
msgArgs.Recipient = recipientName;
msgArgs.To.Append(emailTo);
msgArgs.Subject.Append(Subject);
msgArgs.Host = Sitecore.Configuration.Settings.MailServer;
msgArgs.Port = Sitecore.Configuration.Settings.MailServerPort;
msgArgs.IsBodyHtml = true;
//initialize the message using WFFM's built-in methods
var msg = new ProcessMessage();
msg.AddAttachments(msgArgs);
msg.BuildToFromRecipient(msgArgs);
//change links to be absolute instead of relative
msg.ExpandLinks(msgArgs);
msg.AddHostToItemLink(msgArgs);
msg.AddHostToMediaItem(msgArgs);
//replace the field tokens in the email body with the user-specified values
msg.ExpandTokens(msgArgs);
msg.SendEmail(msgArgs);
//no longer need the file or the stream - safe to close stream and delete delete it
if (fi != null && stream != null)
{
stream.Close();
fi.Delete();
}
}
else
{
Log.Error("Email To is empty", this);
throw new Exception("Email To is empty");
}
}
catch (Exception ex)
{
Log.Error("Test Failed.", ex, (object) ex);
throw;
}
finally
{
BrochureItems.BrochureItemIds = null;
}
}
public void Submit(ID formid, AdaptedResultList fields)
{
Execute(formid, fields);
}
public void OnLoad(bool isPostback, RenderFormArgs args)
{
}
}
It is very possible that the Email Action that WFFM ships with is using the deprecated methods, which could be your problem. I do not have time to look into it, but you can decompile the DLL and look to see what their Email Action is doing. Regardless, the above code should work out of the box, save for updating the fields to those that you are using and removing the code for attaching the PDF, should you choose to not have attachments.
Good luck, and happy coding :)
If you change a field on the form in any way (caption, name, type, etc) the link will change and you need to re-insert the placeholder and move it up to its location in your expected email. This is also true if you duplicate a form. You'll have to reinsert all the fields in the email or you will just get the outcome you show above.
Reinserting upon a change will ensure the value is collected!