Wix Data: Complete Invoice System

Hi, First I am a beginner that has started a project for advanced coders. I was able to receive help and with that I almost finished my project.
To complete, I first need to finish this invoice system. The New Invoice page needs a few details done:

  1. New Invoice No need to be changed to autovalue.
  2. All prices need to be shown with $
  3. Sum prices, tax and total amount
  4. Attracting customers from the customers collection and save in the invoices.

live: https://hortiray.wixsite.com/database/fguy48-uop9l2arwq3

  1. the invoiceNo number field refering to Invoices Collection number field invoiceNo need to be set to autovalue counting from 0001 to 9999. Currently it has code but line 8-11(in bold) has error: wrong spot and no function.
    2)$: The existing code is not working but has worked on other pages before. Might be a different problem?
  2. No code yet, I would like:
    priceInput1 + priceInput2 + priceInput3 = subtotal % tax (input20)= taxamount
    subtotal + taxamount = totalPriceInput
    (Important that priceInput1 and priceinput2 can stay empty)
  3. Existing Customer Dropdown: dropdown1 connecting to the address, suburb, state, postcode, country inputs and resetDropdownsButton

90% of the code I re-used from previous questions/mini-tutorials:
https://www.wix.com/code/home/forum/community-discussion/wix-data-dropdown-multiple-collections-and-repeater and https://www.wix.com/code/home/forum/community-discussion/dropdown-and-database
I am not sure if it works since I have an error in the code but I foresee some issues.

import wixData from 'wix-data';

$w.onReady(function () {
uniqueDropDown1();
$w('#dropdown1').onChange(dropdownHasChanged);
$w('#resetDropdownsButton').onClick(resetDropdownsButton_clicked);
});
			
let newInvoiceNo = await getNewInvoiceNumber();
if (!newInvoiceNo) {newInvoiceNo = 0;}
$w("#invoiceNo").text = newInvoiceNo;;
	

export function priceInput1_change(event, $w) {
let num = parseFloat($w("#priceInput1").value)
$w("#priceInput1").value = '$' + num.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,")
}
export function priceInput2_change(event, $w) {
let num = parseFloat($w("#priceInput2").value)
$w("#priceInput2").value = '$' + num.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,")
}
export function priceInput3_change(event, $w) {
let num = parseFloat($w("#priceInput3").value)
$w("#priceInput3").value = '$' + num.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,")
}
export function subtotal_change(event, $w) {
let num = parseFloat($w("#subtotal").value)
$w("#subtotal").value = '$' + num.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,")
}
export function taxamount_change(event, $w) {
let num = parseFloat($w("#taxamount").value)
$w("#taxamount").value = '$' + num.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,")
}
export function totalPriceInput_change(event, $w) {
let num = parseFloat($w("#totalPriceInput").value)
$w("#totalPriceInput").value = '$' + num.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,")
}
export function getNewInvoiceNo() {
 return wixData.query("Invoices")
    .descending("_createdDate")
    .limit(1)
    .find()
    .then((results => {
      let lastAddedItem = results.items[0];
      let lastInvoiceNo = lastAddedItem.invoiceNo;
      let newInvoiceNo = lastInvoiceNo++;
      return newInvoiceNo;
    })
}

function resetDropdownsButton_clicked(event) {
$w('#dropdown1').selectedIndex = undefined;
$w('#dropdown1').resetValidityIndication();
$w('#adressInput').value = undefined;
$w('#suburbInput').value = undefined;
$w('#stateInput').value = undefined;
$w('#postcodeInput').value = undefined;
$w('#countryInput').value = undefined;
dropdownHasChanged(); 
	}

function uniqueDropDown1() {
wixData.query("Customers")
.limit(1000)
.find()
.then(results => {
const uniqueTitles = getUniqueTitles(results.items, 'customerName');
$w("#dropdown1").options = buildOptions(uniqueTitles);
});
	}

function getUniqueTitles(items, columnKey) {
const titlesOnly = items.map(item => item[columnKey]);
let result = [];
let uniqueSet = new Set(titlesOnly);
let setIterator = uniqueSet.entries();
for (let entry of setIterator) {
let entryValue = entry[0];
if (entryValue) {
result.push(entryValue);
        	}   
    	}
 return result;
	}

function buildOptions(uniqueList) {
return uniqueList.map(curr => {
return { label: curr, value: curr };
		});
	}

function dropdownHasChanged(event) {
let datasetFilter = wixData.filter();
let dropdownValue = "NoCustomerSelected";
if ($w('#dropdown1').selectedIndex >= 0 ) {
   dropdownValue = $w('#dropdown1').value;
   datasetFilter = datasetFilter.eq('customerName', dropdownValue);
		}
$w('#dataset2').setFilter(datasetFilter)
.then(() => {
return $w('#dataset2').refresh();
		})
.then(() => {
return wixData.query("Customers")  // Make sure this is the correct name
.eq("customerName", dropdownValue)
.find();
		})
.then((result) => {
if (result.length !== 1) {
	throw Error("Internal Error? Customer record look up for " +
	dropdownValue + " returned " +
	result.length.toString() + "records");
			}
			let CustomerRecord = result.items[0];
			$w('#adressInput').value = customerRecord.cycle;
			$w('#suburbInput').value = customerRecord.cycle;
			$w('#stateInput').value = customerRecord.cycle;
			$w('#postcodeInput').value = customerRecord.cycle;
			$w('#countryInput').value = customerRecord.cycle;
		})
.catch((error) => {
// If we get here we have encountered an error so console.log() it for now
			console.log(error.message);
			});
	}

Any assistance would be highly appreciated!

1 Like

Hey man!
This is a more complex post so I will try to help you with some of it.

You can’t have code living alone in your page code like you have with the invoice number stuff. You will need to include it inside the page onReady.

$w.onReady(function () { uniqueDropDown1(); 
  let newInvoiceNo = await getNewInvoiceNumber(); 
  if (!newInvoiceNo) {newInvoiceNo = 0;} 
  $w("#invoiceNo").text = newInvoiceNo;;
}); 			  	

I would also recommend you when adding event handlers in Wix Code that you click the element on your page, like the dropdown and then you go to the properties panel on the right and choose under events onChange and click the + icon and it will create a stand alone function for that event handler.

@andreas-kviby Hi mate, thanks for the response. I tried to put the code in the onReady function but it gave me errors. See picture.

I also got confused with the No vs. Number. I changed everything (codes, fields) to only No. Would the code still work?


And at last, would you have any ideas on how to tackle the other problems…?

@hortiray Change:

$w.onReady(function () {

to:

$w.onReady(function async () {

Roi.

@roi-bendet Thanks mate! Will do!

@roi-bendet Unfortunately this did not solve the problem…

@hortiray Can you please share the name of the page?
Roi.

Hi Roi the page is https://hortiray.wixsite.com/database/fguy48-uop9l2arwq3 .

@hortiray Can I suggest that you do a couple of things that might help you solve this and help the rest of us help you too :slight_smile:

  1. Try writing your code so that it describes the use case you are trying to implement. What I mean by this is use function names that describe what you expect the outcome of the function to be and/or add comments to help describe this.

Put comment section before each of your functions in the code. Use it to describe what the function does and what it returns (if anything) when the function exits (e.g. on success the function returns a sorted array, on failure the function throws an exception reporting one of these errors:…).

Add other comments, for example at the start of the page, which describes how you see (your use case) the functions being used. For example:

/***************
  Page Name:   "name of the page"
  Usage:
   This page is loaded when <event> occurs on the site. The page 
   is dynamic and so it is expected that the key record ID is 
   available from the dynamicDataset currentRecord.
   Most elements on this page are set automatically from the
   dataset through Wix Editor bindings except the totals element 
   named $w("#totals") and the average element $w('#average')
   which are calculated in the onReady handler.

   When the page is loaded the onReady function is used to          
   initialize data that needs to be calculated from ...
***************/
  1. Try to use console.log() statements at critical points where the value of your variables or arguments might be null or undefined. In these cases you may find problems due to bad values and you may need to add defensive condition testing to make sure you don’t try to process these bad values. When you find a bad value you can then back track through your code to find out how the bad value occurred. I like to add log statements that reflect the function calls
function logMessage(message) {
    // Note the use of conditional telling me if the argument I 
    // received is null or not
    console.log('logMessage('+(message?message:'null!')+')');
}
  1. Add function stubs for planned functions, that do nothing, but can show how you intend to extend your code. For example
// The following function does ...
function calcAvg(numberList) {
    console.log('TEMP TO BE WRITTEN: calcAvg called');
}

This may seem like a lot of work but it is work that you really need to do when designing your code anyway. This also helps when others try to read your code and reverse engineer it. As you do this you often have an AHHHH! moment where you see where you have an error because you have described what your function does and you realize it isn’t doing that :-).

You have a lot of code as Andreas said above and the more code you have without comments the harder it becomes to understand where a problem (other than a syntax error) might be occurring. Additional data, such as info from console logs helps here.

Cheers
Steve

$w.onReady(async function () {
	uniqueDropDown1();
	$w('#dropdown1').onChange(dropdownHasChanged);
	$w('#resetDropdownsButton').onClick(resetDropdownsButton_clicked);
	let newInvoiceNo = await getNewInvoiceNo();
	if (!newInvoiceNo) { newInvoiceNo = 0; }
	$w("#invoiceNo").value = newInvoiceNo;
});

@hortiray Building on what Roi has added above. You need to do several other things in your code.

  • You have a syntax error on line 43 you need to add a closing parenthesis after results
.then((results) => {
  • On line 132 you need to make CustomerRecord => customerRecord :wink:

  • On line 46 you are incrementing the invoice number AFTER you assign it to newInvoiceNo you should increment it first.

let newInvoiceNo = lastInvoiceNo++;

SHOULD BE

let newInvoiceNo = ++lastInvoiceNo;

So in summary:

  1. You can only use the element scope selector $w inside a function.
  2. If you use the await form of a function that returns a Promise you need to make the function that uses await an async function. The word async must precede the word function in the declaration:
$w.onReady(async function () {

and NOT

$w.onReady(function async () {
  1. Your getNewInvoiceNumber() function call should be getNewInvoiceNo() because this is what your function is called.
  2. Fix line 43 parenthesis.
    5.Use correct text case on line 132
  3. Fix the incrementing syntax on line 46.

Steve

Thanks @Roi Bendet and @Steve Cropper,
I implemented all changes (except for adding comments) and found no more faults . While I will add comments and then post the code, I like to share the following:

  1. InvoiceNo: When I want to create new invoice, no new invoice number is displayed and numbers are only one number (not 0001 but just 1).
  2. $ is working for most fields, except the fields that are read only, that is ok for now since the summing need to be done for those fields which was/is question 3.
  3. Summing of fields, see original question has no coding yet, do you have any ideas?
  4. The dropdown box with related fields, I tried to re-use the codes from previous tutorials but it is not working.
  • In the functionresetDropdownsButton_clicked: Do I add resetValidityIndication for all the fields?

Thank you in advance…

@hortiray I think I have mentioned this before on your other posts. You need to be careful when using code AND binding elements to a dataset. If you have connected element values to a dataset AND you try to change the values you will get strange effects. You should do one or the other NOT both :-).
If the dataset Icon on an element looks like this:

Then you shouldn’t try to manipulate the value for that element in code. Disable the binding and do what you need to in code.

You will need to keep track of the floating values for prices as well as the text values or parse the value strings when performing calculations (to add and strip the ā€œ$ā€ character). Your invoice number and all of your prices are all connected to the dataset so this is one of the primary problems you are experiencing. A simple idea here would be to create an array of objects that contain the Number value for the price and a text version. Then when you want to sum values you can create a special sum object. One simple idea here is to create an invoice price class where each element has its own object in an invoice object like this.

// Create price object
let invoicePrices = {
    "priceInput1": {},
    "priceInput2": {},
    "priceInput3": {},
    "subtotal": {},
    "taxamount": {},
    "totalPriceInput": {},
};

// updateInvoicePrice
// A convenience function that creates or updates an invoicePrice object
// elementName: identifies the element whose value is changing
// value: is optional. If not present the value will be taken from the element
function updateInvoicePrice(elementName, value) {
    let selectedElementPrice = invoicePrices[elementName];
    if (selectedElementPrice) {
        // get element from elementName
        let priceElement = $w('#'+elementName);
        // Select where the value will be taken from argument or element.value
        value = (value ? value : priceElement.value);
        let num = parseFloat(value);
        // Add values to our invoicePrices object
        // Float value
        selectedElementPrice.number = num;
        // Formatted string value
        selectedElementPrice.formattedString = '$' + num.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
        // Change display value
        priceElement.value = selectedElementPrice.formattedString;
    } else {
        console.log("No such element with name:"elementName);
    }
}

// --------------   Change event methods
// We only track changes to actual prices. All other prices are calculated whenever a change 
// occurs to a main price
export function priceInput1_change(event, $w) {
    updateInvoicePrice("priceInput1");
    updateSubtotal();  // Update subtotal - this cascades to other field values 
}
export function priceInput2_change(event, $w) {              
    updateInvoicePrice("priceInput2");
    updateSubtotal();  // Update subtotal - this cascades to other field values  
}

export function priceInput3_change(event, $w) { 
    updateInvoicePrice("priceInput3");
    updateSubtotal();  // Update subtotal - this cascades to other field values 
}

// Convenience function to validate existence of a pricing element value
function hasPriceElement(elementName) {
     return (
         invoicePrices.hasOwnProperty(priceKey) &&
         priceKey.includes(elementName) &&
         invoicePrices[priceKey].number
      );
}

// updateSubtotal: Used to update subtotal whenever one of the price components changes
function updateSubtotal() {
    let sum = 0.0;
    for (let priceKey in invoicePrices) {
        if (hasPriceElement("priceName")) {
            sum += invoicePrices[priceKey].number;
        }
    }
    updateInvoicePrice("subtotal", sum);
    updateTaxAmount();   // Update tax amount - this cascades to other field values 
}

// updateTaxAmount: Used to calculate tax amount when the sub total is changed.
function updateTaxAmount() {
    let taxRate = $w('#input20').value;
    let percentIndex = taxRate.indexOf("%");
    let taxRate = parseFloat(taxRate.substring(0, percentIndex))/100;
    let taxAmount = (hasPriceElement("subtotal") ? invoicePrices.subtotal.number * taxRate);
    updateInvoicePrice("taxamount", taxAmount);
    updateTotalPrice();
}

// updateTotalPrice: used to update the total price when the tax amount has been updated.
function updateTotalPrice() {
    if (hasPriceElement("subtotal") && hasPriceElement("taxamount")) {
        // Add subtotal to taxamount to get our total price
        updateInvoicePrice(
            "totalPriceInput", 
            invoicePrices.subtotal.number +
             invoicePrices.taxamount.number
        );
    }
}

Now with this code you only need to change the prices for priceInput elements. The other values will auto calculate. You will need to use code to update the data collection. You can either use the wix-data save() function or the WixDataset.save() function to do this.

In addition, you need to think about the number of users you are dealing with as website visitors. If you have two or more users visit the site at the same time, the invoice number you give, based on your current algorithm which takes data from the data collection, could be duplicated. Basically they will both read the last invoice number and be given the same next number. You might want to come up with a different model, or only generate the invoice number upon a save operation where you can test for the number existing before the save and if it does, then generate a new one to avoid duplication.

Cheers
Steve

@stevendc Thank you very much.
Yes indeed you have told me to NOT connect to dataset if I use code. I must have been very busy while creating or testing that I have just missed that. I now disconnected the fields (except the product and price-fields). Do I need to disconnect the price fields as well since there is some code for that?
FYI: I changed the page to a write only page.

/***************
  Page Name:   "New Invoice - Write Only"
  Usage:
   This page is loaded when <event> occurs on the site. The page 
   is dynamic and so it is expected that the key record ID is 
   available from the dynamicDataset currentRecord.
   Most elements on this page are set automatically from the
   dataset through Wix Editor bindings except the totals element 
   named $w("#totals") and the average element $w('#average')
   which are calculated in the onReady handler.

   When the page is loaded the onReady function is used to          
   initialize data that needs to be calculated and saved in the Invoice dataset.
   The existing customer data will be taken from the customer dataset.
***************/

import wixData from 'wix-data';

$w.onReady(async function () {
	uniqueDropDown1();
	$w('#dropdown1').onChange(dropdownHasChanged);
	$w('#resetDropdownsButton').onClick(resetDropdownsButton_clicked);

	let newInvoiceNo = await getNewInvoiceNo();
	if (!newInvoiceNo) { newInvoiceNo = 0; }
	$w("#invoiceNo").value = newInvoiceNo;
});

// Create new invoice No
export function getNewInvoiceNo() {
 	return wixData.query("Invoices")
    .descending("_createdDate")
    .limit(1)
    .find()
    .then((results) => {
      let lastAddedItem = results.items[0];
      let lastInvoiceNo = lastAddedItem.invoiceNo;
      let newInvoiceNo = ++lastInvoiceNo;
      return newInvoiceNo;
    })
}

// Create price object
let invoicePrices = {
    "priceInput1": {},
    "priceInput2": {},
    "priceInput3": {},
    "subtotal": {},
    "taxamount": {},
    "totalPriceInput": {},
};

// updateInvoicePrice
// A convenience function that creates or updates an invoicePrice object
// elementName: identifies the element whose value is changing
// value: is optional. If not present the value will be taken from the element
function updateInvoicePrice(elementName, value) {
    let selectedElementPrice = invoicePrices[elementName];
    if (selectedElementPrice) {
        // get element from elementName
        let priceElement = $w('#'+elementName);
        // Select where the value will be taken from argument or element.value
        value = (value ? value : priceElement.value);
        let num = parseFloat(value);
        // Add values to our invoicePrices object
        // Float value
        selectedElementPrice.number = num;
        // Formatted string value
        selectedElementPrice.formattedString = '$' + num.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
        // Change display value
        priceElement.value = selectedElementPrice.formattedString;
    } else {
        console.log("No such element with name:"elementName);
    }
}

// --------------   Change event methods
// We only track changes to actual prices. All other prices are calculated whenever a change 
// occurs to a main price
export function priceInput1_change(event, $w) {
    updateInvoicePrice("priceInput1");
    updateSubtotal();  // Update subtotal - this cascades to other field values 
}
export function priceInput2_change(event, $w) {              
    updateInvoicePrice("priceInput2");
    updateSubtotal();  // Update subtotal - this cascades to other field values  
}

export function priceInput3_change(event, $w) { 
    updateInvoicePrice("priceInput3");
    updateSubtotal();  // Update subtotal - this cascades to other field values 
}

// Convenience function to validate existence of a pricing element value
function hasPriceElement(elementName) {
     return (
         invoicePrices.hasOwnProperty(priceKey) &&
         priceKey.includes(elementName) &&
         invoicePrices[priceKey].number
      );
}

// updateSubtotal: Used to update subtotal whenever one of the price components changes
function updateSubtotal() {
    let sum = 0.0;
    for (let priceKey in invoicePrices) {
        if (hasPriceElement("priceName")) {
            sum += invoicePrices[priceKey].number;
        }
    }
    updateInvoicePrice("subtotal", sum);
    updateTaxAmount();   // Update tax amount - this cascades to other field values 
}

// updateTaxAmount: Used to calculate tax amount when the sub total is changed.
function updateTaxAmount() {
    let taxRate = $w('#input20').value;
    let percentIndex = taxRate.indexOf("%");
    let taxRate = parseFloat(taxRate.substring(0, percentIndex))/100;
    let taxAmount = (hasPriceElement("subtotal") ? invoicePrices.subtotal.number * taxRate);
    updateInvoicePrice("taxamount", taxAmount);
    updateTotalPrice();
}

// updateTotalPrice: used to update the total price when the tax amount has been updated.
function updateTotalPrice() {
    if (hasPriceElement("subtotal") && hasPriceElement("taxamount")) {
        // Add subtotal to taxamount to get our total price
        updateInvoicePrice(
            "totalPriceInput", 
            invoicePrices.subtotal.number +
             invoicePrices.taxamount.number
        );
    }
}

//Resets existing customer fields.
	function resetDropdownsButton_clicked(event) {
    	$w('#dropdown1').selectedIndex = undefined;
		$w('#dropdown1').resetValidityIndication();
		$w('#adressInput').value = undefined;
		$w('#suburbInput').value = undefined;
		$w('#stateInput').value = undefined;
		$w('#postcodeInput').value = undefined;
		$w('#countryInput').value = undefined;
    	dropdownHasChanged(); 
	}

//Displays existing customers from the customer dataset.
	function uniqueDropDown1() {
		wixData.query("Customers")
		.limit(1000)
		.find()
		.then(results => {
			const uniqueTitles = getUniqueTitles(results.items, 'customerName');
			$w("#dropdown1").options = buildOptions(uniqueTitles);
		});
	}

	function getUniqueTitles(items, columnKey) {
		const titlesOnly = items.map(item => item[columnKey]);
 		// We need to return an array
 		let result = [];
 		// Make our list unique by adding it to a Set object
 		let uniqueSet = new Set(titlesOnly);
 		// Get the iterator to cycle through each record
 		let setIterator = uniqueSet.entries();
 		for (let entry of setIterator) {
 		// Add the primary value of each record to our array
 		let entryValue = entry[0];
 		if (entryValue) {
 		// Only use valid values
        result.push(entryValue);
        	}   
    	}
 	return result;
	}

	function buildOptions(uniqueList) {
		return uniqueList.map(curr => {
		return { label: curr, value: curr };
		});
	}

	function dropdownHasChanged(event) {
   	 	// Assume we will build a dataset filter
    	let datasetFilter = wixData.filter();
    	// Get the value of the dropdown if one is selected
    	// If the selectedIndex of the dropdown is >= 0 then a value exists
		let dropdownValue = "NoCustomerSelected";
		if ($w('#dropdown1').selectedIndex >= 0 ) {
    	    dropdownValue = $w('#dropdown1').value;
       	    // If we get here either we found a value, in which case the filter will work.
    	    // or we didn't, which means the dropdown has been reset, in which case the filter is
    	    // Removed by using an empty query.
			datasetFilter = datasetFilter.eq('customerName', dropdownValue);
		}
		$w('#dataset2').setFilter(datasetFilter)
		.then(() => {
    	    return $w('#dataset2').refresh();
		})
		// We returned the refresh result above so we can chain a new then
		.then(() => {
		// The refresh worked so now we fetch the other fields
		// The result comes in a Promise so we return the query
			return wixData.query("Customers")  // Make sure this is the correct name
			.eq("customerName", dropdownValue)
			.find();
		})
		.then((result) => {
			// The result is a WixDataQueryResult
			// We should only have one Customer record!
			if (result.length !== 1) {
				// We catch this error below
				throw Error("Internal Error? Customer record look up for " +
					dropdownValue + " returned " +
					result.length.toString() + "records");
			}
			// We have a record
			let customerRecord = result.items[0];
			
			$w('#adressInput').value = customerRecord.cycle;
			$w('#suburbInput').value = customerRecord.cycle;
			$w('#stateInput').value = customerRecord.cycle;
			$w('#postcodeInput').value = customerRecord.cycle;
			$w('#countryInput').value = customerRecord.cycle;
		})
		.catch((error) => {
			// If we get here we have encountered an error so console.log() it for now
			console.log(error.message);
			});
	}
		function logMessage(message) {
    	// Note the use of conditional telling me if the argument I 
   		// received is null or not
    	console.log('logMessage('+(message?message:'null!')+')');
	}

I found two errors, so far:
line 73: console.log("No such element with name:"elementName)
line 119: duplicate let taxRate

Furthermore, I followed the first Save WixData link you mentioned (second did not work) but after reading I don’t understand what I need to do. Would you able to explain/example me?
Once again, thank you for your hard work and hopefully we can finish this project shortly!
Cheers, Raymond

Hi Raymond
So
Error #1 this is concatenation go two strings. So there needs to be a + character between the string and the string variable - change
name:ā€element
to
name:ā€+element

Error #2: if the variable taxRate has already been declared using let. The second declaration will fail because taxRate already exists. Simply delete the word ā€˜let’ in the second declaration :slight_smile:

Steve

Hi Steve, first of all, merry xmas…
I’ve fixed the first error, thanks. If I change error#2, the next line will automatically receive an error…

Would you be able to help me with that save button as described before?

[@hortiray] Try using console.log() to print out the values you are getting for the various variables being calculated. It’s likely that the taxRate substring is not returning a number that can be parsed by parseFloat.

I cannot help you debug line at a time without knowing what error you are getting. It is important that you spend time understanding the functions being called, what they take as parameters and what they return as results. Then check using console.log, or the browser debugger, what values are being given to a function, and what results are being returned.

Get to know the MDN javascript resources

as well as the Wix code resources to help you. You can also get lots of good help on the w3schools javascript website.

Cheers
Steve