How to get triggers to work in a copied spreadsheet + script? - javascript

I'm currently creating a spreadsheet where users can enter an item number and the sheet will return a description and price in the next two columns.
The spreadsheet pulls the item information from another sheet (not another page, rather a whole different URL) and the sheet updates itself every time an item number is entered (no vlookup because the info is on another URL).
Most likely, multiple people will need this form at the same time, and they will also need a copy for reference records. They all have access to the "Master File", the one I have, and I was hoping they could simply make a copy and then fill out the form.
However, while the code in my script works just fine on the Master File, when they make a copy the program won't run. I know it has to do with the triggers not being copied over and I've read up on writing triggers in the script, but here's the problem.
The users cannot see the script - that is, we don't want them to see any code. So they can't go in, turn on triggers via "Resources" or click the run/debug in script editor.
So basically I need a user to be able to open a shared, view only file, make a copy of a spreadsheet (which gets info from another sheet and has triggers), and use that spreadsheet buy inputting item numbers. Most of these people should not be able to see the inner workings, and wouldn't understand any of it anyway.
I was thinking a possible solution would be like what they do in this video around 25:56 or 37:355 where they can press a button and it writes the triggers. They don't go over how to do it though.

If your users are all in a private domain, your best solution would be to publish a private add-on (i.e. available only to domain users). That's not an option if you're using a consumer account.
Alternatively, you can use a menu-driven function to programmatically create the trigger(s) you need. This is effective in your case, because:
The "original" spreadsheet is shared as read-only, and users are expected to make a personal copy to enter their own data.
The users will be owners of their copies, so contained scripts will run as them and for them.
For example, you can try this shared spreadsheet. It's shared public, read-only, but if you save a copy, you will see a Custom Menu that sets a trigger function, and updates the first cell in the spreadsheet. Ten seconds late, the trigger function updates it again.
The demo script is contained in the spreadsheet, so you can see it for yourself there. Here's all it contains:
// Create a menu that will initialize the trigger
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Custom Menu')
.addItem('Initialize spreadsheet', 'setTrigger')
.addToUi();
}
function setTrigger() {
// clear any existing triggers
var triggers = ScriptApp.getUserTriggers(SpreadsheetApp.getActive())
for (var i=0; i<triggers.length; i++) {
ScriptApp.deleteTrigger(triggers[i]);
}
// set new trigger
ScriptApp.newTrigger("runTrigger")
.timeBased()
.after(10*1000) // 10s delay
.create();
announce("Trigger set. Wait for it...");
}
function runTrigger() {
announce("Trigger fired! This completes our demo.");
}
// Update first cell in spreadsheet
function announce(message) {
var range = SpreadsheetApp.getActive().getSheets()[0].getRange("A1");
range.setValue(message);
}
Instead of a menu, you could include a "button" image, and link a script to that. I didn't look at the video, but that's probably what they did. You can see more about that (silly, imho) option in How do you add UI inside cells in a google spreadsheet using app script?

From what I understood you don't need a script here, spreadsheet formulas will do the trick.
to import data from one spreadsheet to an other you can use the formula "importData" and put these data in a hiden sheet.
then you can use "vlookup" formula on this import or even better a "filter" formula (try the filter formula you'll love it).

Related

Netsuite Customize NetSuite Transaction Summary

I'm trying to figure out how to change the Transaction Summary on the sales order UI.
I've been doing some research and I'm getting the impression I need to create a user event script(?)
For example: I want to add more details like Markup / Freight cost / etc.
I'm very new to this, but please let me know if I'm going in the right direction.
/**
* #NApiVersion 2.0
* #NScriptType UserEventScript
*/
define([], function() {
return {
afterSubmit : function (context) {
var salesorder = context.newRecord;
var total = salesorder.getValue('total');
var subtotal = salesorder.getValue('subtotal');
var tbl = document.getElementById("totallingtable");
log.debug('total', total);
log.debug('subtotal', subtotal);
}
};
});
NetSuite does not support access to the native UI through the DOM. You could do this on client side, but it will absolutely break the record. Therefore, I would recommend either:
Making an HTML field on the Sales Order record and writing HTML/CSS to mimic the Transaction Summary box. It would probably be best to place it in its own subtab; something like "Transaction Details", and write to it with a client script on pageInit().
Add a button to the Sales Order record on User Event beforeLoad(). Have that button open a Suitelet that essentially mimics a popup with HTML/CSS to mimic the Transaction Summary as stated in example 1.
Thinking about it, I think the second option is better.
If you need additional help, please comment back and I'll be happy to assist. I have no problem stepping through code, even if baby steps.
EDIT:
Based on the conversation below, the second option seems better for your business needs.
Before we dive into that, I did some research and believe you were trying to do something similar to this post. That approach may very well work, but is not something I would do, citing this excerpt from the NetSuite Help Documentation:
"SuiteScript does not support direct access to the NetSuite UI through the Document Object Model (DOM). You should only access the NetSuite UI by using SuiteScript APIs."
NetSuite Help Documentation: SuiteScript 2.0 Custom Pages
In addition, I'm adding a few notes to this post regarding the whole thing overall. I'll say this once here, and once at the end as a reminder. But remember, it carries for everywhere.
First, if you don't understand something, don't hesitate to ask questions. The only stupid question is the one you don't ask at any time, even if it's the i-th time you're asking as asking questions is a fundamental part of learning anything. Second, make sound coding decisions. There's no way for me to know if this code is still exactly what you need, especially since I don't know your business or the setup of your account. But, I'm sure going to try my best to help in the spirit of help.
With that said, for this example, do the following in order. Once you get the hang of it, you can work with the nuances as you see fit. I won't discuss any of the nuances here. I wish I could, but that's somewhat like explaining how to ride a bike. Here we go!
Make a sub-folder in your SuiteScripts folder called "Transaction_Details" (make sure to use the underscore). We're going to upload everything to here so it's all in one folder under the folder where we need it to be for scripts to run (that is the SuiteScripts folder). From the Administrator role that's:
Documents > Files > SuiteScripts > Add Folder
Create an HTML file that displays what you're looking for when opened on localhost. Name that file "Transaction_Details.html". NetSuite will default to this file name later, and we're going to use it a few times in our example. So, don't name the file something else. You may want to test the file in a few browsers, especially if you're supporting Internet Explorer. Since we're trying to mimic the Transaction Summary, let's make that our target. To me, it looks like a table with some CSS to make it stand out. Transaction summary is also a native NetSuite term, so we'll call this our "Transaction Details". This way, when someone says, "I don't see it in the Transaction Summary", you can say "Click the Transaction Details button which shows more details than the summary". We want to pass some numbers into this HTML which are the numbers that pertain to our "details". We don't really need syntax to do this, but it can help our readability if we, sort of, alert ourselves that whatever is supposed go in this place is marked in some way. So, we'll arbitrarily mark this as [VARIABLE_TO_REPLACE]. A sample file is below to get you started. When you have the file the way you like it, upload that to your "Transaction_Details" subfolder. From the Administrator role that's:
Documents > Files > SuiteScripts > Transaction_Details > Add File
Select Transaction_Details.html from your computer and set the folder to SuiteScripts : Transaction_Details
<!DOCTYPE html>
<html>
<head>
<title>Transaction Details</title>
<style>
<!--Any CSS you might need; background color, bold, etc.-->
</style>
</head>
<body>
<table>
<tr>
<td>Gross</td>
<td>[GROSS]</td>
</tr>
<tr>
<td>Discount</td>
<td>[DISCOUNT]</td>
</tr>
<tr>
<td>Variable To Replace</td>
<td>[VARIABLE_TO_REPLACE]</td>
</tr>
<!--Rinse and repeat-->
</table>
</body>
</html>
Create a Saved Search with whatever information you need. Let's title the Saved Search "Transaction Details" and give it an ID of "_transaction_details". You may need more than one Saved Search to get everything out. But, in many, many, situations, you can get the data out in one Saved Search. Most times, this is done using SQL. Unfortunately, I don't know everything you'll need here, so I have to leave much of this step up to you; there's a lot of nuances here. However, one thing you should consider is using a filter to filter results which only pertain to a record's Internal ID. This will ensure you're only searching for information on a record. In the Saved Search, you can pick any Internal ID of a relevant record(a Sales Order, most likely) and test on that. Once your results are how you like them, remove the Internal ID filter, as we will dynamically push this as an object in our Suitelet (coming up in step 4). Eventually, you'll need to provide access to the Saved Search. Access is controlled on the Saved Search itself using things like the Audience subtab, and the "Public" checkbox. You will know which access is best for your business. But, note that for anyone to see the Transaction Details we are building they will at least need access to the search. Access permissions is a whole other thing with NetSuite; nuances. To create a saved search from the Administrator role that's:
List > Search > Saved Searches > New > Transaction
Warning: this is a big step, but it really is all one step. Create a Suitelet that loads our HTML file and replaces our placeholder text with the proper text for that placeholder. Call this file "Transaction_Details_Suitelet.js". We're going to replace the text by running the Saved Search we created with an Internal ID filter that points to our transaction and filters the data, making it easier to extract. Now, I'm making an assumption here that the data we need came out in only one row of results. If there is more than one row, that is fine, but you'll have to do your own formatting, or give me a screenshot of your results so I can edit my answer again. An example is below to get you started. This is going to take some configuration, so it's best to get something into NetSuite that at least passes it's code inspection (which is done as you try to upload the file). Once it's uploaded, you can open the URL to the Script from its Script Deployment, which if error-free will render the HTML we've written in step 2. If it doesn't and you're getting frustrated, just upload the script as seen in example 1 below. All example 1 is saying is, "render my HTML", on the condition that your HTML is properly formatted. If that works, you know "Transaction_Details.html" is good, so you can move onto example 2. If example 2 breaks, it is likely "Transaction_Details_Suitelet.js" that is the problem. To upload the Suitelet from the Administrator role that's:
Customization > Scripting > Scripts > New > Select Transaction_Details_Suitelet.js from your computer
Set the folder to SuiteScripts : Transaction_Details
Some error checking will now occur.
If it passes, title it "Transaction Details Suitelet", and give it an ID of "_transaction_details_sl" (sl is short for Suitelet)
Click the Deployments subtab, and title the Deployment "Transaction Details Suitelet". Give it an id of "_transaction_details_sl".
So, the Script and its Deployment should mimic each other in their names. This will be important in our next step
Example 1:
/**
* #NScriptType Suitelet
* #NApiVersion 2.x
*/
define(["N/file"], function(file) {
function onRequest(context) {
if (context.request.method === "GET") {
var fileTransactionDetails = file.load({
id: "/SuiteScripts/Transaction_Details/Transaction_Details.html"
}).getContents();
//Tell NetSuite to take the HTML in our "Transaction_Details.html" file and render that. We'll navigate to the Suitelet which renders our HTML file in step 5.
context.response.write(fileTransactionDetails);
}
}
return {onRequest: onRequest}
})
Example 2
/**
* #NScriptType Suitelet
* #NApiVersion 2.x
*/
define(["N/file"], function(file) {
function onRequest(context) {
if (context.request.method === "GET") {
var fileTransactionDetails = file.load({
id: "/SuiteScripts/Transaction_Details/Transaction_Details.html"
}).getContents();
var searchTransactionDetails = search.load({
id: "customsearch_transaction_details"
});
searchTransactionDetails.filters.push(search.createFilter({
name: "internalid",
operator: search.Operator.EQUALTO,
value: context.request.params.id
}));
var resultsTransactionDetails = searchTransactionDetails.run();
resultsTransactionDetails.each(function(result) {
// getText, if you need the text representation
fileTransactionDetails = fileTransactionDetails.replace("[GROSS]", result.getText(resultsTransactionDetails.columns[0]);
// getValue, if you need something that is a date, a SQL formula result, or something "behind the scenes" like the Internal ID of an element in a list/record.
fileTransactionDetails = fileTransactionDetails.replace("[GROSS]", result.getValue(resultsTransactionDetails.columns[0]);
// I'm intentionally commenting this line out, but if you had more than one row to consider, you would return true, which tells the search, "go to the next row". For now, don't do that.
//return true;
});
//Tell NetSuite to take the HTML in our "Transaction_Details.html" file and render that. We'll navigate to the Suitelet which renders our HTML file in step 5.
context.response.write(fileTransactionDetails);
}
}
return {onRequest: onRequest}
})
Create a User Event Script that places a button, on a Transaction, on beforeLoad, in view mode only, that opens our Suitelet in step 4, where step 4 runs our search in step 3, replaces text in our HTML file from step 2, and renders the whole thing (sorry for the long sentence. but that's really the one sentence summary of the whole thing anyways). A very important note here: it is best to turn buttons on in view mode only so you can ensure all the data which you need is written to the database.
We'll need to upload the User Event script and tell the script to make the button appear on only the records selected in the User Event's Deployment tab. Let's assume its a Sales Order for now. From the Administrator role that's:
Customization > Scripting > Scripts > New > Select Transaction_Details_User_Event.js from your computer and set the folder to SuiteScripts : Transaction_Details
Some error checking will now occur.
If it passes, title it "Transaction Details User Event", and give it an ID of "_transaction_details_ue" (ue is short for User Event).
Select the Deployments tab > Applies To > Sales Order and set the ID to "_so_transaction_details_ue" which is like saying "This is the Sales Order Deployment for Transaction Details on the User Event side".
/**
* #NScriptType UserEventScript
* #NApiVersion 2.x
*/
define(["N/url"], function(url) {
function beforeLoad(context) {
if (context.type === context.UserEventType.VIEW) {
// This gives us the link to our Suitelet.
// We can pass any URL parameters to our Suitelet with params.
// One param you will definitely need is id,
// which is the id of the current record you are opening
// the transaction details from, and is used in our
// Saved Search in step 3.
// Recall that we need the id's of our Suitelet and it's Script
// Deployment to be exactly what we said above. NetSuite will prepend
// "customscript" and "customdeploy" to the id of the Suitelet and
// Deployment, respectively, so we need to do the same.
// Missing this step is a common reason why links won't open as
// expected.
// Lastly, although returnExternalUrl defaults to false, let's
// set it to false for peace of mind. We don't want to expose
// anything to the outside.
var urlTransactionDetails = url.resolveScript({
scriptId: "customscript_transaction_details_sl",
deploymentID: "customdeploy_transaction_details_sl",
params: {
id: context.newRecord.id
},
returnExternalUrl: false
});
// Because we're using at least 2.1, we can use `backticks` to
// interpolate urlTransactionDetails into window.open, which makes
// things much easier.
context.form.addButton({
id: "custpage_view_transaction_details,
label: "Transaction Details,
functionName: `window.open("${urlTransactionDetails}");`
});
}
}
return {beforeLoad: beforeLoad}
}
And that's it! Hopefully that gets you farther along. As I stated before, don't hesitate to ask questions. Also, practice good coding practices. Practice good anything for that matter. My intentions are to help you and others here. Unfortunately, this post will not solve everything, but I hope it will help you and others who stop by.
Let me know how it goes!

When using a string to call a function from another function, how would one go about it?

For clarity:
The user will never be expected to input code. User inputs, aside from name customization (which customized names will NEVER be run through code), will be limited to button presses (which will then potentially activate functions which are waiting in the database entries).
I have one defined constructor in my javascript code, and essentially will have a database which allows the user certain inputs, be they pre-defined inputs (the majority of the time) which will execute other code or customized inputs (which would never have the potential to change anything which can execute code).
An example would be the following code: new DatabaseItem("511", ["feat", "general", "1"], "Armor Proficiency", ["description", "if(database[5][0][0].returnData().includes(0)){possible=["500","Cancel"];database[5][0][0].forEach(function tempFunction(value,index){if(value==0){possible.push(index);}});}else{database[5][0][0].addTrait("invalid");alert("No eligible targets. Please select another feat.");}"])
which would create a new DatabaseItem at position 511 with traits "feat", "general", "1"; the title "Armor Proficiency"; and data of "description" and would execute the following code when triggered:
if(database[5][0][0].returnData().includes(0)) {
possible = ["500", "Cancel"];
database[5][0][0].forEach(
function tempFunction(value, index) {
if(value==0) {possible.push(index);}
}
);
} else {
database[5][0][0].addTrait("invalid");
alert("No eligible targets. Please select another feat.");
}
Then after the user selects a choice (be it "Cancel", or one of the valid indexes) supposing that choosing this was valid in the first place, it would then execute more code based on the user's choice.
Based on what I've read, the eval() function would work (but is generally considered unsafe) but Function() may also work. What would be the recommended course of action?
Relevant information:
-this is a stand-alone webpage-based application. The framework is provided in the html file, and this is an excerpt from the js file.
-nothing is stored after the webpage is closed. Save data is generated via a savestring which implements changes the user makes and the positions of said changes, which the user is then free to copy to a text file. Invalid savestrings would be rejected by the loader.

Display dynamic form options in google sheets

I have a Google sheet named 'Data' where a user can enter their own product names and sale numbers associated with it (meant to be a template so the product names may change user to user). I have read in the data and the product names (PRODUCT_NAMES) into some global data objects so I have access to it. I have created a menu item called "Plot Data" which goes to function (for now) testFunction()
I would like to display a form to the user that displays a checkbox grid item of all their product names (and maybe more future options), based on which ones they check I create a graph or calculate some values.
My question is two part:
1) How to create a form with dynamic grid choices, say one user has
PRODUCT_NAMES = {"Shirt", "Pants", "Shoes"}
and another has
PRODUCT_NAMES = {"Cars", "Trucks", "Suvs", "Trailers", "Boats"}
2) How to display the form in a google sheet? I have right now:
function testFunction() {
// Create a new form since the options change
var form = FormApp.create('Test Form');
var item = form.addCheckboxItem();
item.setTitle("This is a Test");
// TODO: Add dynamic options here
var formUrl = form.getPublishedUrl();
var response = UrlFetchApp.fetch(formUrl);
var formHtml = response.GetContextText();
var htmlApp = HtmlService
.createHtmlOutput(formHtml)
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setTitle('Sales Data')
.setWidth(500)
.setHeight(450);
SpreadsheetApp.getUi().showSidebar(htmlApp);
}
But this does not seem to work, I may be misunderstanding something
Question 2b) How to access the user responses.
I am learning javascript and GAS on the fly, so this is all new to me. I would like to try and keep the user options part as a Google Form, just cause its easier from an API perspective what is going on.
From my comment, expanded a bit, for future users:
It would be better to show a UI Modal Dialog with a basic HTML form. This keeps the code within the script itself.
The issue with what you want to do is once the form is made and the spreadsheet is linked the data will still go to a new sheet per form, not to where you want it to go, and then you'd have to setup a onEdit() trigger to get the values from the new sheet, generate charts, then delete the sheet and, somehow, the form. It is possible, but probably more work than a simple Modal Dialog.
Consider the HTML option as there are numerous simple-to-follow tutorials and examples and resources on the web. If you do end up trying it out, I'd suggest Bootstrap to start!
Good luck either way!

Attempting to use a global array inside of a JS file shared between 2 HTML files and failing

So I have one HTML page which consists of a bunch of form elements for the user to fill out. I push all the selections that the user makes into one global variable, allTheData[] inside my only Javascript file.
Then I have a 2nd HTML page which loads in after a user clicks a button. This HTML page is supposed to take some of the data inside the allTheData array and display it. I am calling the function to display allTheData by using:
window.onload = function () {
if (window.location.href.indexOf('Two') > -1) {
carousel();
}
}
function carousel() {
console.log("oh");
alert(allTheData.toString());
}
However, I am finding that nothing gets displayed in my 2nd HTML page and the allTheData array appears to be empty despite it getting it filled out previously in the 1st HTML page. I am pretty confident that I am correctly pushing data into the allTheData array because when I use alert(allTheData.toString()) while i'm still inside my 1st HTML page, all the data gets displayed.
I think there's something happening during my transition from the 1st to 2nd HTML page that causes the allTheData array to empty or something but I am not sure what it is. Please help a newbie out!
Web Storage: This sounds like a job for the window.sessionStorage object, which along with its cousin window.localStorage allows data-as-strings to be saved in the users browser for use across pages on the same domain.
However, keep in mind that they are both Cookie-like features and therefore their effectiveness depends on the user's Cookie preference for each domain.
A simple condition will determine if the web storage option is available, like so...
if (window.sessionStorage) {
// continue with app ...
} else {
// inform user about web storage
// and ask them to accept Cookies
// before reloading the page (or whatever)
}
Saving to and retrieving from web storage requires conversion to-and-from String data types, usually via JSON methods like so...
// save to...
var array = ['item0', 'item1', 2, 3, 'IV'];
sessionStorage.myApp = JSON.stringify(array);
// retrieve from...
var array = JSON.parse(sessionStorage.myApp);
There are more specific methods available than these. Further details and compatibility tables etc in Using the Web Storage API # MDN.
Hope that helps. :)

How do I pass a value from an HTML form submission to a Google Sheet and back to HTML in a Google Apps Script Web App

I'm trying to create a basic time clock web app.
So far, I'm using this script to create this web app which takes the input values and puts them in this spreadsheet for the time stamping part.
I need it to use one of the values from the form and perform a lookup in this sheet (take the longId and find me the name) and return the (name) value to the html page as a verification for the end user that they were identified correctly. Unfortunately, I don't know enough to grasp what I'm doing wrong. Let me know if I need to provide more info.
Edit 1
I'm thinking that I wasn't clear enough. I don't need the user info from entry, I need the user from a lookup. The user will be entering their ID anonymously, I need to match the ID to their info, and bring the info back for them to verify.
Edit 2
Using the link provided by Br. Sayan, I've created this script using this spreadsheet as above to test one piece of this. The web app here spits out: undefined. It should spit out "Student 3" Still not sure what I'm doing wrong.
One way for the next button to grab the student input field:
<input type="submit" onclick="studentName(document.getElementById('student').value)" value="Next..."/>
That sends the value to this func in Javascript.html:
function studentName(value) {
google.script.run
.withSuccessHandler(findSuccess)
.findStudent(value);
}
Which sends it to a findStudent(value) in Code.gs
You do the lookup and the return value goes back to findSuccess( result ) back in Javascript.html. Handle the result from there.
Also consider keeping the stock preventDefault() code that comes with the Web App template in the Help > Welcome Screen.
Please try this one:
(source: technokarak.com)
Also please have a look at:
Retrieve rows from spreadsheet data using GAS
EDIT:
Please make these changes in your function and let us know.
function findValue() {
var data = SpreadsheetApp.openById("15DRZRQ2Hcd7MNnAsu_lnZ6n4kiHeXW_OMPP3squbTLE").getSheetByName("Volatile Data").getDataRange().getValues();
for(i in data) {
if(data[i][3] == 100000003) {
Logger.log("yes");
Logger.log(data[i][0]);
var student = [];
student.push(data[i][0]);
return student;
}
}
}
It is a complicated answer, I have had a lot of success with:
function process(object){
var user = Session.getActiveUser().getEmail();
var key = object.Key;
send(key);
}
function send(k){
var ss =
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var lastR = ss.GetLastRow();
ss.GetRange(lastR,1).SetValue(k);
}
On your html button you will need to have inside the tags
onClick="google.script.run
.withSuccessHandler(Success)
.process(this.parentNode);"
In order for this to work, obviously you will need to have your fields named accordingly.
Edit: The only thing I did not include in the code was a Success handler, which will be in your html of the GAS script. This should point you in a direction that can resolve that.
Hope this helps.

Categories

Resources