Generating invoice from Google Sheets - google-sheets

I have a Google spreadsheet in which I record my freelance jobs. I have it set up that each line calculates whether it is paid for. (Payments are pulled from a separate sheet.)
What I would like to do is to generate an invoice, where I would select the customer and I get a listing of all unpaid entries for that customer.
Using a arrayed filter function does the job, but I can't use that as an invoice because I need the total line underneath, and would prefer the table format matching the count of entries.
Is it possible to insert such information into a Google Doc as a table, or within Sheets, to push the lines following an array down?
I thought this would be a simple enough concept but I can't find anything that does the full deal.

You could try this script. I'm not sure if the final results is what you are looking for. In case it is not, it can be easily modified:
function onEdit(e) {
//If you change the Customer in the Invoice sheet, it runs the code
if (e.range.getA1Notation() == 'A1' && e.source.getSheetName() == 'Invoice'){
var sprsheet = SpreadsheetApp.getActiveSpreadsheet();
var invoice = sprsheet.getSheetByName("Invoice");
var times = sprsheet.getSheetByName("Times");
var in_customer = invoice.getRange("A1").getValue(); //Name you selected in the dropdown menu
var data = times.getRange("A1:H").getValues(); //All the data from the Time sheet
var total = 0;
//Loops through all the data looking for unpaid subtotals from that customer
for (var i = 0; i < data.length; i++){
/*> "i" represents the row, the second number is the column
> The rows start at 0 since it is the first array position.
*/
if (in_customer == data[i][2]) {
if (data[i][7] == 'N'){
total += Number(data[i][5]); //Accumulates each subtotal into total
invoice.appendRow([data[i][0], data[i][1], data[i][3], data[i][5]]);
}
}
}
invoice.appendRow(["Total: ","","", total]);
}
}
This results in (I changed some values to test it):
As you see I added some headers.
References:
Range Class
onEdit Trigger

Related

Function to check for duplicates based on condition

I have a simple Google sheet that records what sessions people are signed up for (3 concurrent sessions per day):
The same person cannot be in more than 1 session on a given day. I'd like to create a function in column B that checks for that situation and flags it, as in Susan, Keith, and Amy in the example above (I've highlighted in yellow the conditions that would trigger a flag).
If there were just one date, I'd use a countif (or maybe countifs?) to check for more than 1 TRUE for that date. But with multiple dates, I think some sort of iterative function or query is needed. I have a feeling I may be missing a simple formula, but it's eluding me. I may add more dates, so the solution needs to allow for n number of dates in the range.
UPDATE: My scenario has become a little more complex. I'm designating a potential role each person can play in each session and then using the checkboxes to indicate who is playing what role in each session. A given person can't be in more than 1 session per day (but a given person may be in 0 sessions on a given day). The below image shows this updated scenario, with the yellow highlights showing the conditions that I want flagged via the function in column B.
Here's a link to the Google sheet if you want to create a copy.
Given the use case provided, you can apply the formula below to B3 and drag the auto-complete handle:
=IF(ARRAYFORMULA(SUM(INT(C3:K3))) = COUNTUNIQUE($C$1:$1), "", "FLAG")
I'm converting the Boolean values to INT and summing them up. If the sum is equal to the count of unique days in the first row, then everything is fine, otherwise, FLAG!
In other words, if there are more (or less) checks than days, it should be flagged.
You can also set up a conditional formatting to paint the cell accordingly.
Alternatively, if you’d like to treat each scenario you can use =IFS() as below:
=IFS(ARRAYFORMULA(SUM(INT(C3:K3))) > COUNTUNIQUE($C$1:$1), "HIGHER", ARRAYFORMULA(SUM(INT(C3:K3))) < COUNTUNIQUE($C$1:$1), "LOWER", ARRAYFORMULA(SUM(INT(C3:K3))) = COUNTUNIQUE($C$1:$1), "OK")
References:
Sheets Functions documentation
IF
IFS
ARRAYFORMULA
SUM
INT
COUNTUNIQUE
EDIT:
Since the changes in the original scope significantly impacted my previous answer, here is a suggestion using a custom formula:
function checkFlags(){
var ss = SpreadsheetApp.getActive(); // get active Sheets
var ws = ss.getSheetByName("Sheet1"); // getting tab named "Sheet1"
var currentCellRange = ss.getActiveRange(); // getting active cell, in the context of a custom formula, it gets the one being calculated at the time
var rowIndex = currentCellRange.getRowIndex(); //getting current row number
var rowValues = ws.getRange(`${rowIndex}:${rowIndex}`).getValues()[0]; //getting row cells values
var sessionsList = []; //temp variable to store useful data from cells
for (var i = 0; i < rowValues.length; i++) { //reading cells on the row to create a date/flag array
var cell = rowValues[i]; //getting Range of current cell
if (typeof(cell) == 'boolean'){ //if the current cell has a boolean value, it is a session flag
var headerDate = ws.getRange(2, (i+2)).getDisplayValue(); //getting the header value on row 2 (current date for the session flag)
sessionsList.push({date: headerDate, session: cell});//storing date and session flag value on the temp variable
}
};
var groupBy = function(xs, key) { //handle function to proccess the sessionsList variable and group flag values by 'date'
return xs.reduce(function(rv, x) {
(rv[x[key]] = rv[x[key]] || []).push(x);
return rv;
}, {});
};
var tempGroupedArray = groupBy(sessionsList, 'date'); //grouping temp sessionsList by 'date'. This will return an array like [ { date: '<date>', session: true/false }, { date: '<date>', session: true/false }, ...]
for (dateFlags of Object.entries(tempGroupedArray)) {//looping through the `grouped by date` array
var tempCount = dateFlags[1].filter(x => x.session==true).length; //how many `trues` are for this date
if (tempCount > 1){ //if there is more than one session set as "true" for this date...
return 'FLAG'; //...immediately stop execution and return FLAG
}
};
//if it passed the loop above, it means there is no flags
return ''; //then return blank
}
NOTES: This custom formula will not update the result in the cell after a change on the flag values, you must delete/paste it to force if wanted.

How to pass cell value to a SUM function

I am trying to do a SUM of last 12 rows in the column (I'll be adding more rows into this column so I wanted to automate the calculation).
First of all, I am able to get the value of last cell with some value in this column by =SUMPRODUCT(MAX((B1:B200<>"")*ROW(B1:B200))) - result is stored in C1. However, I am not sure how to use this value inside the SUM formula, I was thinking something like =SUM(B(get value of C1)-12:B(get value of C1).
I tried multiple things but none of them have worked - I also don't mind using a different approach if it gets the job done.
You can create your own custom function to do that using Google Apps Script (GAS).
Try the following:
function onEdit(e){
var row = e.range.getRow();
var col = e.range.getColumn();
if ( col==2 && e.source.getActiveSheet().getName() == "Sheet1" ){
e.source.getActiveSheet().getRange("C1").setValue(sumLast12());
}
}
function sumLast12() {
var sheet = SpreadsheetApp.getActive().getSheetByName("Sheet1");
var sheet_size = sheet.getLastRow();
var elmt = sheet.getRange("B1:B"+sheet_size).getValues().flat([1]);
var elmt12 = elmt.slice(-12);
var sum = 0;
for( var i = 0; i < elmt12.length; i++ ){
sum += parseInt( elmt12[i], 10 );
}
return sum;
}
Explanation:
In order to activate this functionality go the menu bar on top of the
spreadsheet file and click on Tools => Script editor and copy the
aforementioned code into a blank script document (see attached
screenshot for more information) and save the document (cntrl+s).
After the script has been saved, everytime you edit a cell in column
B (either by adding a new value on the bottom or modify an existing value, the script will automatically update the value in
cell C1 with the sum of the last 12 values in column B.
Note that if you don't want to change my code, name the sheet you are working with as Sheet1.
Does this work?
=SUM(FILTER(B:B,ROW(B:B)>=MAX(ROW(B:B))-12)

Importing Url's of sheets shared with me into google sheets automatically whenever one is shared

I'm pretty new to excel or google sheets. The work place, that I work at does not have anything stream lined.
I'm trying to create my own work book that I can refresh everyday I log in so that I can have a list of things that I need to work on for that day.
One of the functions that I would like to have is, whenever a new sheet is shared with me on Google Sheets, I want the URL for that sheet to populate in one of the cells in my workbook automatically and arranged based on timestamp.
I was trying to search for this on Google, but I read that: shared with me docs are not stored anywhere specifically.
Any help or pointing me in the right direction is highly appreciated.
It is easy to fetch the files that have been shared with you. For that, you can simply call the Drive API's Files: list method specifying in the q parameter the sharedWithMe attribute.
Afterwards, you can use the SpreadsheetApp class to gather a spreadsheet and insert data into it. In this case, you can simply make several calls of apendRow() to insert the results.
Finally, properties can be used to store the status of the script (last date checked), so that it can know where to resume from. In this case below, we'll be saving the date in the LAST_DATE property.
Code.gs
var SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';
var SHEET_NAME = 'YOUR_SHEET_NAME';
function myFunction() {
var sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
var lastDate = new Date(PropertiesService.getScriptProperties().getProperty('LAST_DATE'));
var currentDate = new Date();
var files = getFiles(lastDate);
for (var i=0; i<files.length; i++) {
var row = [
new Date(files[i].sharedWithMeDate),
files[i].title,
files[i].alternateLink,
files[i].sharingUser.emailAddress,
files[i].owners.map(getEmail).join(', ')];
sheet.appendRow(row);
}
console.log('lastDate: %s, currentDate: %s, events added: %d', lastDate, currentDate, files.length);
PropertiesService.getScriptProperties().setProperty('LAST_DATE', currentDate.toISOString());
}
function getEmail(user) {
return user.emailAddress;
}
function getFiles(lastSharedDate) {
var query = "sharedWithMe and mimeType = 'application/vnd.google-apps.spreadsheet'";
var res = Drive.Files.list({
q: query,
orderBy: "sharedWithMeDate desc",
fields: "*",
pageSize: 1000
});
// `query` parameter cannot compare sharedWithMeDate, so we do it afterwards
return res.items.filter(function (i) {
return (new Date(i.sharedWithMeDate) > lastSharedDate);
}).reverse();
}
You can set up the script to be ran periodically (i.e once a day, or more in case you'd need it) using Time-driven triggers.

Should this be a SUMIF formula?

I'm trying to make a formula that can recognize in Column A the name Brooke B for instance here, from there I'd like to SUM the values listed in Column I Cash Discounts for that specific user.
(Yes this user has no Cash Discounts, thus column I states "Non-Cash Payment").
There's about 80 users total here, so I'd prefer to automate the name recognition in Column A.
Sheet: https://docs.google.com/spreadsheets/d/1xzzHT7VjG24UJ4ZXaiZWsfzroTpn7jCJLexuTOf6SQs/edit?usp=sharing
Desired Results listed in Cash Discounts sheet, listed per user in column C.
You are trying to calculate the total amount of the Cash Discount per person given to people in a list. You have data that has been exported from a POS system to which that you have added a formula to calculate the amout of the discount on a line by line basis. You have speculated whether the discount totals could be calculated using SUMIFS formulae.
In my view, the layout of the spreadsheet and the format of the POS report do not lend themselves to isolating discrete data elements though Google sheets functions (though, no doubt, someone with greater skills than I will disprove this theory). Column A, containing names, also includes sub-groupings (and their sub-totals) as well as transaction dates. There are 83 unique persons and over 31,900 transaction lines.
This answer is a script-based solution which updates a sheet with the names and values of the discount totals. The elapsed execution time is #11 seconds.
function so5882893202() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// get the Discounts sheet
var discsheetname = "Discounts";
var disc = ss.getSheetByName(discsheetname);
//get the Discounts data
var discStartrow = 3;
var discLR = disc.getLastRow();
var discRange = disc.getRange(discStartrow, 1, discLR-discStartrow+1, 9);
var discValues = discRange.getValues();
// isolate Column A
var discnameCol = discValues.map(function(e){return e[0];});//[[e],[e],[e]]=>[e,e,e]
//Logger.log(discnameCol); // DEBUG
// isolate Column I
var discDiscounts = discValues.map(function(e){return e[8];});//[[e],[e],[e]]=>[e,e,e]
//Logger.log(discDiscounts); // DEBUG
// create an array to build a names list
var names =[]
// get the number of rows on the Discounts sheet
var discNumrows = discLR-discStartrow+1;
// Logger.log("DEBUG: number of rows = "+discNumrows);
// identify search terms
var searchPercent = "%";
var searchTotal = "Total";
// loop through Column A
for (var i=0; i<discNumrows; i++){
//Logger.log("DEBUG: i="+i+", content = "+discnameCol[i]);
// test if value is a date
if (Object.prototype.toString.call(discnameCol[i]) != "[object Date]") {
//Logger.log("it isn't a date")
// test whether the value contains a % sign
if ( discnameCol[i].indexOf(searchPercent) === -1){
//Logger.log("it doesn't have a % character in the content");
// test whether the value contains the word Total
if ( discnameCol[i].indexOf(searchTotal) === -1){
//Logger.log("it doesn't have the word total in the content");
// test whether the value is a blank
if (discnameCol[i] != ""){
//Logger.log("it isn't empty");
// this is a name; add it to the list
names.push(discnameCol[i])
}// end test for empty
}// end test for Total
} // end for percentage
} // end test for date
}// end for
//Logger.log(names);
// get the number of names
var numnames = names.length;
//Logger.log("DEBUG: number of names = "+numnames)
// create an array for the discount details
var discounts=[];
// loop through the names
for (var i=0;i<numnames;i++){
// Logger.log("DEBUG: name = "+names[i]);
// get the first row and last rows for this name
var startrow = discnameCol.indexOf(names[i]);
var endrow = discnameCol.lastIndexOf(names[i]+" Total:");
var x = 0;
var value = 0;
// Logger.log("name = "+names[i]+", start row ="+ startrow+", end row = "+endrow);
// loop through the Cash Discounts Column (Column I) for this name
// from the start row to the end row
for (var r = startrow; r<endrow;r++){
// get the vaue of the cell
value = discDiscounts[r];
// test that it is a value
if (!isNaN(value)){
// increment x by the value
x = +x+value;
// Logger.log("DEBUG: r = "+r+", value = "+value+", x = "+x);
}
}
// push the name and the total discount onto the array
discounts.push([names[i],x]);
}
//Logger.log(discounts)
// get the reporting sheet
var reportsheet = "Sheet10";
var report = ss.getSheetByName(reportsheet);
// define the range (allow row 1 for headers)
var reportRange = report.getRange(2,1,numnames,2);
// clear any existing content
reportRange.clearContent();
//update the values
reportRange.setValues(discounts);
}
Report Sheet - extract
Not everyone wants a script solution to their problem. This answer seeks to supply a repeatable solution using common garden-variety formula/functions.
As noted elsewhere, the layout of the spreadsheet does not lend itself to a quick/simple solution, but it IS possible to break down the data to compile a non-script answer. Though it may "seem" as though the following formula are less than "simple, when taken one-at-a-time they are logical, very easy to create, and very easy to verify successful outcomes.
Note: It is important to know at the outset that the first row of data = row#3, and the last row of data = row#31916.
Step#1 - get Text values from ColumnA
Enter this formula in Cell J3, and copy to row 31916
=if(isdate(A3),"",A3):
evaluates Column A, if the content is a date, returns blank, otherwise, returns the context
Taking Customer "AJ" as an example, the content at this point includes:
AJ
10% BuildingDiscount
10% BuildingDiscount Total:
Northwestern 10%
Northwestern 10% Total:
AJ Total:
Step#2 - ignore the values that contain "10%" (this removes both headings and sub-subtotals
Enter this formula in Cell K3 and copy to row 31916
=iferror(if(search("10%",J3)>0,"",J3),J3): searches for "10%" in Column J. Returns all values except those that containing "10%".
Taking Customer "AJ" as an example, the content at this point includes:
AJ
AJ Total:
**Step#3 - ignore the values that contain the word "Total"
Enter this formula in Cell L3 and copy to row 31916.
=iferror(if(search("total",K3)>0,"",K3),K3)
Taking Customer "AJ" as an example, the content at this point includes:
AJ
Results after Step#3
You might wonder, "couldn't this be done in a single formula?" and/or "an array formula would be more efficent". Both those thoughts are true, but we're looking at simple and easy, and a single formula is NOT simple (as shown below); and given that, an array formula is out-of-the-question unless/until an expert can wave a magic wand over the data.
FWIW - Combining Steps#1, 2 & 3
each of the Steps#1, 2 and 3 build on each other. So it is possible to create a single formula that combines these steps.
enter this formula in Cell J3, and copy dow to row #31916.
=iferror(if(search("total",iferror(if(search("10%",if(isdate(A3),"",A3))>0,"",if(isdate(A3),"",A3)),if(isdate(A3),"",A3)))>0,"",iferror(if(search("10%",if(isdate(A3),"",A3))>0,"",if(isdate(A3),"",A3)),if(isdate(A3),"",A3))),iferror(if(search("10%",if(isdate(A3),"",A3))>0,"",if(isdate(A3),"",A3)),if(isdate(A3),"",A3)))
As the image showed, step#3 concludes with mainly empty cells in Column L; the only populated cell is the first instance of the customer name at the start of their transactions - such as "Alec" in this example. However (props to #Rubén) it is possible to populate the blank transaction Cells in Column L. An arrayformula to find the previous non-empty cell in another column on Webapps explains how.
Step#4 - Create a customer name for each transaction row.
Enter this formula in Cell M3, it will automatically populate the cells to row#31916
=ArrayFormula(vlookup(ROW(3:31916),{IF(LEN(L3:L31916)>0,ROW(3:31916),""),L3:L31916},2))
Step#5 - Get the discount amount for each transaction value
The discount values are already displayed in Column I. They are interspersed with text values, so the formula for tests if this is a total line by testing the value in Column D; only if there is a vale (Product item) does the formula then test of there is a value in column I.
Enter this formula in Cell N3, it will automatically populate the cells to row#31916
=ArrayFormula(if(len(D3:D31914)>0,if(ISNUMBER(I3:I31916),I3:I31916,0),""))
Screenshot after step#5
Reporting by Query
Reporting is done via queries. These can go anywhere, but it is probably more convenient to put it on a separate sheet.
Step#6.1 - query the results to create report showing total by ALL customers
=query(Discounts_analysis!$M$2:$N$31916,"select M, sum(N) where N is not null group by M label M 'Customer', sum(N) 'Total Discount' ",1)
Step#6.2 - query the results to create report showing total by customer where the customer received a discount
=query(Discounts_analysis!$M$2:$N$31916,"select M, sum(N) where N >0 group by M label M 'Customer', sum(N) 'Total Discount' ",1)
Step#6.3 - query the results to create report showing customers with no discount
- `=query(query(Discounts_analysis!$M$2:$N$31916,"select M, sum(N) where N is not null group by M label M 'Customer', sum(N) 'Total Discount' ",1),"select Col1 where Col2=0")`
Queries screenshot

Trying to write to a Google Sheet opened with "openById"

Project description: Begin with a roster spreadsheet with multiple tabs (one for each group), each tab with a list of members for rows. Columns are rehearsal or program dates. Second sheet contains scanned attendance records. Open the attendance record sheet and read each record. One column contains tab name of sheet on the roster sheet. Change to that sheet and search for the scanned member. Mark the column matching the date with an X to denote attendance. Mark the scanned attendance record with an X to denote processed.
Everything is working save the final writing of the X to the sheet opened by ID. I can't figure out the syntax to update the appropriate row/col cell.
I'd appreciate some help. Thanks!
function processAttendanceRecords(){
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var ss = SpreadsheetApp.openById("1234567");
var data = ss.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
var rhrsDate = Utilities.formatDate(data[i][3], "PST", "M/d");
sheet.setActiveSheet(sheet.getSheetByName(data[i][1]));
var members = sheet.getDataRange().getValues();
var col = members[0].indexOf(rhrsDate);
for (var j = 1; j < members.length; j++) {
if (data[i][6] == members[j][0] && data[i][7] == members[j][1]) {
SpreadsheetApp.getActiveSheet().getRange(j+1,col+1).setValue('X');
SpreadsheetApp.getActiveSheet(ss).getRange(i+1,9).setValue('X');
}
}
}
}
If you are accessing spreadsheet by ID then it is good practice to mention the sheet name or else it will take the first sheet by default.
var ss = SpreadsheetApp.openById("123456").getSheetByName("Sheet1");
Since you haven't defined the sheet name you can't use getRange(Row, Column). So your need to mention the sheet name like above and then change the last line.
ss.getRange(i+1,9).setValue('X');

Resources