Google Apps Script - Exporting events from Google Sheets to Google Calendar - how can I stop it from removing my spreadsheet formulas? - excel

I've been trying to create a code that takes info from a Google Spreadsheet, and creates Google Calendar events. I'm new to this, so bear with my lack of in-depth coding knowledge!
I initially used this post to create a code:
Create Google Calendar Events from Spreadsheet but prevent duplicates
I then worked out that it was timing out due to the number of rows on the spreadsheet, and wasn't creating eventIDs to avoid the duplicates. I got an answer here to work that out!
Google Script that creates Google Calendar events from a Google Spreadsheet - "Exceeded maximum execution time"
And now I've realised that it's over-writing the formulas, I have in the spreadsheet, auto-completing into each row, as follows:
Row 12 - =if(E4="","",E4+1) // Row 13 - =if(C4="","",C4+1) // Row 18 - =if(B4="","","WHC - "&B4) // Row 19 - =if(B4="","","Docs - "&B4)
Does anyone have any idea how I can stop it doing this?
/**
* Adds a custom menu to the active spreadsheet, containing a single menu item
* for invoking the exportEvents() function.
* The onOpen() function, when defined, is automatically invoked whenever the
* spreadsheet is opened.
* For more information on using the Spreadsheet API, see
* https://developers.google.com/apps-script/service_spreadsheet
*/
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [{
name : "Export WHCs",
functionName : "exportWHCs"
},
{
name : "Export Docs",
functionName : "exportDocs"
}];
sheet.addMenu("Calendar Actions", entries);
};
/**
* Export events from spreadsheet to calendar
*/
function exportWHCs() {
// check if the script runs for the first time or not,
// if so, create the trigger and PropertiesService.getScriptProperties() the script will use
// a start index and a total counter for processed items
// else continue the task
if(PropertiesService.getScriptProperties().getKeys().length==0){
PropertiesService.getScriptProperties().setProperties({'itemsprocessed':0});
ScriptApp.newTrigger('exportWHCs').timeBased().everyMinutes(5).create();
}
// initialize all variables when we start a new task, "notFinished" is the main loop condition
var itemsProcessed = Number(PropertiesService.getScriptProperties().getProperty('itemsprocessed'));
var startTime = new Date().getTime();
var sheet = SpreadsheetApp.getActiveSheet();
var headerRows = 4; // Number of rows of header info (to skip)
var range = sheet.getDataRange();
var data = range.getValues();
var calId = "flightcentre.com.au_pma5g2rd5cft4lird345j7pke8#group.calendar.google.com";
var cal = CalendarApp.getCalendarById(calId);
for (i in data) {
if (i < headerRows) continue; // Skip header row(s)
var row = data[i];
var date = new Date(row[12]); // First column
var title = row[18]; // Second column
var tstart = new Date(row[15]);
tstart.setDate(date.getDate());
tstart.setMonth(date.getMonth());
tstart.setYear(date.getYear());
var tstop = new Date(row[16]);
tstop.setDate(date.getDate());
tstop.setMonth(date.getMonth());
tstop.setYear(date.getYear());
var id = row[17]; // Sixth column == eventId
// Check if event already exists, update it if it does
try {
var event = cal.getEventSeriesById(id);
}
catch (e) {
// do nothing - we just want to avoid the exception when event doesn't exist
}
if (!event) {
//cal.createEvent(title, new Date("March 3, 2010 08:00:00"), new Date("March 3, 2010 09:00:00"));
var newEvent = cal.createEvent(title, tstart, tstop).addEmailReminder(5).getId();
row[17] = newEvent; // Update the data array with event ID
}
else {
event.setTitle(title);
}
if(new Date().getTime()-startTime > 240000){ // if > 4 minutes
var processed = i+1;// save usefull variable
PropertiesService.getScriptProperties().setProperties({'itemsprocessed':processed});
range.setValues(data);
MailApp.sendEmail(Session.getEffectiveUser().getEmail(),'progress sheet to cal','item processed : '+processed);
return;
}
debugger;
}
// Record all event IDs to spreadsheet
range.setValues(data);
}
/**
* Export events from spreadsheet to calendar
*/
function exportDocs() {
// check if the script runs for the first time or not,
// if so, create the trigger and PropertiesService.getScriptProperties() the script will use
// a start index and a total counter for processed items
// else continue the task
if(PropertiesService.getScriptProperties().getKeys().length==0){
PropertiesService.getScriptProperties().setProperties({'itemsprocessed':0});
ScriptApp.newTrigger('exportDocs').timeBased().everyMinutes(5).create();
}
// initialize all variables when we start a new task, "notFinished" is the main loop condition
var itemsProcessed = Number(PropertiesService.getScriptProperties().getProperty('itemsprocessed'));
var startTime = new Date().getTime();
var sheet = SpreadsheetApp.getActiveSheet();
var headerRows = 4; // Number of rows of header info (to skip)
var range = sheet.getDataRange();
var data = range.getValues();
var calId = "flightcentre.com.au_pma5g2rd5cft4lird345j7pke8#group.calendar.google.com";
var cal = CalendarApp.getCalendarById(calId);
for (i in data) {
if (i < headerRows) continue; // Skip header row(s)
var row = data[i];
var date = new Date(row[13]); // First column
var title = row[19]; // Second column
var tstart = new Date(row[15]);
tstart.setDate(date.getDate());
tstart.setMonth(date.getMonth());
tstart.setYear(date.getYear());
var tstop = new Date(row[16]);
tstop.setDate(date.getDate());
tstop.setMonth(date.getMonth());
tstop.setYear(date.getYear());
var id = row[20]; // Sixth column == eventId
// Check if event already exists, update it if it does
try {
var event = cal.getEventSeriesById(id);
}
catch (e) {
// do nothing - we just want to avoid the exception when event doesn't exist
}
if (!event) {
//cal.createEvent(title, new Date("March 3, 2010 08:00:00"), new Date("March 3, 2010 09:00:00"));
var newEvent = cal.createEvent(title, tstart, tstop).addEmailReminder(5).getId();
row[20] = newEvent; // Update the data array with event ID
}
else {
event.setTitle(title);
}
if(new Date().getTime()-startTime > 240000){ // if > 4 minutes
var processed = i+1;// save usefull variable
PropertiesService.getScriptProperties().setProperties({'itemsprocessed':processed});
range.setValues(data);
MailApp.sendEmail(Session.getEffectiveUser().getEmail(),'progress sheet to cal','item processed : '+processed);
return;
}
debugger;
}
// Record all event IDs to spreadsheet
range.setValues(data);
}

You have to ways to solve that problem.
First possibility : update your sheet with array data only on columns that have no formulas, proceeding as in this other post but in your case (with multiple columns to skip) it will rapidly become tricky
Second possibility : (the one I would personally choose because I 'm not a "formula fan") is to do what your formulas do in the script itself, ie translate the formulas into array level operations.
following your example =if(E4="","",E4+1) would become something like data[n][4]=data[n][4]==''?'':data[n+1][4]; if I understood the logic (but I'm not so sure...).
EDIT
There is actually a third solution that is even simpler (go figure why I didn't think about it in the first place...) You could save the ranges that have formulas, for example if col M has formulas you want to keep use :
var formulM = sheet.getRange('G1:G').getFormulas();
and then, at the end of the function (after the global setValues()) rewrite the formulas using :
sheet.getRange('G1:G').setFormulas(formulM);
to restore all the previous formulas... as simple as that, repeat for every column where you need to keep the formulas.

Related

office.js code after context.sync not running

I'm having trouble to extract values from an excel file with the office.js add in I'm writing.
The add in shall help my colleagues to prepare report sheets for each teacher.
It is supposed to filter the corresponding courses from the master table and send the data to the next processing step (create word files for each teacher).
I've tried filtering ranges with autofilter and creating a table with the data, but it seems that no code is executed after return context.sync()
I've read the official tutorial and some of the code on buildingofficeaddins.com but my function never executes the code after "return context.sync()"
function mselectTeacher(teachers) {
Excel.run(function (context) {
var sheet = context.workbook.worksheets.getActiveWorksheet();
var lfv = sheet.tables.add("A1:M211", true);
var wsy = lfv.columns.getItem("WS/SS");
var studium = lfv.columns.getItem("Studium");
// some more colums
wsy.load("values");
studium.load("values");
return context.sync()
.then(function () {
//I actually want to filter the rows by teacher,
//this is only for testing
for (var i = 1; i < 20; i++) {
console.log(wsy[i] + "," + studium[i]);
}
});
});
}
Is the problem, that I'm calling Excel.run from within another function?
Could you try this?
function mselectTeacher(teachers) {
Excel.run(function (context) {
var sheet = context.workbook.worksheets.getActiveWorksheet();
var lfv = sheet.tables.add("A1:M211", true);
var wsy = lfv.columns.getItem("WS/SS");
var studium = lfv.columns.getItem("Studium");
// some more colums
wsy.load("values");
studium.load("values");
context.sync()
//I actually want to filter the rows by teacher,
//this is only for testing
for (var i = 1; i < 20; i++) {
console.log(wsy[i] + "," + studium[i]);
}
});
}
I tried your code in ScriptLab on excel web and saw "The requested resource doesn't exist"error in F12
I think it's due to no columns named "WS/SS" "Studium" in your new added table. It works after i changed the columns name with "WS/SS" "Studium"

Attach excel workbook or convert sheets into excel, then send via gmail using gscript, so that workbook is editable by recipient?

I want to be able to send an email to 100+ emails that I have, with an attachment. The attachment needs to be an editable excel file for the recipients. I can access it either as excel or sheets, whichever is easier.
I've tried as much as I could find already online. And I can send a PDF, and I can send a Google Drive link which delivers an editable sheets doc; but I want the end user to be able to access/edit it even if they do not have Google Drive.
This one successfully sends an editable PDF:
function sendEmails() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 2; // First row of data to process
var numRows = 1; // Number of rows to process
// Fetch the range of cells A2:B2
var dataRange = sheet.getRange(startRow, 1, numRows, 1);
// 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'
var file = DriveApp.getFileById("xxxxxxxxx");
MailApp.sendEmail(emailAddress, subject, message, {attachments: [file]});
}
}
I tried this one, to be able to attach a document (hoping an editable excel file) but I get an error code saying that the document is missing (even though it's the exact same doc ID I use in the above, which works):
function sendEmails() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 2; // First row of data to process
var numRows = 1; // Number of rows to process
// Fetch the range of cells A2:B2
var dataRange = sheet.getRange(startRow, 1, numRows, 1);
// 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'
var file = DocumentApp.openById("xxxxxxxxx");
MailApp.sendEmail(emailAddress, subject, message, {attachments: [file]});
}
}
I wanted it to send an excel file, but it won't even find the document even when I use the same doc ID that I use in the 1st syntax above, which works.
This sends an excel doc.. but recipient unable to open. Is this not formatting correctly?
function getGoogleSpreadsheetAsExcel(){
try {
/*var ss = SpreadsheetApp.getActive();*/
var ss = DriveApp.getFileById("xxxxx");
var url = "https://docs.google.com/feeds/download/spreadsheets/Export?
key=" + ss + "&exportFormat=xlsx";
var params = {
method : "get",
headers : {"Authorization": "Bearer " + ScriptApp.getOAuthToken()},
muteHttpExceptions: true
};
var blob = UrlFetchApp.fetch(url, params).getBlob();
blob.setName(ss.getName() + ".xlsx");
MailApp.sendEmail("xxxx", "Google Sheet to Excel", "The
XLSX file is attached", {attachments: [blob]});
} catch (f) {
Logger.log(f.toString());
}
}
Here's the correct syntax to send email from gmail with an excel attachment using google script
function excelemail(){
var file = DriveApp.getFileById('1pJpDI2HD28_gPWGnj-emV0L4rXBBS0HC');
GmailApp.sendEmail('xxxx', 'Attachment example', 'Please see the attached file.' , {attachments: [file], name: 'Automatic Emailer Script' })
}

New Filter of Gmail for auto movetrash specific subjects with time condition. Warning: it never matches the incoming email

I want to make filters in Gmail email with certain subjects automatically deleted if more than 1 day from example#gmail.com. but when the filter is entered there is a warning from gmail:
Filter searches containing "label:", "in:", "is:", date range (e.g. "before:" or "after:") or stars criteria (e.g. "has:yellow-star") are not recommended as they will never match incoming emails. Do you still wish to continue to the next step?
How do you do it in the latest version (Gmail)?
I want to delete emails with certain subjects more than 1 day from example#gmail.com, how do that?.
Try using Google Script:
- Open: https://script.google.com
- Click File => New => Script File
- Type this code
// reference: https://medium.com/#fw3d/a-cleaning-robot-for-your-gmail-inbox-f44c01306ea2
// Modify by: Dicky Ibrohim for delete (move to trash) specific subject, sender, and time
function removetotrash() {
var delayDays = 1; // Impact form email more than 1day
var maxDate = new Date();
maxDate.setDate(maxDate.getDate()-delayDays);
// What will be executed, adjust the string here
var searches = [
'"SUBJECT WANT SEARCH TO DELETE" from:example#gmail.com'
];
// Looping for 500 last email
var threads = [];
for (var i = 0; i < searches.length; i++) {
var tmp_threads = GmailApp.search(searches[i], 0, 500); // search until 500 mail
var threads = threads.concat(tmp_threads);
}
// Then remove all to trash
for (var i = 0; i < threads.length; i++) {
if (threads[i].getLastMessageDate()<maxDate)
{
threads[i].moveToTrash();
}
}
}
Click File => Save
Make it work automatic: Click icon "Current project's triggers",
Click "Create new triger"
Setup AND SELECT intermediate time
Save

Each month, populate spreadsheet data to next column [script needed]

I am developing a "Dashboard" [Sample Sheet Here] in which a number of data points are automatically calculated using formulas in Column C (NOTE: formulas not included in sample sheet).
I would like to create a monthly log of Column C data, in which cell values are copied to the next blank column of the corresponding Row
I have previously used the following script to log changes vertically, and could use a hand with repurposing for my desired outcome.
function onEdit(event) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = event.source.getActiveSheet();
var r = event.source.getActiveRange();
if(s.getName() == "Action Items" && r.getColumn() == 2 && r.getValue()) {
var row = r.getRow();
var numColumns = s.getLastColumn();
var targetSheet = ss.getSheetByName("Portion Tracking");
if(targetSheet.getLastRow() == targetSheet.getMaxRows()) {
targetSheet.insertRowAfter(targetSheet.getLastRow());
}
//Changes Start Here
var myRow = targetSheet.getLastRow()+1;
s.getRange(row, 1).copyTo(targetSheet.getRange(myRow,1));
s.getRange(row, 2).copyTo(targetSheet.getRange(myRow,2));
s.getRange(row, 3).copyTo(targetSheet.getRange(myRow,3));
}
}
AppendColumn()
I played around with this idea once and using a function I took from here and some of my own carving. I've comeup with an appendColumn() function.
Anyway it sounded like something you might want to have. You can take a look at it and in the meantime I'll take a look at your code.
function appendColumn(columnA)
{
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var rg=sh.getDataRange();
var vA=rg.getValues();
var vB=transpose(vA);
sh.clear();
sh.getRange(1,1,vB.length,vB[0].length).setValues(vB);
sh.appendRow(columnA);
vD=sh.getDataRange().getValues();
sh.clear();
vE=transpose(vD);
sh.getRange(1,1,vE.length,vE[0].length).setValues(vE);;
}
function transpose(a)
{
return Object.keys(a[0]).map(function (c) { return a.map(function (r) { return r[c]; }); });
}
To test it I just called it like this.
function testAppendColumn()
{
appendColumn(['',1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]);
}
I'd like to see the source data. But assuming it has not changed then I'm guessing that this technique will work for you.
function onEdit(event)
{
var ss = event.source;
var s = event.source.getActiveSheet();
var r = event.range;
if(s.getName() == "Action Items" && r.getColumn() == 2 && r.getValue())
{
var row = r.getRow();
var numColumns = s.getLastColumn();
var targetSheet = ss.getSheetByName("Portion Tracking");
if(targetSheet.getLastRow() == targetSheet.getMaxRows())
{
targetSheet.insertRowAfter(targetSheet.getLastRow());
}
//Changes Start Here
var myCol = targetSheet.getLastColumn()+1;
s.getRange(row, 1).copyTo(targetSheet.getRange(1,myCol));
s.getRange(row, 2).copyTo(targetSheet.getRange(2,myCol));
s.getRange(row, 3).copyTo(targetSheet.getRange(3,myCol));
}
}
I don't understand the logic of connecting this to onEdit trigger but I'll assume that you do.

Netsuite Past Due Reminder using SuiteScript

The script is complete! Thanks for all those who replied :)
/*
* Author: Laura Micek
* Date: 5-13-15
* Purpose: This script creates a saved search in order to pull the information needed to send out an email to alert customers that
* their account is past due. The saved searched makes sure that the customer is 11 days or more past due, checks to see if they are
* exempt from past due reminders, and that their account balance is greater than 1. Once the saved search runs, it will loop thru the
* customers that meet these requirements and it will use the days past due to determine if an email needs to be sent. An email will
* only be sent if the days past due are equal to 11 or if the days past due minus 11, modded by 8 equals 0 which means that it has
* been 8 days since the last notification.
*/
function email_late_customers(type) {
//variables
var send_from = 22730; // Internal ID of NS User
//setup filters and result columns for a customer saved search
var filters = new Array();
filters[0] = new nlobjSearchFilter('daysoverdue',null,'greaterthanorequalto',11);
filters[1] = new nlobjSearchFilter('custentitypastdueremind',null,'is', 'F');
filters[2] = new nlobjSearchFilter('balance',null,'greaterthan', 1);
var columns = new Array();
columns[0] = new nlobjSearchColumn('internalid');
columns[1] = new nlobjSearchColumn('email');
columns[2] = new nlobjSearchColumn('daysoverdue');
//run saved search and loop thru results
var customers = nlapiSearchRecord('customer',null,filters,columns);
for (var i = 0; customers != null && i < customers.length; i++) {
//grab all the customer data
var this_customer = customers[i];
var cust_id = this_customer.getValue('internalid');
var send_to = this_customer.getValue('email');
var getpastduedays = this_customer.getValue('daysoverdue');
//this is the check to see if the amount of days is over 11 to see if another email needs to be sent.
if(getpastduedays > 11) {
var checkPastDue = (getpastduedays - 11) % 8;
}
/*
if the above checkPastDues evaluates to zero then it has been 8 days since the last notification, this is the other condition to send an email. The first being that the customer is 11 days past due.
*/
if(getpastduedays == 11 || checkPastDue == 0) {
//email subject
var subject = 'Your Account is Past Due';
// create body text
var body = 'Hello, \r\r';
body += ' This is a reminder that your account is currently past due. Attached is a current detailed aging of your account for your reference.\r\r ';
body += ' Can you please review and let me know the status of payment?\r\r';
body += ' Your prompt attention to this matter would be greatly appreciated. If you have any questions reguarding this account, please ';
body += ' contact us as soon as possible. Any questions or invoice copy requests can be email to ar#doubleradius.com.\r\r';
body += ' If payment has been recently been made, please accept our thanks and ignore this reminder.\r\r';
body += 'Thank You!\r\r';
//setup filters and result columns for a transaction saved search
var filters = new Array();
filters[0] = new nlobjSearchFilter('status',null,'is', 'CustInvc:A');
filters[1] = new nlobjSearchFilter('type',null,'is', 'CustInvc');
filters[2] = new nlobjSearchFilter('email',null,'is', send_to);
filters[3] = new nlobjSearchFilter('mainline',null,'is', 'T');
var columns = new Array();
columns[0] = new nlobjSearchColumn('internalid');
//run saved search and loop thru results
var transactions = nlapiSearchRecord('transaction',null,filters,columns);
var invoices = [];
for (var i = 0; transactions != null && i < transactions.length; i++) {
//grab all the transaction data
var this_transaction = transactions[i];
invoices[i] = this_transaction.getValue('internalid');
}
//print the statement to a PDF file object
var attachment = [];
for (var i = 0; invoices != null && i < invoices.length; i++) {
attachment[i] = nlapiPrintRecord('TRANSACTION',invoices[i],'DEFAULT',null);
}
//send the PDF as an attachment
nlapiSendEmail(send_from,/*send_to*/ 'lauram#doubleradius.com', subject, body, null, null, null, attachment);
}
}
}
You don't need scripting to achieve this simple requirement. All you need is a saved search and a workflow. The key thing here is you need to come up with the right criteria on your saved search. Once you have the right saved search you set your workflow's Initiation to run on Scheduled and choose the frequency. Use the Send Email action to send the email to the customers and you are good to go.
Also, from the saved search you can join the customer record to the Messages Field so you will have the ability to check when was the last email sent.
You might also need a Email Template.
If you're going to use a Scheduled Script you will need to collect the records you want to inspect. If these are Customer records then be sure to setup searchFilters and searchColumns to come back and then collect the results.
// set Customer record filters and columns to return
var filters = new Array();
filters.push( new nlobjSearchFilter('isActive', null, 'is', 'F') );
filters.push( new nlobjSearchFilter('someotherfield', null, 'isempty') );
var cust_cols = new Array();
cust_cols.push( new nlobjSearchColumn('companyname') );
cust_cols.push( new nlobjSearchColumn('someotherfield') );
// now get the records that fit your filters and return the cols specified
overdue_customers = nlapiSearchRecord('customer', null, searchfilter, columns);
I generally like to move my content/body creating code into separate functions but it's really not that important depending on how complicated your script is. For instance I have a couple of scripts that do a lot of processing that have to send some information on a record by record basis but also sends "digest" type emails to others. Think customers and account managers. While it was a pain to set up everything up to handle both cases having a email content generating function was much saner and easier to read.
Though there are many approaches you could use for this, I think your scheduled script approach will work just fine. You can make the logic for identifying which emails to send by offloading the 8-day calculation to your search filters, instead of manually trying to compute. I would have a filter in my search of something like:
new nlobjSearchFilter('custentity_dayssincelastemailed', null, 'before', 'previousOneWeek');
See the Help article titled Search Date Filters for more details on what you can do with Dates in filters.
After that, I believe you should be able to create an Email Template and utilize that in your code to set the title and the boilerplate of the email body.
Where I'm a little less certain, and what may be more difficult for you, is your A/R Aging attachment. Not sure I've worked with attaching statements.

Resources