Skip to content

Commit 29e2ab6

Browse files
authored
Merge pull request googleworkspace#49 from mcodik/master
Add invoicing demo
2 parents 1ea9ebb + 9577678 commit 29e2ab6

File tree

8 files changed

+389
-0
lines changed

8 files changed

+389
-0
lines changed

sheets/next18/.claspignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
README.md

sheets/next18/Constants.gs

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
18+
/* Salesforce config */
19+
20+
// Salesforce OAuth configuration, which you get by creating a developer project
21+
// with OAuth authentication on Salesforce.
22+
var SALESFORCE_CLIENT_ID = '<FILL IN WITH YOUR CLIENT ID>';
23+
var SALESFORCE_CLIENT_SECRET = '<FILL IN WITH YOUR CLIENT SECRET>';
24+
25+
// The Salesforce instance to talk to.
26+
var SALESFORCE_INSTANCE = 'na1';
27+
28+
/* Invoice generation config */
29+
30+
// The ID of a Google Doc that is used as a template. Defaults to
31+
// https://docs.google.com/document/d/1awKvXXMOQomdD68PGMpP5j1kNZwk_2Z0wBbwUgjKKws/view
32+
var INVOICE_TEMPLATE = '1awKvXXMOQomdD68PGMpP5j1kNZwk_2Z0wBbwUgjKKws';
33+
34+
// The ID of a Drive folder that the generated invoices are created in. Create
35+
// a new folder that your Google account has edit access to.
36+
var INVOICES_FOLDER = '';

sheets/next18/Invoice.gs

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Copyright Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
18+
/**
19+
* Generates invoices based on the selected rows in the spreadsheet. Assumes
20+
* that the Salesforce accountId is in the first selected column and the
21+
* amount owed is the 4th selected column.
22+
*/
23+
function generateInvoices() {
24+
var range = SpreadsheetApp.getActiveRange();
25+
var values = range.getDisplayValues();
26+
var sheet = SpreadsheetApp.getActiveSheet();
27+
28+
for (var i = 0; i < values.length; i++) {
29+
var row = values[i];
30+
var accountId = row[0];
31+
var amount = row[3];
32+
var invoiceUrl = generateInvoice(accountId, amount);
33+
sheet.getRange(range.getRow() + i, range.getLastColumn() + 1)
34+
.setValue(invoiceUrl);
35+
}
36+
}
37+
38+
/**
39+
* Generates a single invoice in Google Docs for a given Salesforce account and
40+
* an owed amount.
41+
*
42+
* @param {string} accountId The Salesforce account Id to invoice
43+
* @param {string} amount The owed amount to invoice
44+
* @return {string} the url of the created invoice
45+
*/
46+
function generateInvoice(accountId, amount) {
47+
var folder = DriveApp.getFolderById(INVOICES_FOLDER);
48+
var copied = DriveApp.getFileById(INVOICE_TEMPLATE)
49+
.makeCopy('Invoice for ' + accountId, folder);
50+
var invoice = DocumentApp.openById(copied.getId());
51+
var results = fetchSoqlResults(
52+
'select Name, BillingAddress from Account where Id = \'' +
53+
accountId + '\'');
54+
var account = results['records'][0];
55+
56+
invoice.getBody().replaceText(
57+
'{{account name}}', account['Name']);
58+
invoice.getBody().replaceText(
59+
'{{account address}}', account['BillingAddress']['street']);
60+
invoice.getBody().replaceText(
61+
'{{date}}', Utilities.formatDate(new Date(), 'GMT', 'yyyy-MM-dd'));
62+
invoice.getBody().replaceText('{{amount}}', amount);
63+
invoice.saveAndClose();
64+
return invoice.getUrl();
65+
}
66+
67+
/**
68+
* Generates a report in Google Slides with a chart generated from the sheet.
69+
*/
70+
function generateReport() {
71+
var sheet = SpreadsheetApp.getActiveSheet();
72+
var chart = sheet.newChart()
73+
.asColumnChart()
74+
.addRange(sheet.getRange('A:A'))
75+
.addRange(sheet.getRange('C:D'))
76+
.setNumHeaders(1)
77+
.setMergeStrategy(Charts.ChartMergeStrategy.MERGE_COLUMNS)
78+
.setOption('useFirstColumnAsDomain', true)
79+
.setOption('isStacked', 'absolute')
80+
.setOption('title', 'Expected Payments')
81+
.setOption('treatLabelsAsText', false)
82+
.setXAxisTitle('AccountId')
83+
.setPosition(3, 1, 114, 138)
84+
.build();
85+
86+
sheet.insertChart(chart);
87+
88+
// Force the chart to be created before adding it to the presentation
89+
SpreadsheetApp.flush();
90+
91+
var preso = SlidesApp.create('Invoicing Report');
92+
var titleSlide = preso.getSlides()[0];
93+
94+
var titleShape = titleSlide.getPlaceholder(
95+
SlidesApp.PlaceholderType.CENTERED_TITLE).asShape();
96+
titleShape.getText().setText('Invoicing Report');
97+
98+
var newSlide = preso.appendSlide(SlidesApp.PredefinedLayout.BLANK);
99+
newSlide.insertSheetsChart(chart);
100+
101+
showLinkDialog(preso.getUrl(), 'Open report', 'Report created');
102+
}

sheets/next18/LinkDialog.html

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
5+
</head>
6+
<body>
7+
<div>
8+
<a href="<?= url ?>" target="_blank"><?= message ?></a>
9+
</div>
10+
</body>
11+
</html>

sheets/next18/README.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Invoicing Demo for Google Sheets
2+
3+
This sample was created for a talk for Google Cloud NEXT'18 entitled "Building
4+
on the Docs Editors: APIs and Apps Script". It is an implementation of a
5+
Google Sheets add-on that:
6+
7+
* Authenticates with Salesforce via OAuth2, using the
8+
[Apps Script OAuth2 library](https://github.com/gsuitedevs/apps-script-oauth2).
9+
* Runs [SOQL](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_sosl_intro.htm)
10+
queries against Salesforce and outputs the results into a new sheet
11+
* Creates invoices in Google Docs and a sample presentation in Google Slides
12+
using the imported data.
13+
14+
![Demo gif](demo.gif?raw=true "Demo")
15+
16+
17+
## Getting started
18+
19+
* Install [clasp](https://github.com/google/clasp)
20+
* Run `clasp create <script name>` to create a new script
21+
* Follow Salesforce's [instructions](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/quickstart.htm)
22+
to sign up as a developer and create an OAuth2 application
23+
* Set your callback URL to `https://script.google.com/macros/d/{SCRIPT ID}/usercallback`
24+
where `{SCRIPT ID}` is taken from the URL outputted by `clasp create`.
25+
* Update `Constants.gs` with your Salesforce client ID and client secret
26+
* Run `clasp push` to upload the contents of this folder to Apps Script
27+
* Run `clasp open` to open the project in the Apps Script IDE
28+
* Follow the [Test as Add-on](https://developers.google.com/apps-script/add-ons/test)
29+
instructions to run the add-on in a spreadsheet
30+
* On your test spreadsheet's menu, visit Add-ons -> &lt;script name&gt; ->
31+
Login to Salesforce to sign in to Salesforce.

sheets/next18/Salesforce.gs

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Copyright Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
18+
/**
19+
* Creates an add-on menu, the main entry point for this add-on
20+
*/
21+
function onOpen() {
22+
SpreadsheetApp.getUi().createAddonMenu()
23+
.addItem('Login To Salesforce', 'login')
24+
.addItem('Run SOQL Query', 'promptQuery')
25+
.addSeparator()
26+
.addItem('Logout From Salesforce', 'logout')
27+
.addSeparator()
28+
.addItem('Generate Invoices', 'generateInvoices')
29+
.addItem('Generate Report', 'generateReport')
30+
.addToUi();
31+
}
32+
33+
/** Ensure the menu is created when the add-on is installed */
34+
function onInstall() {
35+
onOpen();
36+
}
37+
38+
/**
39+
* If we dont have a Salesforce OAuth token, starts the OAuth flow with
40+
* Salesforce.
41+
*/
42+
function login() {
43+
var salesforce = getSalesforceService();
44+
if (!salesforce.hasAccess()) {
45+
showLinkDialog(salesforce.getAuthorizationUrl(),
46+
'Sign-in to Salesforce', 'Sign-in');
47+
}
48+
}
49+
50+
/**
51+
* Displays a modal dialog with a simple HTML link that opens in a new tab.
52+
*
53+
* @param {string} url the URL to link to
54+
* @param {string} message the message to display to the user as a link
55+
* @param {string} title the title of the dialog
56+
*/
57+
function showLinkDialog(url, message, title) {
58+
var template = HtmlService.createTemplateFromFile('LinkDialog');
59+
template.url = url;
60+
template.message = message;
61+
SpreadsheetApp.getUi().showModalDialog(template.evaluate(), title);
62+
}
63+
64+
/**
65+
* Creates a Salesforce OAuth2 service, using the Apps Script OAuth2 library:
66+
* https://github.com/gsuitedevs/apps-script-oauth2
67+
*
68+
* @return {Object} a Salesforce OAuth2 service
69+
*/
70+
function getSalesforceService() {
71+
return OAuth2.createService('salesforce')
72+
.setAuthorizationBaseUrl(
73+
'https://login.salesforce.com/services/oauth2/authorize')
74+
.setTokenUrl('https://login.salesforce.com/services/oauth2/token')
75+
.setClientId(SALESFORCE_CLIENT_ID)
76+
.setClientSecret(SALESFORCE_CLIENT_SECRET)
77+
.setCallbackFunction('authCallback')
78+
.setPropertyStore(PropertiesService.getUserProperties());
79+
}
80+
81+
/**
82+
* Authentication callback for OAuth2: called when Salesforce redirects back to
83+
* Apps Script after sign-in.
84+
*
85+
* @param {Object} request the HTTP request, provided by Apps Script
86+
* @return {Object} HTMLOutput to render the callback as a web page
87+
*/
88+
function authCallback(request) {
89+
var salesforce = getSalesforceService();
90+
var isAuthorized = salesforce.handleCallback(request);
91+
var message = isAuthorized ?
92+
'Success! You can close this tab and the dialog in Sheets.'
93+
: 'Denied. You can close this tab and the dialog in Sheets.';
94+
95+
return HtmlService.createHtmlOutput(message);
96+
}
97+
98+
/**
99+
* Prompts the user to enter a SOQL (Salesforce Object Query Language) query
100+
* to execute. If given, the query is run and its results are added as a new
101+
* sheet.
102+
*/
103+
function promptQuery() {
104+
var ui = SpreadsheetApp.getUi();
105+
var response = ui.prompt('Run SOQL Query',
106+
'Enter your query, ex: "select Id from Opportunity"',
107+
ui.ButtonSet.OK_CANCEL);
108+
var query = response.getResponseText();
109+
if (response.getSelectedButton() === ui.Button.OK) {
110+
executeQuery(query);
111+
}
112+
}
113+
114+
/**
115+
* Executes the given SOQL query and copies its results to a new sheet.
116+
*
117+
* @param {string} query the SOQL to execute
118+
*/
119+
function executeQuery(query) {
120+
var response = fetchSoqlResults(query);
121+
var outputSheet = SpreadsheetApp.getActive().insertSheet();
122+
var records = response['records'];
123+
var fields = getFields(records[0]);
124+
125+
// Builds the new sheet's contents as a 2D array that can be passed in
126+
// to setValues() at once. This gives better performance than updating
127+
// a single cell at a time.
128+
var outputValues = [];
129+
outputValues.push(fields);
130+
for (var i = 0; i < records.length; i++) {
131+
var row = [];
132+
var record = records[i];
133+
for (var j = 0; j < fields.length; j++) {
134+
var fieldName = fields[j];
135+
row.push(record[fieldName]);
136+
}
137+
outputValues.push(row);
138+
}
139+
140+
outputSheet.getRange(1, 1, outputValues.length, fields.length)
141+
.setValues(outputValues);
142+
}
143+
144+
/**
145+
* Makes an API call to Salesforce to execute a given SOQL query.
146+
*
147+
* @param {string} query the SOQL query to execute
148+
* @return {Object} the API response from Salesforce, as a parsed JSON object.
149+
*/
150+
function fetchSoqlResults(query) {
151+
var salesforce = getSalesforceService();
152+
if (!salesforce.hasAccess()) {
153+
throw new Error('Please login first');
154+
} else {
155+
var params = {
156+
'headers': {
157+
'Authorization': 'Bearer ' + salesforce.getAccessToken(),
158+
'Content-Type': 'application/json'
159+
}
160+
};
161+
var url = 'https://' + SALESFORCE_INSTANCE +
162+
'.salesforce.com/services/data/v30.0/query';
163+
var response = UrlFetchApp.fetch(url +
164+
'?q=' + encodeURIComponent(query), params);
165+
return JSON.parse(response.getContentText());
166+
}
167+
}
168+
169+
/**
170+
* Parses the Salesforce response and extracts the list of field names in the
171+
* result set.
172+
*
173+
* @param {Object} record a single Salesforce response record
174+
* @return {Array<string>} an array of string keys of that record
175+
*/
176+
function getFields(record) {
177+
var fields = [];
178+
for (var field in record) {
179+
if (record.hasOwnProperty(field) && field !== 'attributes') {
180+
fields.push(field);
181+
}
182+
}
183+
return fields;
184+
}
185+
186+
/** Resets the Salesforce service, removing any saved OAuth tokens. */
187+
function logout() {
188+
getSalesforceService().reset();
189+
}

sheets/next18/appsscript.json

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"timeZone": "America/New_York",
3+
"dependencies": {
4+
"libraries": [{
5+
"userSymbol": "OAuth2",
6+
"libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF",
7+
"version": "26"
8+
}]
9+
},
10+
"exceptionLogging": "STACKDRIVER",
11+
"oauthScopes": [
12+
"https://www.googleapis.com/auth/script.container.ui",
13+
"https://www.googleapis.com/auth/script.external_request",
14+
"https://www.googleapis.com/auth/spreadsheets.currentonly",
15+
"https://www.googleapis.com/auth/drive",
16+
"https://www.googleapis.com/auth/documents",
17+
"https://www.googleapis.com/auth/presentations",
18+
]
19+
}

sheets/next18/demo.gif

897 KB
Loading

0 commit comments

Comments
 (0)