//======================================================================||
//		 NOP Design JavaScript Shopping Cart		        ||
//								        ||
// For more information on SmartSystems, or how NOPDesign can help you  ||
// Please visit us on the WWW at http://www.nopdesign.com	        ||
//								        ||
// Javascript portions of this shopping cart software are available as  ||
// freeware from NOP Design.  You must keep this comment unchanged in   ||
// your code.  For more information contact FreeCart@NopDesign.com.     ||
//								        ||
// JavaScript Shop Module, V.4.4.0				        ||
// 	      	   	   					        ||
// This modified version by Eugene Reimer, Version 2007-11-23,	        ||
// has		 	    	   	   	   		        ||
//   quantity-based discount pricing;				        ||
// does		 	    	   	   	   		        ||
//   shipping-calculations by Size & Weight as well as by Zone,         ||
//   alterable for retailer-location and package-deliverer,	        ||
//   as well as for the items being offered for sale,		        ||
//   WITHOUT any programming; 	    	    			        ||
// detects situations where breaking up a parcel into several smaller   ||
//   packages lowers the shipping-cost, as is useful, for example,      ||
//   with Canada-Post's pricing;	       			        ||
// supports 2 kinds of sales-tax, with different exceptions,	        ||
//   with up to three regions of applicability by customer location,    ||
//   the two taxes can be shown on separate lines,                      ||
//   taxes can also be shown included in prices, yet itemized;          ||
//   everything that's needed in Canada, Australia, Europe, etc;        ||
// can handle any currency,      	      			        ||
//   whether the smallest actual-unit is thousandths or thousands, etc  ||
//   (although not the old British Pound-Shilling-Pence notation);      ||
// supports Checkout via:					        ||
//   PayPal,	     						        ||
//   Google-Checkout,						        ||
//   as well as the 3 methods from the original version;	        ||
// supports ONE-step checkout,	       				        ||
//   as well as the TWO-step checkout from the original version;        ||
// uses:     	    	     	      	       			        ||
//   parts of UPS-Shipping-by-Weight by Stefko, Ver1.3 (12-20-03);      ||
// for an example, see:		     		       		        ||
//   http://www.nativeorchid.org/shopping.htm;			        ||
// for information on this version,				        ||
//   see http://ereimer.net/nopercart.htm 			        ||
//   or contact  ereimer@shaw.ca.    				        ||
//     		      	   	    	     			        ||
// Copyright 2007 NopDesign.com, Stefko, Eugene Reimer;		        ||
// can be freely used, modified, copied, distributed, sold, etc,        ||
// under the terms of either the LGPL or the GPL (your choice);	        ||
// see http://www.gnu.org/licenses for the details of these terms.      ||
//     				       	   	      	    	        ||
//======================================================================||

//----------------------------------------------------------------------||
//			 Global Options				        ||
//			----------------			        ||
// Shopping Cart Options, you can modify these options to change the    ||
// the way the cart functions.					        ||
//								        ||
// Language Packs						        ||
// ==============						        ||
// You may include a language-pack along with nopercart.js in your      ||
// HTML pages to change the language.  For example:		        ||
//	<SCRIPT SRC="noper-language-fr.js"></SCRIPT>		        ||
//	<SCRIPT SRC="nopercart.js"></SCRIPT>			        ||
//								        ||
// Many different language packs are provided together with this        ||
// software. Note: I'm far from fluent in (most of) those languages.    ||
// If you construct a new one OR make improvements to one of the        ||
// supplied ones, please email the result to  ereimer@shaw.ca	        ||
// so it can be included with this software, to help other users.       ||
//								        ||
// Options For Everyone:					        ||
// =====================					        ||
// * MoneySymbol: string, the symbol which represents dollars/euro/etc  ||
//   in your locale.						        ||
// * DisplayPopupOnAdd: true/false, controls whether the user is        ||
//   provided with a popup when adding to the cart.                     ||
// * DisplayPopupOnRemove: true/false, controls whether the user is     ||
//   provided with a popup when removing from the cart.                 ||
// * DisplayChangeQty: true/false; whether user permitted to change     ||
//   Qty on the managecart page.                                        ||
// * DisplayWtColumn: true/false, controls whether the managecart       ||
//   page displays shipping-weight column.			        ||
// * DisplaySzColumn: true/false, controls whether the managecart       ||
//   page displays shipping-size column.			        ||
// * DynamicWtSzColumns: number, possible values are:                   ||
//     0 = avoid using More-Info, Less-Info buttons;                    ||
//     1 = the MoreInfo-state displays the Size-Column;		        ||
//     2 = the MoreInfo-state displays the Weight-Column;	        ||
//     3 = the MoreInfo-state displays both Weight+Size-Columns;        ||
//   the Less-Info-state always displays neither;	     	        ||
//   nonzero means there'll be either a More-Info or Less-Info button   ||
//   on the PkgAttr row & so requires that DisplayPkgAttrRow be true.   ||
// * WTUNITS: string, units for item & package weights, eg: "lb".       ||
// * SZUNITS: string, units for item & package sizes, eg "cm".          ||
// * WTROUND: number; package-weight will be rounded-up to multiple of  ||
//   1/WTROUND;  use 1 for weight in grams, perhaps 100 for pounds.     ||
// * SZROUND: number; package-length,width,height will each be rounded  ||
//   to multiple of 1/SZROUND.                                          ||
// * MoneyPLACES: number, controls rounding & display of money-amounts; ||
//   use 2 for US-dollars rounded to cents, shown as dollars and cents; ||
//   or -3 if amounts are to be rounded to multiples of 1000.           ||
// * DisplayPkgAttrRow: true/false; controls whether the managecart     ||
//   page shows the total size & weight line.                           ||
// * DisplayShippingRow: true/false, controls whether the managecart    ||
//   and checkout pages display shipping-cost line.	                ||
// * DisplayTaxRow: true/false, controls whether the managecart	        ||
//   and checkout pages display tax-cost line, or two lines depending   ||
//   the TaxNames option.                                               ||
// * DisplayTaxIncluded: true/false; if true, then item prices are      ||
//   shown with taxes included, and the tax-subtotal lines are shown    ||
//   after the Total line, with the phrase "included in total".         ||
//   Note: if you use this tax-included pricing, then consider having   ||
//   a note on your View-Cart page advising a customer who needs the    ||
//   tax details to "print this page before checking out"  (because the ||
//   statement from the payment-processor will be inadequate WRT the    ||
//   tax details).                                                      ||
// * TaxNames: array of string; use this to give names to your taxes,   ||
//   in which case DisplayTaxRow will show them on TWO lines;           ||
//   example: TaxNames = ["PST","GST"];                                 ||
// * TaxRateRegional: number, the tax-rate for a tax that applies to    ||
//   customers in Region#0.  Used when you need to charge a tax to      ||
//   those in the same state/province you're in, but not to others.     ||
// * TaxRate: number, the tax-rate for a tax that applies to customers  ||
//   in both Region#0 and Region#1;  use TaxRate=0.075 for a 7.5% tax;  ||
//   Note: when TaxRateRegional is zero, then TaxRate applies to all    ||
//   customers.                                                         ||
//   Note: Both TaxRate and TaxRateRegional will be nonzero for most    ||
//   Canadian, Australian, European retailers, at most one will be for  ||
//   US-Americans.	                                                ||
//   TAX-EXCEPTIONS: the first letter of an item's ID_NUM is used to    ||
//   indicate taxation-exceptions, as follows:     	                ||
//     "n" - non-taxable - neither tax applies to this item;	        ||
//     "p" - only the TaxRateRegional tax applies to this item;	        ||
//     "g" - only the TaxRate tax applies to this item;	  	        ||
//     OTHER - both taxes apply to this item;	  		        ||
//     (beware of ID_NUMs accidentally beginning with "p", "g", etc)    ||
//   When both tax-rates are applicable, the combined-tax is computed   ||
//   as Amount TIMES (TaxRate+TaxRateRegional).  Does anyone need       ||
//   two kinds of tax with one going on top of the other?               ||
// * (the TaxByRegion option has been discarded in this version;        ||
//   a nonzero TaxRateRegional conveys the same information.)	        ||
// * RegionFromZone: array, mapping from Zone to array of Regions legal ||
//   for that Zone, the first being the default for that Zone;          ||
//   for example, use [[0,1],[2]] if someone in your Zone#0 is in       ||
//   Region #0 or #1, someone in Zone#1 is in Region#2;                 ||
//   or use [[1,0],[2]] to make Region#1 the default for Zone#0;        ||
//   use [] to disable the validity-checking it offers.                 ||
// * RegionDefault: number, controls how sales-tax calculations are     ||
//   done when user hasn't yet indicated which Region he/she is in;     ||
//   0 means Region#0, 1 means Region#1, 2 means Region#2;              ||
//   this option is ignored when RegionFromZone is being used.          ||
// * RegionPrompt: string, with which to prompt a user who has not yet  ||
//   indicated which Region he/she is in;                               ||
//   use the empty-string to suppress such a prompt, in which case      ||
//   such user will be charged sales-tax using the defaults.            ||
// * RegionTable: replaces the TaxableRegion, NonTaxableRegion options; ||
//   example: ["Manitoba Resident", "Other Canadian", "Other Country"]; ||
//   use [TaxableRegion, NonTaxableRegion] for compatibility with the   ||
//   original NOP-Design version;  if TaxRateRegional is zero, this     ||
//   option is ignored.  if TaxRateRegional is nonzero, this array can  ||
//   have either 2 or 3 entries: use 2 regions if your TaxRate applies  ||
//   to all customers; use 3 regions when you need a third region that  ||
//   is excempt from both taxes.                                        ||
// * MinimumDonation: number, the minimum money-amount you're willing   ||
//   to accept as a donation;  example: a Canadian Charity using PayPal ||
//   needs a 3.50 minimum to ensure compliance with the 80%-rule.       ||
// * MinimumDonationPrompt: string, with which to prompt donor not      ||
//   meeting the minimum-donation amount.                               ||
// * MinimumOrder: number, the minimum money-amount that must be        ||
//   purchased before a user is allowed to checkout.  Set it to 0.01    ||
//   to disable this minimum, and yet prevent a completely empty cart   ||
//   being sent to your payment-processing.                             ||
// * MinimumOrderPrompt: string, with which to prompt user who hasn't   ||
//   met the minimum-order amount.				        ||
// * gcCurrency: string, currency-code to be passed to Google-Checkout  ||
//   for each item;  ignored when PaymentProcessor not "gc".            ||
//								        ||
// Payment Processor Options:					        ||
// ==========================					        ||
// * PaymentProcessor: string, a short code for your payment-processor  ||
//   gateway.  Set this option to one of:   	     		        ||
//     "an"  (Authorize.net)					        ||
//     "wp"  (Worldpay)						        ||
//     "lp"  (LinkPoint)					        ||
//     "pp"  (PayPal)						        ||
//     "gc"  (GoogleCheckout)					        ||
//     ""    (custom HTML checkout-page)			        ||
//     "cgi" (custom CGI/PHP/ASP checkout-script)		        ||
//   When using "cgi", please review your OutputItem settings,          ||
//   explained below under Options for Programmers.                     ||
//								        ||
// * PaymentProcessor2: string, taking the same values as shown for     ||
//   PaymentProcessor, but applies to CheckoutCart -- for those	        ||
//   wanting a two-step checkout-process.			        ||
//   (TWO-Step-Checkout involves first an HTML checkout-page; and       ||
//   that page invokes CheckoutCart.)				        ||
// 			      	   				        ||
// Options to Control the Shipping-Calculations:		        ||
// =============================================		        ||
// * ShipTable: array, explained below.				        ||
// * PackTable: array, explained below.				        ||
// * SHIPPINGCO: string, abbreviated name of package-deliverer.	        ||
// * ZoneDefault: integer,  default entry-number in ShipTable;	        ||
//   usually set to the most expensive case, so that a careless	        ||
//   customer will pay at least enough.	     	       		        ||
// * ZonePrompt: string, with which to prompt a user who has not yet    ||
//   indicated which Shipping-Zone he/she is in; use the empty-string   ||
//   to suppress such a prompt, in which case such user will be         ||
//   charged shipping according to ZoneDefault.	   	     	        ||
// * ShipTaxRate: number, the tax-rate to be applied to shipping prices ||
//   in ShipTable; use 0.06 if Canadian, 0.0 if US-American, or         ||
//   whatever "VAT" applies to your shipping price.                     ||
// * ShipTaxName: string, for showing the tax included in the shipping  ||
//   amount;  use empty-string to not have that tax shown.              ||
// * HandlingChargePerOrder: amount, handling-charge for the first      ||
//   package in an order.    	     		     	     	        ||
// * HandlingChargePerExtraPackage: amount, handling-charge for each    ||
//   additional package in an order;  use a huge number if you much     ||
//   prefer to ship one order as a single package.	       	        ||
//								        ||
// Options For Programmers:					        ||
// ========================					        ||
// * OutputItem<..>: string, the name of the pair value passed at       ||
//   checkouttime.  Change these only if you are connecting to a CGI    ||
//   script and need other field names, or are using a secure service   ||
//   that requires specific field names.			        ||
//   NOTE: used only when PaymentProcessorX=="cgi".                     ||
// * AppendItemNumToOutput: true/false, if set to true, the number of   ||
//   each ordered item will be appended to the output string.  For      ||
//   example if OutputItemId is 'ID_' and this is set to true, the      ||
//   output field name will be 'ID_1', 'ID_2' ... for each item.        ||
//   When in doubt use true,  This option must be true for at least     ||
//   PaymentProcessorX=="pp" ||"gc", likely others, perhaps all others? ||
// * Debug: integer, 255 for maximum debug-info, 256 for minimal,       ||
//   zero for none.  (the integer is a bitstring.)                      ||
//   NOTE: using Debug=256 after modifying the options is recommended   ||
//   for everyone, not just programmers.  However, Debug=0 should be    ||
//   used in production.                                                ||
//								        ||
// (The former HiddenFieldsToCheckout option has been discarded in      ||
// this version, replaced by PaymentProcessorX=="cgi".)     	        ||
//----------------------------------------------------------------------||

//------------------------------------------------------------------------
// Language Strings
// ----------------
// These strings will be used if you have not included a language-pack.
// If using English, simply modify the strings below;
// if using another language, then modify them in the file 
// noper-language-XX.js -- where XX is the language you are using.
// (File noper-language-en.js has been discarded in this version.)
//
// If you construct a new one OR make improvements to one of the 
// supplied ones, please email the result to  ereimer@shaw.ca
// so it can be included with this software, to help other users.
//------------------------------------------------------------------------
if(typeof strSorry == "undefined"){						//ER: make language-pack optional; was: if(!bLanguageDefined){
   strSorry = "I'm Sorry, your cart is full;  please proceed to checkout.";
   strAdded = "Added to your shopping cart:";					//ER: minor punctuation changes
   strAddedQuantity = "Quantity: ";						//ER: new;  needed by, but not added by, Stefko mods
   strAddedProduct  = "Product:  ";						//ER: new;  needed by, but not added by, Stefko mods
   strRemove = "Click 'OK' to remove this product from your shopping cart.";
   strILabel = "Product ID &nbsp; &nbsp; ";					//ER: added trailing whitespace
   strDLabel = "Product Name";							//ER: Name/Description->Name(?)
   strQLabel = "Qty";								//Quantity->Qty by Stefko
   strPLabel = "Price";
   strWLabel = "Weight";							//ER: new - for displaying Product WEIGHT in ManageCart;  replaces strSLabel
   strZLabel = "Size";								//ER: new - for displaying Product SIZE in ManageCart
   strALabel = "Amount";							//ER: new - replaces Stefko's "Ext." for displaying Price*Qty in CheckoutCart
   strRLabel = "Remove From Cart";					//ER: was: "Remove from Cart"  (Netscape-4 needs NBSP rather than EMPTY)
   strRButton= "Remove";						    //ER: was: "Remove"
   strMButton= "More Info";							//ER: new;  for the DynamicWtSzColumns option
   strLButton= "Less Info";							//ER: new;  for the DynamicWtSzColumns option
   strSUB = "SUBTOTAL";
   strWTSZTOT = "PACKAGE ATTRIBUTES";						//ER: new;  Stefko had strWTOT="TOTAL WEIGHT";  but now it's for Weight+Size
   strSHIP = "SHIPPING";
   strTAX = "TAX";
   strTOT = "TOTAL";
   strErrQty = "Invalid Quantity.";
   strNewQty = "Please enter new quantity:";
   strSHIPPINGZONE = "SHIPPING<BR>ZONE";					//ER: new;  needed by, but not added by, Stefko mods
   strTAXABLEREGION = "TAXABLE<BR>REGION";					//ER: new;  needed by my rewrite of the sales-tax-by-region code
   strEA = "/ea";								//ER: new;  needed by the original NopDesign version
   strCartEmpty = "Your cart is empty";						//ER: new;  needed by the original NopDesign version
   strAsMultiple = "as multiple packages:";					//ER: new;  for message from ComputeShipping
   strAsSingle = "as-one:";							//ER: new;  for message from ComputeShipping
   strBroken="our shipping-calculator is broken; please inform our webmaster";	//ER: new;  message from ComputeShipping
   strTotalNaN="Your browser's javascript appears to be broken; another browser may help; a reboot may help; if problem persists, please inform our webmaster";  //ER: new;  message from ValidateCart, when total is not a number
   strINCLUDEDINTOTAL = "Included in Total";					//ER: new;  for the DisplayTaxIncluded option
   Language = "nl";
}

//Options for Programmers:
OutputItemId	     = "ID_";
OutputItemQuantity   = "QUANTITY_";
OutputItemPrice	     = "PRICE_";
OutputItemName	     = "NAME_";
OutputItemWeight     = "WEIGHT_";	//Renamed by Stefko: Shipping-->Weight
OutputItemLength     = "LENGTH_";	//ER: new
OutputItemWidth	     = "WIDTH_";	//ER: new
OutputItemHeight     = "HEIGHT_";	//ER: new
OutputItemAddtlInfo  = "ADDTLINFO_";
OutputOrderZone	     = "SHIPZONE";	//Added by Stefko
OutputOrderRegion    = "TAXREGION";	//ER: new
OutputOrderSubtotal  = "SUBTOTAL";
OutputOrderShipping  = "SHIPPING";
OutputOrderTax	     = "TAX";
OutputOrderTotal     = "TOTAL";
AppendItemNumToOutput= true;		//MUST be true for at least PayPal & Google-Checkout;  ER: suspect no-one ever uses false??
Debug                = 0;		//ER: new;  any nonzero value gets the DEBUG sanity-test alerts; the 1-bit gets DEBUG1 alerts, the 2-bit gets DEBUG2, etc
function DEBUG(str)   {if(Debug)   alert(str);}
function DEBUG1(str)  {if(Debug&1) alert(str);}
function DEBUG2(str)  {if(Debug&2) alert(str);}
function DEBUG4(str)  {if(Debug&4) alert(str);}
function DEBUG8(str)  {if(Debug&8) alert(str);}
function DEBUG16(str) {if(Debug&16)alert(str);}

//Options for Everyone:
MoneySymbol	     = "€";
DisplayPopupOnAdd    = true;		//suppress "add to cart" popups;  DEFAULT true in nopdesign version
DisplayPopupOnRemove = true;		//suppress "remove from cart" popups;  not suppressable in nopdesign version
DisplayChangeQty     = true;		//Added by Stefko
DisplayWtColumn	     = true;		//don't show a Shipping-WEIGHT column in ManageCart;  replaced DisplayShippingColumn
DisplaySzColumn	     = true;		//don't show a Shipping-SIZE column in ManageCart
DynamicWtSzColumns   = 3;		//allow user to click on "More Info" button, to get both Shipping-WEIGHT+SIZE columns shown
WTUNITS	             = "gr";		//needed by, but not added by, Stefko mods (he used "lbs")
SZUNITS	             = "cm";		//units for Item & Package SIZEs
WTROUND		     = 100;		//Package-Weight will be rounded-up to multiple of 1/WTROUND;	 Use 1 for weight in grams, perhaps 100 if using pounds or kg
SZROUND		     = 100;		//Package-Length,Width,Height will each be rounded to multiple of 1/SZROUND;
MoneyPLACES	     = 2;		//currency rounding & display;  2 for two decimal places as in dollars and cents
DisplayPkgAttrRow    = true;		//show the total size & weight
DisplayShippingRow   = true;		//show the shipping-cost-line
DisplayTaxRow	     = true;		//omit the tax-line
DisplayTaxIncluded   = false;		//do not show tax-included prices
TaxNames             = ["BTW"];	//names for the two sales-taxes in Canada
TaxRate		         = 0;		//no universally-charged sales-tax
TaxRateRegional	     = 0;		//no within-province-only sales-tax
RegionTable          = ["Nederland", "België", "Other Country"];
RegionFromZone       = [[0,1],[2],[2]];	//customer in Zone#0 can be in Region#0 or #1;  one in Zone #1 or #2 can only be in Region#2
RegionDefault        = 0;		//default to Region#0;
RegionPrompt	     = "Please indicate whether you are a resident of Manitoba for tax purposes, before continuing";
DefaultDonation      = 25;		//default Donation-Amount, for donor who provides an invalid amount
MinimumDonation      = 0.00;		//minimum Donation-Amount, to comply with Eighty-Percent rule for Canadian charities, given the present PayPal fee-structure
MinimumDonationPrompt= "We're sorry but we're unable to accept a donation of less than 3.50 via PayPal; (we accept arbitrarily small donations via CanadaHelps)";
MinimumOrder	     = 0.01;
MinimumOrderPrompt   = "'Oops!\n\nWe kunnen je bestelling niet afronden omdat\ner nog niets op je boodschappenlijstje is geplaatst.";
gcCurrency           = "EUR";		//currency-code for Google-Checkout    
//--ER: UNYANK (some of) the following to test various options, Tax-calculations, etc:					DEMO ONLY
//MoneyPLACES	     = -1;			//test currency needing rounding to multiple of ten			DEMO ONLY
  DisplayTaxIncluded = false;			//test tax-included pricing						DEMO ONLY
  DisplayTaxRow      = false;			//show the tax-line							DEMO ONLY
  TaxRate            = 00;			//charge 6% sales-tax to those in Regions #0 and #1 (eg: Canadian GST)	DEMO ONLY
  TaxRateRegional    = 0;			//charge 7% provincial-sales-tax to residents of Region#0		DEMO ONLY
  Debug              = 256;			//use 256 for minimal DEBUG-alert output				DEMO ONLY
  DefaultDonation    = 0.01;			//default Donation-Amount (tiny donations useful for testing)		DEMO ONLY
  MinimumDonation    = 0.01;			//minimum Donation-Amount (tiny donations useful for testing)		DEMO ONLY

//Payment Processor Options:
PaymentProcessor     = "pp";		//ER: for PayPal as the payment-processor in a one-step checkout
PaymentProcessor2    = "cgi";		//ER: for a CGI/PHP/ASP script as the 2nd-step of a two-step checkout-process

//========================================================================
// Shipping-Info Table
// -------------------
// Two examples are offered below, to illustrate how you can go about 
// creating a table that describes shipping from your location, by the 
// package-deliverer you've chosen.  
// The first example is for a Canadian retailer using Canada-Post;
// the 2nd example, based on Stefko, is for a US retailer using UPS.
//
// (You'll need minor changes even if you are a Canadian retailer
// if your location is other than Winnipeg; similarly for US+UPS,
// if your location is other than Wichita.)
//
// The Canada-Post example shows Weight & Size based shipping,
// using Weight in grams, sizes in centimetres;
// the UPS example is Weight-only, in pounds.
//
// RESTRICTION: in the present version, the Length & Width must be the
// same in all pkginfo-entries for one zone.
//========================================================================
//
//---------------------------------------------------------------------
//---EXAMPLE-1: Table for Canadian-retailer who ships by Canada-Post---
//---------------------------------------------------------------------
ShipTable = [];
ShipTable[0]= new ShipEntry("Nederland",	[]);
ShipTable[1]= new ShipEntry("België",		[]);
//ShipTable[2]= new ShipEntry("To International", []);
//package-categories within Nederland:
ShipTable[0].pkginfo[0]=new PkgClass( 100.0, new Size(38.0, 26.5, 3.2), 1.38, 0.00, 1,  "");	//OSL up to 100g, size 33.6x23.4x1.8cm, costs 1.10 flat-rate (=vast-tarief)
ShipTable[0].pkginfo[1]=new PkgClass( 250.0, new Size(38.0, 26.5, 3.2), 1.84, 0.00, 1,  "");	//OSL up to 200g, size 33.6x23.4x1.8cm, costs 1.86 flat-rate (=vast-tarief)
ShipTable[0].pkginfo[2]=new PkgClass( 500.0, new Size(38.0, 26.5, 3.2), 2.30, 0.00, 1, "");	//OSL up to 500g, size 33.6x23.4x1.8cm, costs 2.55 flat-rate (=vast-tarief)
ShipTable[0].pkginfo[3]=new PkgClass( 2000.0, new Size(38.0, 26.5, 3.2), 2.76, 0.00, 1, "");	//OSL up to 500g, size 33.6x23.4x1.8cm, costs 2.55 flat-rate (=vast-tarief)
ShipTable[0].pkginfo[4]=new PkgClass( 100.0, new Size(140.0, 78.0,  58.0), 6.75, 0.00, 1, "");	//OSL up to 500g, size 33.6x23.4x1.8cm, costs 2.55 flat-rate (=vast-tarief)
ShipTable[0].pkginfo[5]=new PkgClass(10000.0, new Size(140.0, 78.0,  58.0),6.75, 0.00, 1, "");	//P parcel costs 10.11 + 0.689 per-500g;  often needs splitting-up
ShipTable[0].pkginfo[6]=new PkgClass(999999, new Size(38.0, 26.5,999.9),10.00, 0.00,500,"");
//package-categories for belgië shipping:
ShipTable[1].pkginfo[0]=new PkgClass( 100.0, new Size(38.0, 26.5, 3.2), 1.76, 0.00, 1,  "");	//OSL 100g costs 1.86 flat-rate (=vast-tarief) (OSL/LSP)
ShipTable[1].pkginfo[1]=new PkgClass( 250.0, new Size(38.0, 26.5, 3.2), 2.45, 0.00, 1,  "");	//OSL 200g costs 3.10 flat-rate (=vast-tarief)
ShipTable[1].pkginfo[2]=new PkgClass( 500.0, new Size(38.0, 26.5, 3.2), 4.02, 0.00, 1,  "");	//LSP 250g costs 3.86 flat-rate (=vast-tarief)
ShipTable[1].pkginfo[3]=new PkgClass(1000.0, new Size(38.0, 26.5, 3.2), 6.47, 0.00, 1,  "");	//OSL 500g costs 6.20 flat-rate (=vast-tarief) (OSL/LSP)
ShipTable[1].pkginfo[4]=new PkgClass(2000.0, new Size(38.0, 26.5, 3.2), 8.04, 0.00, 1,  "");	//SP 1kg costs 10.30 flat-rate (=vast-tarief)
ShipTable[1].pkginfo[5]=new PkgClass( 250.0, new Size(60.0, 60.0, 90.0), 3.25, 0.00, 1,  "");	//OSL 200g costs 3.10 flat-rate (=vast-tarief)
ShipTable[1].pkginfo[6]=new PkgClass( 500.0, new Size(60.0, 60.0, 90.0), 4.25, 0.00, 1,  "");	//LSP 250g costs 3.86 flat-rate (=vast-tarief)
ShipTable[1].pkginfo[7]=new PkgClass(2000.0, new Size(60.0, 60.0, 90.0), 8.00, 0.00, 1,  "");	//SP 1kg costs 10.30 flat-rate (=vast-tarief)
ShipTable[1].pkginfo[8]=new PkgClass(5000.0, new Size(60.0, 60.0, 90.0), 16.45, 0.00, 1,  "");	//OSL 500g costs 6.20 flat-rate (=vast-tarief) (OSL/LSP)
ShipTable[1].pkginfo[9]=new PkgClass(10000.0, new Size(60.0, 60.0, 90.0),20.95, 0.00, 1, "");	//SP 1kg costs 10.30 flat-rate (=vast-tarief)
ShipTable[1].pkginfo[10]=new PkgClass(999999, new Size(60.0, 60.0,999.9),25.00, 0.00,500,"");	//P costs 13.42 + 1.517/500g;  rarely needs splitting-up
//package-categories for to-International shipping:
//ShipTable[2].pkginfo[0]=new PkgClass( 100.0, new Size(33.6, 23.4,  1.8), 3.60, 0.00, 1,  "");	//OSL 100g flat-rate (OSL/LSP)
//ShipTable[2].pkginfo[1]=new PkgClass( 200.0, new Size(33.6, 23.4,  1.8), 6.20, 0.00, 1,  "");	//OSL 200g flat-rate
//ShipTable[2].pkginfo[2]=new PkgClass( 250.0, new Size(33.6, 23.4,  1.8), 7.29, 0.00, 1,  "");	//LSP 250g flat-rate
//ShipTable[2].pkginfo[3]=new PkgClass( 500.0, new Size(33.6, 23.4,  1.8),12.40, 0.00, 1,  "");	//LSP 500g flat-rate (LSP-Surface: 7.50;  OSL/LSP-Air: 12.40)
//ShipTable[2].pkginfo[4]=new PkgClass(1000.0, new Size(33.6, 23.4, 33.0),12.60, 0.00, 1,  "");	//SP 1kg flat-rate
//ShipTable[2].pkginfo[5]=new PkgClass(2000.0, new Size(33.6, 23.4, 33.0),18.50, 0.00, 1, "*");	//SP 2kg flat-rate
//ShipTable[2].pkginfo[6]=new PkgClass(999999, new Size(33.6, 23.4,999.9),28.08, 3.662,500,"");	//P costs 28.08 + 3.662/500g;  often needs splitting-up
SHIPPINGCO =  "TNT";			//abbreviated CanadaPost as our package-deliverer, used in messages
ZoneDefault = 0;			//use N to make entry# N be the Default; ie: 0 for the first, 1 for the 2nd, etc
ZonePrompt  = "";			//do not prompt for no-zone-selected, but simply use ZoneDefault
ShipTaxRate = 0;			//using 0.06 to indicate that Canadian GST must be added to shipping-prices quoted by CanadaPost
ShipTaxName = true;			//don't show tax included in shipping;  use "TAX-GST Included in Shipping" to show it
HandlingChargePerOrder =	0.00;	//HandlingCharge for the first package in an order
HandlingChargePerExtraPackage = 0.00;	//HandlingCharge per additional package
//--ER: UNYANK (some of) the following to test various options:								DEMO ONLY
  ShipTaxName="TAX-GST Included in Shipping";				//show tax included in shipping-charge		DEMO ONLY
  ZonePrompt="Kies uw verzend regio om verder te gaan";	//prompt for Zone				DEMO ONLY
//
// NOTES:
// ------
// 1. Canada-Post's size-limit for Oversized-Letter (OSL) is 38x27x2cm, however the closest (readily-obtainable) padded envelope, 14.5x9.5 inches,
// offers usable inside dimensions of 33.6x23.4x1.8 cm,	 so that's how the limit is expressed above.
//
// 2. Canada-Post's size-limit for Small-Packet (SP) is Length<=60cm AND Length+Width+Height<=90cm;
// that limit is expressed above as 33.6x23.4x33.0 (which sums to 90cm) in order to simplify the package-size calculations;
// (for the item-sizes in the PackingRule-example below, could do very slightly better by expressing it as 32x23x35, and making all pkgs 32x23).
//
// 3. For its OSL, LSP, SP categories, Canada-Post prices are exactly 3-way (within-Canada, to-USA, to-International) as shown above;
// however, for PARCELS (P) their rate varies by City/Province within Canada, by State for to-USA, and by Country/Region for International;
// the Parcel-rates shown above are: Winnipeg-to-Halifax used for within-Canada, to-Florida for USA, and to-Australia for International.
// Parcel prices consist of base-rate plus fuel-surcharge; base-rates are revised once per year at most; but fuel-surcharges vary slightly from week to week;
// the prices shown were last checked on 2007-July15.
// Parcels are priced in 0.5kg ranges; i.e. weight gets rounded-up to multiple of 500g.
//
// 4. "*" (flag) in last column indicates multiple of these (or smaller) may cost less than shipping as one package;
// Some EXAMPLES where SPLITTING is beneficial (with 2007 Canada-Post rates) are:
// within-Canada as FIVE 500g OSL's rather than one 2.5kg parcel (FIVE 500g OSL's even wins over a 2.0kg parcel);
// to-International as FIVE 2kg small-packets rather than one 10kg parcel;
// the to-USA pricing is much closer to being sensible,  however sending 1kg SP + 500g OSL costs less than one 1.5kg parcel.
// !!possibly BETTER for program to derive the "*" info itself, since it may be non-intuitive?
//
//----------------------------------------------------------------------------
//---EXAMPLE-2: Table for Wichita-retailer using UPS, from Stefko's program---
//              (to use it, remove the leading "//" from each line)
//----------------------------------------------------------------------------
//ShipTable = [];
//ShipTable[0] = new ShipEntry("Zone 1 - Wichita KS",						       []);
//ShipTable[1] = new ShipEntry("Zone 2 - Tulsa OK",						       []);
//ShipTable[2] = new ShipEntry("Zone 3 - Ft.Smith AR, Lincoln/Omaha NE, Okla.City/Norman/Marietta OK", []);
//ShipTable[3] = new ShipEntry("Zone 4 -",							       []);
//ShipTable[4] = new ShipEntry("Zone 5 -",							       []);
//ShipTable[5] = new ShipEntry("Zone 6 - All Florida Locations",				       []);
//ShipTable[6] = new ShipEntry("Zone 7 -",							       []);
//ShipTable[7] = new ShipEntry("Zone G",							       []);
//SHIPPINGCO =  "UPS";			//name UPS as the package-deliverer
//ZoneDefault =  7;
//ZonePrompt  = "";			//do not prompt for no-zone-selected, but simply use ZoneDefault
//ShipTaxRate =  0.00;			//use 0.0 as the multiplier, no added-tax in the USA
//ShipTaxName = "";			//don't show tax included in shipping
//HandlingChargePerOrder =	 0.00;	//no HandlingCharge for the first package in an order
//HandlingChargePerExtraPackage= 0.00;	//no HandlingCharge per additional package
//nullsize= new Size(0,0,0);
////--entry#0 Wichita
//ShipTable[0].pkginfo[0]=new PkgClass( 500, nullsize,  0.00, 0.00, 1,  "");	//up to 500lb is free  (surprisingly generous of UPS:-)
////--entry#1 Tulsa
//ShipTable[1].pkginfo[0]=new PkgClass( 500, nullsize,  8.19, 0.20, 1,  "");	//up to 500lb is  8.19 + 0.20/lb
////--entry#2 Ft-Smith etc
//ShipTable[2].pkginfo[0]=new PkgClass(  70, nullsize,  8.35, 0.15, 1,  "");	//up to  70lb is  8.35 + 0.15/lb
//ShipTable[2].pkginfo[1]=new PkgClass(  74, nullsize, 11.82, 0.20, 1,  "");	//up to  74lb is 11.82 + 0.20/lb
//ShipTable[2].pkginfo[2]=new PkgClass(  79, nullsize, 13.82, 0.20, 1,  "");	//up to  79lb is 13.82 + 0.20/lb
//ShipTable[2].pkginfo[3]=new PkgClass( 500, nullsize, 19.70, 0.30, 1,  "");	//up to 500lb is 19.70 + 0.30/lb
////--entry#3
//ShipTable[3].pkginfo[0]=new PkgClass( 500, nullsize,  8.49, 0.25, 1,  "");	//up to 500lb is  8.49 + 0.25/lb
////--entry#4
//ShipTable[4].pkginfo[0]=new PkgClass( 500, nullsize,  8.52, 0.30, 1,  "");	//up to 500lb is  8.52 + 0.30/lb
////--entry#5 Florida
//ShipTable[5].pkginfo[0]=new PkgClass(  77, nullsize,  8.67, 0.35, 1,  "");	//up to  77lb is  8.67 + 0.35/lb
//ShipTable[5].pkginfo[1]=new PkgClass(  80, nullsize, 11.67, 0.35, 1,  "");	//up to  80lb is 11.67 + 0.35/lb
//ShipTable[5].pkginfo[2]=new PkgClass(  85, nullsize, 13.67, 0.35, 1,  "");	//up to  85lb is 13.67 + 0.35/lb
//ShipTable[5].pkginfo[3]=new PkgClass( 500, nullsize, 14.60, 0.40, 1,  "");	//up to 500lb is 14.60 + 0.40/lb
////--entry#6
//ShipTable[6].pkginfo[0]=new PkgClass( 500, nullsize,  8.57, 0.50, 1,  "");	//up to 500lb is  8.57 + 0.50/lb
////--entry#7 Zone-G
//ShipTable[7].pkginfo[0]=new PkgClass( 500, nullsize,  8.53, 0.65, 1,  "");	//up to 500lb is  8.53 + 0.65/lb


//========================================================================
// Packing-Rule Info (OPTIONAL)
// ----------------------------
// The program will work without this Packing-Rule info, but will then
// be more prone to overcharging the customer for shipping.
// You may want to pre-compute packing rules like these, which will be
// feasible provided your list of item-sizes is fairly short.
// (And if its really short, you probably won't need them:-)
//
// The EXAMPLE is for a site selling items of 4 sizes, as shown below
// (itmbk, itmdv, itmlg, itmsm).  You will need to adapt it for your
// item-sizes OR simply omit it.
//========================================================================
PackTable = [];			//--LEAVE THIS LINE even when omitting Packing-Rule Info
//--OPTIONAL INFO - the rest of this section may be omitted---
itmca= new Size(32.0, 23.00, 0.3);	//a calendar, 32x23x0.3cm                      weight:101g  (07oct18:new, 07oct29:80g->95g, 07nov15:95g->101g)
itmbk= new Size(23.0, 15.00, 1.2);	//a book, 23x15x1.2cm (or 8.5x5.5x0.5 inches)  weight:333.333g
itmdv= new Size(15.0, 13.00, 0.6);	//a DVD in a jewel-case			       weight:50g
itmlg= new Size(11.5,  8.00, 1.8);	//3rd item-size				       weight:16g
itmsm= new Size( 8.0,  5.75, 1.8);	//4th item-size, however N of these will be simplified to N/2 (rounded up) of the preceding
pkg2=  new Size(23.0, 16.00, 1.8);	//some rules yield this package-size;  2 of these will become a PKG2
PKG1=  new Size(32.0, 23.00, 1.2);	//two itmbks fit into one of these, and it can go as a CanadaPost Oversized-Letter
PKG2=  new Size(32.0, 23.00, 1.8);	//two pkg2's fit into one of these, and it can go as a CanadaPost Oversized-Letter

//Packing-Rules to make Size-Minimized packages that fit within CanadaPost Oversized-Letter (OSL) limits  (these are cute but NOT NEEDED):
packBySz = [];
packBySz[0]= new PackingRule([itmsm], [2],	    itmlg);	//means: 2 itmsm count as one itmlg;  subsequent rules only need 3 item-sizes
packBySz[1]= new PackingRule([itmbk,itmdv], [1, 1], pkg2);	//means: 1 itmbk + 1 itmdv fit into a 23x16x1.8 package (actual 23x15x1.8)
packBySz[2]= new PackingRule([itmdv,itmlg], [3, 1], pkg2);	//means: 3 itmdv + 1 itmlg fit into a 23x16x1.8 package (actual 21x15x1.8)
packBySz[3]= new PackingRule([itmlg], [4],	    pkg2);	//means: 4 itmlg	   fit into a 23x16x1.8 package
packBySz[4]= new PackingRule([itmbk], [2],	    PKG1);	//means: 2 itmbk	   fit into one PKG1  (two 23x16x1.2 pkgs become one 32x16x1.2)
packBySz[5]= new PackingRule([pkg2],  [2],	    PKG2);	//means: 2 pkg2		   fit into one PKG2  (two 23x16x1.8 pkgs become one 32x16x1.8)
packBySz[6]= new PackingRule([itmbk,pkg2], [1,1],   PKG2);	//means: 1 itmbk + 1 pkg2  fit into one PKG2  (two 23x16x1.2/1.8  become one 32x16x1.8)

//Packing-Rules to make 500g Weight-limited PKG2's:
packByWt = [];
packByWt[0]= new PackingRule([itmsm], [2],		     itmlg);
packByWt[1]= new PackingRule([itmbk,itmdv,itmlg], [1, 3, 1], PKG2);	//size-limit permits [1, 4, 1] but that's too heavy (gt 500g);  ditto for [2, 2, 0]
packByWt[2]= new PackingRule([itmbk,itmdv,itmlg], [1, 1, 4], PKG2);
packByWt[3]= new PackingRule([itmdv,itmlg], [6, 2],	     PKG2);
packByWt[4]= new PackingRule([itmdv,itmlg], [3, 5],	     PKG2);
packByWt[5]= new PackingRule([itmlg], [8],		     PKG2);
packByWt[6]= new PackingRule([itmbk,itmdv,itmca], [1, 1, 1], PKG2);	//weight prevents more...  (itmca rules added 07oct18)
packByWt[7]= new PackingRule([      itmdv,itmca], [   4, 2], PKG2);
packByWt[8]= new PackingRule([      itmdv,itmca], [   2, 3], PKG2);	//07nov15: 2+4->2+3
packByWt[8]= new PackingRule([      itmdv,itmca], [   1, 4], PKG2);	//07oct29: 0+6->0+5;  07nov15: 0+5->1+4  (was a rule on itmca only)
//ckByWt[10]=new PackingRule([itmbk,      itmca], [1,    1], PKG2);	//weight prevents more...  (07oct29:1+2->1+1 & yanked as rule now useless)

PackTable[0] = packByWt;	//Within-Canada:     use Weight-limited packing
PackTable[1] = packByWt;	//To-USA:	     use Weight-limited packing -- was packBySz for Size-minimized packing
PackTable[2] = packByWt;	//To-International:  use Weight-limited packing -- was packBySz for Size-minimized packing
// NOTE: I wrote the packBySz rules first, and have so far been unable to discard them;  they are NOT being used;  also have NOT been updated for itmca (calendar);
// Can use the packByWt rules everywhere, since, although this will sometimes make the single-package size bigger than optimal, that can only happen 
//   for an order of predominantly high-density items;  ergo, such a non-minimal package-size will never force a higher price;
//   and that likely holds for any user, any pkg-deliverer, or at least close enough...
// Conclusion: scrap the idea of ComputePackageSize routine doing the computation twice;  new user need only supply one set of rules (at most).
//!!FUTURE ENHANCEMENT:  supply a script that reads a given set of HTML files, extracting items for sale, and then computes the PackingRules...
//Note: our calendars demonstrate a flaw in the "shipping-weight" approach:  each calendar weighs 95g, however the weight of one plus packaging is just over 100g;
//	similarly 2 plus packaging is just over 200g;  however 5 plus packaging is just UNDER 500g;
//	without a modification for packaging-weight, the only answer is to overcharge (on shipping) for an order of 5 calendars.


//======================================================================||
//----------------------------------------------------------------------||
// YOU DO NOT NEED TO MAKE ANY MODIFICATIONS BELOW THIS LINE            ||
// If you wish to venture below this line and are new to javascript,    ||
// then I recommend:  http://www.crockford.com/javascript/survey.html;  ||
// (my code would be better had I read it first:-)                      ||
//----------------------------------------------------------------------||
//======================================================================||


//========================================================================
// Objects and Methods related to Package-Size;
// by Eugene Reimer 2007-July01.
// This is my first attempt at Javascript programming...
//
// Some notes to myself:
// -- don't really need constructors;  eg: ShipEntry(A,B) --> {zone:A, pkginfo:B}
// -- possibly avoid size-field & doubly-dotted-selectors??  the SizeXX functions can accept any object having L,W,H fields...
// -- could rewrite ShipTable & packBySz initializers using ARRAYOBJECT.push
//    also ARRAYOBJECT[ARRAYOBJECT.length]=xxx;	 -->  ARRAYOBJECT.push(xxx);
// -- was tempted to use for(VAR in ARRAYOBJECT) iteration, but it's illegal/ill-advised for arrays (only for objects);  also doesn't do what's wanted...
// -- wondered whether it's possible to redefine/overload the == operator instead of my SizeEQ routine;  found some info in:
//    http://www.mozilla.org/js/language/js20-2000-07/libraries/operator-overloading.html  -- doesn't sound promising...
// -- oldest supported browsers:  Netscape-4.06 and IE6 supported Javascript-1.3 / ECMA-262,  which has push,unshift,split,  but not regexprs!
//
// To Minify for use on selling sites, with a comment about where to find a readable version:
//    jsminify <nopercart-readable.js >nopercart.js "(c) NOP-Design...Eugene Reimer."  "Readable version at http://ereimer.net/nopercart.htm"
// (jsminify is available from  http://ereimer.net/jsminify  or use jsmin from  http://www.crockford.com/javascript/jsmin.html)
//
// Future enhancements:
// May use parts of the Bin-Packing algorithm by Martello, Pisinger, Vigo,
// as published in http://www.diku.dk/~pisinger/3dbpp.c
// (see also http://forums.devshed.com/software-design-43/3d-bin-packing-49217.html);
//========================================================================
var PkgQueue = null;					//an array of Qszwt entries;  describes the items (later the packages) in the current order
var PkgAsOne = null;					//of type Qszwt;  the resulting package size & weight
var sComputeShippingNote="";				//an additional note about shipping to be shown to the customer during View-Cart
var gVat=0;						//amount of VAT-tax included in the shipping-cost
function substr(X,A,B) {var s=""+X; return s.substring(A,B);}
function NumberZ(s) {var N=Number(s); if(isNaN(N))N=0; return(N);}	//like the javascript Number, except it returns zero instead of NaN
function Integer(s) {return Math.round(NumberZ(s));}
MoneyROUND_FRA= Math.pow(10, +MoneyPLACES);		//replaces 100 in all money-rounding  when PLACES>0
MoneyROUND_NOF= Math.pow(10, -MoneyPLACES);		//for reciprocal-based money-rounding when PLACES<=0
function CentsFRA(f) {return  Math.round(f*MoneyROUND_FRA) / MoneyROUND_FRA;}
function CentsNOF(f) {return  Math.round(f/MoneyROUND_NOF) * MoneyROUND_NOF;}
Cents = (MoneyPLACES>0 ?CentsFRA :CentsNOF);		//init function according to which rounding-method is needed
function Element(E,S) {for(var e=S.length;e--;)if(E==S[e])return true; return false;}	//is-an-element-of for set as an array; may switch to bitstring...
//function Element(E,S) {return (S&(1<<E))!=0;}						//is-an-element-of for bitstring  (set with elements 0..31)
//--sanity-checking and initialization related to Options:
while(PackTable.length < ShipTable.length) PackTable.push([]);										//ensure an entry for each Zone
if(RegionFromZone.length && RegionFromZone.length < ShipTable.length) DEBUG("RegionFromZone must have as many entries as ShipTable");
X=[];for(Z=ShipTable.length;Z--;)X[Z]=false;for(Z=RegionFromZone.length; Z--;)X[Z]=(RegionFromZone[Z].length==1);  RegionFromZoneOvA=X;	//array of Overrides booleans
X=0; Z=RegionFromZone.length; if(Z>0) {X=1; while(Z--)X&=RegionFromZoneOvA[Z];}  RegionFromZoneOverrides=X; 				//was an option, now derived
if(RegionFromZone.length) {X=[];for(R=0;R<RegionTable.length;++R)X.push(R); while(RegionFromZone.length < ShipTable.length)RegionFromZone.push(X);}  //one for each zone
//------------------------------------------------------------------------
// CONSTRUCTOR:	 ShipEntry (zone, pkginfo)
//------------------------------------------------------------------------
function ShipEntry (zone,pkginfo){
   this.zone=zone;
   this.pkginfo=pkginfo;
}
//------------------------------------------------------------------------
// CONSTRUCTOR:	 PkgClass (weight, size, costfixed, costperwtunit, wtunit, flag)
//------------------------------------------------------------------------
function PkgClass (weight,size,costfixed,costperwtunit,wtunit,flag){
   this.weight=weight;
   this.size=size;
   this.costfixed=costfixed;
   this.costperwtunit=costperwtunit;
   this.wtunit=wtunit;
   this.flag=flag;
}
//------------------------------------------------------------------------
// CONSTRUCTOR:	 PackingRule (itmsizeinfo, itmqtyinfo, pkgsize)
//------------------------------------------------------------------------
function PackingRule (itmsizeinfo,itmqtyinfo,pkgsize){
   this.itmsizeinfo=itmsizeinfo;
   this.itmqtyinfo=itmqtyinfo;
   this.pkgsize=pkgsize;
}
//------------------------------------------------------------------------
// CONSTRUCTOR:	 Size (Length, width, height)
//------------------------------------------------------------------------
function Size (Length,width,height){
   this.Length=NumberZ(Length);	//calling it this.length could be trouble since objects already have a length attribute?
   this.width =NumberZ(width );
   this.height=NumberZ(height);
}
//------------------------------------------------------------------------
// CONSTRUCTOR:	 Qszwt (qty, size, weighteach)
//------------------------------------------------------------------------
function Qszwt (qty,size,weighteach){
   this.qty   =Integer(qty);
   this.size  =size;
   this.weight=NumberZ(weighteach) * qty;
   this.wt=[];  for(var w=0; w<qty; ++w) this.wt[w]= NumberZ(weighteach);
   //the wt array is needed to handle same-size items having different weights
}
//------------------------------------------------------------------------
// FUNCTION:  SizeStr (size) -- convert size to string
//------------------------------------------------------------------------
function SizeStr (size){
   return(size.Length + "x" + size.width + "x" + size.height);
}
//------------------------------------------------------------------------
// FUNCTION:  SizeVolume (size) -- returns the volume==Length*width*height;
//------------------------------------------------------------------------
function SizeVolume (size){
   return(size.Length * size.width * size.height);
}
//------------------------------------------------------------------------
// FUNCTION:  SizeEQ (size1, size2) -- compare two sizes for equality
//------------------------------------------------------------------------
function SizeEQ (size1,size2){
   return(size1.Length==size2.Length && size1.width==size2.width && size1.height==size2.height);
}
//------------------------------------------------------------------------
// FUNCTION:  InitPkgQueue() -- initialize the PkgQueue
//------------------------------------------------------------------------
function InitPkgQueue(){
   PkgQueue = [];	//init to an empty array
}
//------------------------------------------------------------------------
// FUNCTION:  AddPkgQueueEntry (qty, size, weighteach) -- revise the global PkgQueue, an array-of-Qszwt;
// if there is an entry for size in PkgQueue, then update its qty & weight;
// otherwise add an entry.
//------------------------------------------------------------------------
function AddPkgQueueEntry (qty,size,weighteach){
   for(var i=0; i<PkgQueue.length; ++i) if(SizeEQ(PkgQueue[i].size, size)){
      PkgQueue[i].qty += Integer(qty);
      PkgQueue[i].weight += NumberZ(weighteach)*Integer(qty);  for(var w=0; w<qty; ++w) PkgQueue[i].wt.push(NumberZ(weighteach));
      return;
   }
   PkgQueue.push(new Qszwt(qty,size,weighteach));
}
//------------------------------------------------------------------------
// FUNCTION:  RemovePkgQueueEntry (i) -- remove the PkgQueue[i] entry
//------------------------------------------------------------------------
function RemovePkgQueueEntry (i){
   PkgQueue.splice(i, 1);
}
//------------------------------------------------------------------------
// FUNCTION:  ShowPkgQueue -- display the PkgQueue  (convert to string)
//------------------------------------------------------------------------
function ShowPkgQueue(){
   var str="";
   for(var i=0; i<PkgQueue.length; ++i){
      str+= "qty:"+PkgQueue[i].qty +"; sz:"+SizeStr(PkgQueue[i].size) +"; wt:"+Math.round(PkgQueue[i].weight) +" [";
      for(var w=0; w<PkgQueue[i].qty; ++w) str+= Math.round(PkgQueue[i].wt[w]) + " ";
      str+= "]\n";
   }
   return str;
}
//------------------------------------------------------------------------
// FUNCTION:  PickAndApplyPackingRule -- find & apply the best matching PackingRule,
// thereby simplifying the global PkgQueue;  returns false if no rule matches;
// Algorithm:
// if we have any size mentioned in only one PackingRule, then apply that rule;
// otherwise, pick the best matching rule (by volumetric efficiency**) and apply it
//------------------------------------------------------------------------
function PickAndApplyPackingRule(PackingRule){
   var SZ=null;	 var P=null;  var bestGoodness=0;
   for(var i=0; i<PkgQueue.length; ++i){
      var sz=PkgQueue[i].size,  p=null,  ct=0;
      for(var r=0; r<PackingRule.length; ++r) for(var e=0; e<PackingRule[r].itmsizeinfo.length; ++e) if(SizeEQ(PackingRule[r].itmsizeinfo[e], sz)){
	 p=r; ++ct; break;
      }
      if(ct==1) {SZ=sz; P=p; break;}				//--have found P an ONLY rule for size SZ
   }
   if(SZ==null){
      var minRV=99999999; for(var r=0; r<PackingRule.length; ++r) {var RV= SizeVolume(PackingRule[r].pkgsize); if(RV<minRV) minRV=RV;}
      for(var r=0; r<PackingRule.length; ++r){
	 var MV=0;
	 for(var e=0; e<PackingRule[r].itmsizeinfo.length; ++e) for(var i=0; i<PkgQueue.length; ++i) if(SizeEQ(PackingRule[r].itmsizeinfo[e], PkgQueue[i].size)){
	    MV+= SizeVolume(PkgQueue[i].size) * Math.min(PkgQueue[i].qty, PackingRule[r].itmqtyinfo[e]);
	 }
	 var RV=SizeVolume(PackingRule[r].pkgsize),  relRV=RV/minRV;
	 var VE=MV/RV;						//volumetric-efficiency (Matching-Volume over Result-Volume)
	 var g=VE/relRV;					//(**)modified volumetric-efficiency, to favour rule with smaller result-size
	 if(g>bestGoodness) {P=r; bestGoodness=g;}		//--have found a new BEST rule P
      }
   }
   if(P==null) return false;	//--indicate NO matching rule found
   if(SZ!=null)	 sRule= "PackingRule[" + P + "] is ONLY rule for sz:" + SizeStr(SZ) + "\n";
   else		 sRule= "PackingRule[" + P + "] is BEST g:" + Math.round(bestGoodness*1000)/1000 + "\n";
   sRules+=sRule;
   //now apply rule P; first reducing qty or removing matched PkgQueue-entries, then adding an entry for the resulting pkg-size;
   var wei=0;
   for(var e=0; e<PackingRule[P].itmsizeinfo.length; ++e) for(var i=0; i<PkgQueue.length; ++i) if(SizeEQ(PackingRule[P].itmsizeinfo[e], PkgQueue[i].size)){
      var Q= Math.min(PkgQueue[i].qty, PackingRule[P].itmqtyinfo[e]);
      //wei+= PkgQueue[i].weight * Q;
      for(w=0; w<Q; ++w) {wei+= PkgQueue[i].wt[w]; PkgQueue[i].weight-= PkgQueue[i].wt[w];}
      PkgQueue[i].qty-= Q;
      if(PkgQueue[i].qty==0) RemovePkgQueueEntry(i);
      else		     PkgQueue[i].wt.splice(0, Q);
   }
   AddPkgQueueEntry(1, PackingRule[P].pkgsize, wei);
   return true;
}
sRule="";	//global var showing most recently applied PackingRule, for DEBUG purposes only
sRules="";	//global var showing which PackingRules were applied,	for DEBUG purposes only
//------------------------------------------------------------------------
// FUNCTION:  ComputePackageSize -- compute sum of item-sizes from global PkgQueue;
// such summing, not altogether straightforward, is known as the 3D Bin Packing Problem;
// RESULT:  global PkgQueue gets a simplified list of sub-packets;
//	    global PkgAsOne describes the order as a single package;
// Algorithm:
// while any packingrule matches the item-sizes in PkgQueue do
//    pick and apply the best matching rule (see PickAndApplyPackingRule)
// done
//------------------------------------------------------------------------
function ComputePackageSize (ZoneParam){
   DEBUG2(ShowPkgQueue());
   var PR=PackTable[ZoneParam];
   if(PR.length>0){
      //---use Packing-Rules Method;
      //   Condidered doing this twice:	 with packBySz to get the smallest packages,  with packByWt to get the weight-limited separate packages for comparison
      //   (2nd would need to start with a copy of PkgQueue from before 1st; see System.arraycopy;  OR: better to construct new list in both steps)
      //   but doubt it's required  (using ByWt rules can lead to single-package being bigger than optimal, but only when items are high-density...)
      sRules="Zone:"+ShipTable[ZoneParam].zone+"\n";
      //==!!possibly add some sanity-checking on the PackingRules??
      while((PkgQueue.length>1 || (PkgQueue.length==1 && PkgQueue[0].qty>1)) && PickAndApplyPackingRule(PR)) DEBUG2(sRule+ShowPkgQueue());	//was: {}
      DEBUG1(sRules+"Packages:\n"+ShowPkgQueue());
   }
   //---use Crude Method, both in the absence of, and AFTER using PackingRules (to compute size as single package)
   var thk=0,  len=0,  wid=0,  wei=0;
   for(var i=0; i<PkgQueue.length; ++i){
      if(PkgQueue[i].size.Length > len) len = PkgQueue[i].size.Length;
      if(PkgQueue[i].size.width  > wid) wid = PkgQueue[i].size.width ;
      thk += PkgQueue[i].size.height * PkgQueue[i].qty;
      wei += PkgQueue[i].weight;
   }
   PkgAsOne = new Qszwt(1, new Size(len,wid,Math.round(thk*SZROUND)/SZROUND), Math.ceil(wei * WTROUND)/WTROUND);	//weight rounded-up
   //==!!NEEDED: Crude-Method, try placing several small items (LxW) into one layer, guided by package-size defns -- in case PackingRules omitted;
   //  OR: supply a script that reads a given set of HTML files, extracting items for sale, and then computes the PackingRules  (and make PackingRules required)
}
//------------------------------------------------------------------------
// FUNCTION: ComputeShipping -- compute the shipping cost for size and weight of package
//------------------------------------------------------------------------
function ComputeShipping (ZoneParam){
   sComputeShippingNote="";
   if(PkgAsOne.weight==0 && PkgAsOne.size.height==0) return 0.00;
   var Ship= ShipTable[ZoneParam].pkginfo;			//Ship is array of PkgClass(weight,size,costfixed,costperwtunit,wtunit,flag)
   function PricePkg(Ship,weight,height){			//function to lookup price for one pkg
      for(var c=0; c<Ship.length; ++c) if(weight<=Ship[c].weight && height<=Ship[c].size.height)
      {  return  Ship[c].costfixed + Ship[c].costperwtunit * Math.ceil(weight / Ship[c].wtunit); }	//return shipping-price
      return 99999.99;											//return illegal-weight-or-size indication
   }
   var asOne= PricePkg(Ship, PkgAsOne.weight, PkgAsOne.size.height);		//---compute price as a single package
   
   var asMult=99999.99,	 FC=null,  iN=0;
   for(var c=0; c<Ship.length; ++c) if(Ship[c].flag=="*") FC=c;
   if(FC!=null){								//---also price as multiple smaller packages (to handle pricing anomalies...)
      var maxHt=Ship[FC].size.height;
      var maxWt=Ship[FC].weight;
      var accHt=0,  accWt=0, sW="";  function R(f) {return " "+Math.ceil(f)+WTUNITS;}
      asMult=0;
      for(var i=PkgQueue.length; i--;) for(var j=PkgQueue[i].qty; j--;){	//do in reverse order (doing lightest ones first is best, at least for my examples:-)
	 var Wt= PkgQueue[i].wt[j];
	 var Ht= PkgQueue[i].size.height;
	 if( Wt>maxWt || Ht>maxHt) {asMult=99999.99; break;}			//having ByWt-packing, now treat this as "separate packages impossible"	 !!double-break??
	 if(accWt+Wt>maxWt || accHt+Ht>maxHt){					//handle previously accumulated little ones
	    asMult+= PricePkg(Ship, accWt, accHt);  ++iN; sW+=R(accWt);
	    accHt=0; accWt=0;
	 }
	 accWt+=Wt;  accHt+=Ht;							//accumulate another little one
      }
      if(accWt+accHt){asMult+=PricePkg(Ship,accWt,accHt); ++iN; sW+=R(accWt);}	//finish off any non-handled accumulation
   }
   asOne= Cents(asOne);   var asOneVat=  Cents(asOne *ShipTaxRate);
   asMult=Cents(asMult);  var asMultVat= Cents(asMult*ShipTaxRate);
   asOne += asOneVat  + HandlingChargePerOrder;						//add HandlingCharges BEFORE comparing
   asMult+= asMultVat + HandlingChargePerOrder + (iN-1)*HandlingChargePerExtraPackage;	//add HandlingCharges BEFORE comparing
   var cost;
   if(asOne<=asMult) {cost=asOne;  gVat=asOneVat;}													//as one package
   else		     {cost=asMult; gVat=asMultVat; sComputeShippingNote="("+strAsMultiple+sW+"; "+strAsSingle+MoneySymbol+moneyFormat(asOne)+")";}	//as multiple packages  !!sW+asOne nice when debugging; may remove??
   if(cost>=99999)   {sComputeShippingNote=strBroken;  return 99999.99;}
   return  cost;
   //
   //!!consider: keep info on the items within each packet, so can produce packing-instructions showing which items go into which size envelope (for the shipping dept)
}
//------------------------------------------------------------------------
// FUNCTION: NewZone -- update the ZoneSelected cookie
//------------------------------------------------------------------------
function NewZone (ZoneParam){
   SetCookie("ZoneSelected", ZoneParam, null, "/");
   var RegionCookie= GetCookie("RegionSelected");
   if(RegionCookie && RegionFromZone.length && !Element(RegionCookie, RegionFromZone[ZoneParam]))  DeleteCookie("RegionSelected","/");	//delete cookie if now illegal
   location.href=location.href;
}
//------------------------------------------------------------------------
// FUNCTION: NewRegion -- update the RegionSelected cookie
//------------------------------------------------------------------------
function NewRegion (RegionParam){
   SetCookie("RegionSelected", RegionParam, null, "/");
   var ZoneCookie= GetCookie("ZoneSelected");
   if(ZoneCookie && RegionFromZone.length && !Element(RegionParam, RegionFromZone[ZoneCookie]))  DeleteCookie("ZoneSelected","/");	//delete cookie if now illegal
   location.href=location.href;
}
//------------------------------------------------------------------------
// FUNCTION: MoreLessInfo -- do toggling-update to the MoreState cookie, for DynamicWtSzColumns option
//------------------------------------------------------------------------
function MoreLessInfo(){
   var MoreState=GetCookie("MoreState");  if(MoreState==null) MoreState= (DisplayWtColumn?1:0)*2 + (DisplaySzColumn?1:0);
   MoreState= ((MoreState&DynamicWtSzColumns)==DynamicWtSzColumns ?0 :DynamicWtSzColumns);	//toggle Wt&Sz-state as per DynamicWtSzColumns option
   SetCookie("MoreState", MoreState, null, "/");						//update cookie
   location.href=location.href;									//redraw page
}


//========================================================================
// The rest still resembles the NOP-Design version of nopcart.js (with some Stefko mods).
// ER: have revised ALL  parseFloat --> NumberZ	(to permit whole or fractional number where fractional was needed)
// ER: have revised ALL  parseInt   --> Integer	(new function that Rounds to whole-number)
// ER: introduced a few functions to reduce duplication, thus making future maintenance less tedious / error-probe
// ER: my other revisions are flagged with a comment containing "ER:"
// ER: after my Quantity-Discount Pricing mods (2007-08-07), any resemblance to the original is but faint
//========================================================================


//------------------------------------------------------------------------
// FUNCTION: CKquantity
// PARAMETERS: Quantity to validate
// RETURNS: Quantity as a whole-number, but in a string
// PURPOSE: Make sure quantity is a whole-number
//------------------------------------------------------------------------
function NumberV(checkString) {
   var sNewString="", K=0;
   for(var i=0; i<checkString.length; ++i){
      ch = checkString.substring(i, i+1);
      if(ch>="0" && ch<="9")     sNewString += ch;	//keep all digits
      else if(ch=="." && ++K==1) sNewString += ch;	//ER: keep only the first dot
   }
   return(NumberZ(sNewString));
}
function CKquantity(checkString) {
   var N=Integer(NumberV(checkString));  if(N==0) N=1;  return(""+N);
}
//------------------------------------------------------------------------
// FUNCTION: CKprice
// PARAMETERS: Price to validate
// RETURNS: Price as a number, but in a string
// ER: introduced this routine to validate an Online-Donation amount;
//------------------------------------------------------------------------
function CKprice(checkString) {
   var N=Cents(NumberV(checkString));
   if(N==0) N=DefaultDonation;  else if(N<MinimumDonation) {N=MinimumDonation; alert(MinimumDonationPrompt);}
   return(moneyFormat(N));
}

//------------------------------------------------------------------------
// FUNCTION: AddToCart
// PARAMETERS: Form Object
// RETURNS: Cookie to user's browser, optionally with a popup prompt
// PURPOSE: Add a product to the user's shopping cart
//------------------------------------------------------------------------
function AddToCart(thisForm) {
   var iNumberOrdered = 0;
   var bAlreadyInCart = false;
   var notice = "";
   iNumberOrdered= Integer(GetCookie("NumberOrdered"));
   sID_NUM   = "";      if(thisForm.ID_NUM        != null) sID_NUM   = thisForm.ID_NUM.value;
   sQUANTITY = "1";     if(thisForm.QUANTITY      != null) sQUANTITY = thisForm.QUANTITY.value;
   sPRICE    = "0.00";  if(thisForm.PRICE         != null) sPRICE    = thisForm.PRICE.value;
   sNAME     = "";      if(thisForm.NAME          != null) sNAME     = thisForm.NAME.value;
   sWEIGHT   = "0";     if(thisForm.WEIGHT        != null) sWEIGHT   = thisForm.WEIGHT.value;	//ER: sSHIPPING-->sWEIGHT
   sLENGTH   = "0";     if(thisForm.LENGTH        != null) sLENGTH   = thisForm.LENGTH.value;	//ER: new
   sWIDTH    = "0";     if(thisForm.WIDTH         != null) sWIDTH    = thisForm.WIDTH.value;	//ER: new
   sHEIGHT   = "0";     if(thisForm.HEIGHT        != null) sHEIGHT   = thisForm.HEIGHT.value;	//ER: new
   sADDTLINFO= "";      if(thisForm.ADDITIONALINFO!= null) sADDTLINFO= thisForm.ADDITIONALINFO[thisForm.ADDITIONALINFO.selectedIndex].value;
   if(thisForm.ADDITIONALINFO2!= null) sADDTLINFO += "; " + thisForm.ADDITIONALINFO2[thisForm.ADDITIONALINFO2.selectedIndex].value;
   if(thisForm.ADDITIONALINFO3!= null) sADDTLINFO += "; " + thisForm.ADDITIONALINFO3[thisForm.ADDITIONALINFO3.selectedIndex].value;
   if(thisForm.ADDITIONALINFO4!= null) sADDTLINFO += "; " + thisForm.ADDITIONALINFO4[thisForm.ADDITIONALINFO4.selectedIndex].value;
   if(thisForm.USERENTRY      != null) sADDTLINFO += (sADDTLINFO?"; ":"") + thisForm.USERENTRY.value;	//ER: avoid leading semicolon

   //Is this product already in the cart?  If so, increment quantity instead of adding another.
   for(var i=1; i<=iNumberOrdered; ++i){
      GetRow(i);		//ER: get fields for row-i
      if(fields[0] == sID_NUM &&
         fields[2] == sPRICE  &&
         fields[3] == sNAME   &&
         fields[8] == sADDTLINFO
      ){							//already in cart, so increase its quantity;  ER: a match on all but PRICE deserves a DEBUG-alert?
	 bAlreadyInCart = true;
	 dbUpdatedOrder = sID_NUM + "|" +
	    (Integer(sQUANTITY)+Integer(fields[1])) + "|" +
	    sPRICE  + "|" +
	    sNAME   + "|" +
	    sWEIGHT + "|" +
	    sLENGTH + "|" +
	    sWIDTH  + "|" +
	    sHEIGHT + "|" +
	    sADDTLINFO;
	 sNewOrder = "Order." + i;
	 DeleteCookie(sNewOrder, "/");
	 SetCookie(sNewOrder, dbUpdatedOrder, null, "/");
	 notice = strAdded + "\n-------------------------------------\n" + strAddedQuantity + sQUANTITY + "\n" + strAddedProduct + sNAME;
	 break;
      }
   }
   if(!bAlreadyInCart){						//not in cart, so add it
      iNumberOrdered++;
      if(iNumberOrdered > 15) alert(strSorry);			//limit nbr-rows in cart to 15;  ER: was 12  (the limit is 20 cookies for one domain)
      else {
	 dbUpdatedOrder = sID_NUM + "|" +
	    sQUANTITY + "|" +
	    sPRICE    + "|" +
	    sNAME     + "|" +
	    sWEIGHT   + "|" +
	    sLENGTH   + "|" +
	    sWIDTH    + "|" +
	    sHEIGHT   + "|" +
	    sADDTLINFO;
	 sNewOrder = "Order." + iNumberOrdered;
	 SetCookie(sNewOrder,       dbUpdatedOrder, null, "/");
	 SetCookie("NumberOrdered", iNumberOrdered, null, "/");
	 notice = strAdded + "\n-------------------------------------\n" + strAddedQuantity + sQUANTITY + "\n" + strAddedProduct + sNAME;
      }
   }
   if(DisplayPopupOnAdd && notice!="")  alert(notice);
}


//------------------------------------------------------------------------
// FUNCTION: moneyFormat
// PARAMETERS: Number to be formatted
// RETURNS: Formatted Number
// PURPOSE: convert float to #.## string
// ER: I first rewrote this to something I could understand, since supporting MoneyPLACES option was unthinkable otherwise;
//------------------------------------------------------------------------
function moneyFormatFRA(input) {	//for any currency that uses a fraction, example US-dollars
   var cents= "" + Math.round(input * MoneyROUND_FRA);
   while(cents.length < MoneyPLACES+1)  cents="0"+cents;
   return  cents.substring(0, cents.length-MoneyPLACES) + "." + cents.substring(cents.length-MoneyPLACES, cents.length);
   //
   //ER: was:
   // var dollars = Math.floor(input);
   // var tmp = new String(input);
   // for ( var decimalAt = 0; decimalAt < tmp.length; decimalAt++ ) {
   //   if ( tmp.charAt(decimalAt)=="." ) break;
   // }
   // var cents = "" + Math.round(input * 100);
   // cents = cents.substring(cents.length-2, cents.length)
   // dollars += ((tmp.charAt(decimalAt+2)=="9")&&(cents=="00"))? 1 : 0;
   // if ( cents == "0" ) cents = "00";
   // return(dollars + "." + cents);
}
function moneyFormatNOF(input) {	//ER: for a currency that uses no fraction (and may need rounding to a multiple of 1000 etc)
   return ""+Cents(input);
}
moneyFormat = (MoneyPLACES>0 ?moneyFormatFRA :moneyFormatNOF);	//ER: init function according to whether a fraction is needed  (MoneyPLACES indicates that)

//------------------------------------------------------------------------
// FUNCTION: getCookieVal
// PARAMETERS: offset
// RETURNS: URL unescaped Cookie Value
// PURPOSE: Get a specific value from a cookie
//------------------------------------------------------------------------
function getCookieVal (offset) {
   var endstr = document.cookie.indexOf (";", offset);
   if(endstr == -1)  endstr= document.cookie.length;
   return unescape(document.cookie.substring(offset, endstr));
}
//------------------------------------------------------------------------
// FUNCTION: FixCookieDate
// PARAMETERS: date
// RETURNS: date
// PURPOSE: Fix cookie date, store back in date
//------------------------------------------------------------------------
//function FixCookieDate (date) {
//   var base= new Date(0);
//   var skew= base.getTime();
//   date.setTime(date.getTime() - skew);
//}
//------------------------------------------------------------------------
// FUNCTION: GetCookie
// PARAMETERS: Name
// RETURNS: Value in Cookie
// PURPOSE: Retrieve cookie from users browser
//------------------------------------------------------------------------
function GetCookie (name) {
   var arg = name + "=";
   var alen = arg.length;
   var clen = document.cookie.length;
   var i = 0;
   while(i < clen){
      var j = i + alen;
      if(document.cookie.substring(i, j)==arg)  return(getCookieVal(j));
      i = document.cookie.indexOf(" ", i) + 1;
      if(i == 0) break;
   }
   return(null);
}
//------------------------------------------------------------------------
// FUNCTION: SetCookie
// PARAMETERS: name, value, expiration date, path, domain, security
// RETURNS: Null
// PURPOSE: Store a cookie in the users browser
//------------------------------------------------------------------------
function SetCookie (name,value,expires,path,domain,secure) {
   document.cookie = name + "=" + escape (value) +
   ((expires) ? "; expires=" + expires.toGMTString() : "") +
   ((path) ? "; path=" + path : "") +
   ((domain) ? "; domain=" + domain : "") +
   ((secure) ? "; secure" : "");
}
//------------------------------------------------------------------------
// FUNCTION: DeleteCookie
// PARAMETERS: Cookie name, path, domain
// RETURNS: null
// PURPOSE: Remove a cookie from users browser.
//------------------------------------------------------------------------
function DeleteCookie (name,path,domain) {
   if(GetCookie(name)){
      document.cookie = name + "=" +
      ((path) ? "; path=" + path : "") +
      ((domain) ? "; domain=" + domain : "") +
      "; expires=Thu, 01-Jan-70 00:00:01 GMT";
   }
}
//------------------------------------------------------------------------
// FUNCTION: GetRow
// PURPOSE: read one cart-row from the cookie-database
// RETURNS: global array fields, an array containing the fields
// NOTE: in the database-format, fields are separated by "|"
// ER: made this a function to improve maintainability.
//------------------------------------------------------------------------
function GetRow(i){
   RowKey = "Order." + i;
   dbrow = "";
   dbrow = GetCookie(RowKey);
   Token0 = dbrow.indexOf("|", 0);
   Token1 = dbrow.indexOf("|", Token0+1);
   Token2 = dbrow.indexOf("|", Token1+1);
   Token3 = dbrow.indexOf("|", Token2+1);
   Token4 = dbrow.indexOf("|", Token3+1);
   Token5 = dbrow.indexOf("|", Token4+1);
   Token6 = dbrow.indexOf("|", Token5+1);
   Token7 = dbrow.indexOf("|", Token6+1);
   fields = [];
   fields[0] = dbrow.substring(0, Token0);		// Product ID
   fields[1] = dbrow.substring(Token0+1, Token1);	// Quantity
   fields[2] = dbrow.substring(Token1+1, Token2);	// Price
   fields[3] = dbrow.substring(Token2+1, Token3);	// Product Name
   fields[4] = dbrow.substring(Token3+1, Token4);	// Weight
   fields[5] = dbrow.substring(Token4+1, Token5);	// Length;      ER: new
   fields[6] = dbrow.substring(Token5+1, Token6);	// Width;	ER: new
   fields[7] = dbrow.substring(Token6+1, Token7);	// Height;      ER: new
   fields[8] = dbrow.substring(Token7+1, dbrow.length);	// Addtl-Info;  ER: was fields[5]
   
}
//------------------------------------------------------------------------
// FUNCTION: RemoveFromCart
// PARAMETERS: Row Number to Remove
// RETURNS: Null
// PURPOSE: Remove an item from a users shopping cart
//------------------------------------------------------------------------
function RemoveFromCart(RemOrder) {
   if( (DisplayPopupOnRemove ? confirm(strRemove) : true) ){	//ER: suppress the confirm when DisplayPopupOnRemove==false
      NumberOrdered = Integer(GetCookie("NumberOrdered"));
      for(var i=RemOrder; i < NumberOrdered; ++i){
	 NewOrder1 = "Order." + (i+1);
	 NewOrder2 = "Order." + (i);
	 database = GetCookie(NewOrder1);
	 SetCookie (NewOrder2, database, null, "/");
      }
      NewOrder = "Order." + NumberOrdered;
      SetCookie ("NumberOrdered", NumberOrdered-1, null, "/");
      DeleteCookie(NewOrder, "/");
      location.href=location.href;
   }
}
//------------------------------------------------------------------------
// FUNCTION: EmptyTheCart
// PURPOSE: Remove all items from a users shopping cart.
// Intended for a thanks-for-your-purchase page (that your payment-processor is instructed to return to),
// since after checkout one expects an empty cart...
//------------------------------------------------------------------------
function EmptyTheCart(){
   NumberOrdered = Integer(GetCookie("NumberOrdered"));
   for(var i=1; i <= NumberOrdered; ++i){
      NewOrder = "Order." + i;
      DeleteCookie(NewOrder, "/");
   }
   SetCookie("NumberOrdered", 0, null, "/");
}
//------------------------------------------------------------------------
// FUNCTION: ChangeQuantity
// PARAMETERS: Order Number to Change Quantity
// RETURNS: Null
// PURPOSE: Change quantity of an item in the shopping cart
//------------------------------------------------------------------------
function ChangeQuantity(OrderItem,NewQuantity) {
   if(isNaN(NewQuantity)){
      alert(strErrQty);
   }else{
      GetRow(OrderItem);	//ER: get fields for row-OrderItem
      dbUpdatedOrder = fields[0] + "|" +
         NewQuantity + "|" +
         fields[2]   + "|" +
         fields[3]   + "|" +
         fields[4]   + "|" +
         fields[5]   + "|" +
         fields[6]   + "|" +
         fields[7]   + "|" +
         fields[8];
      sNewOrder = "Order." + OrderItem;
      DeleteCookie(sNewOrder, "/");
      SetCookie(sNewOrder, dbUpdatedOrder, null, "/");
      location.href=location.href;
   }
}
////------------------------------------------------------------------------
//// FUNCTION:	RadioChecked
//// PARAMETERS:  Radio button to check
//// RETURNS:	True if a radio has been checked
//// PURPOSE:	Form validation
//// ER: NOT USED
////------------------------------------------------------------------------
//function RadioChecked( radiobutton ) {
//   var bChecked = false;
//   var rlen = radiobutton.length;
//   for(var i=0; i < rlen; ++i)  if(radiobutton[i].checked)  bChecked = true;
//   return bChecked;
//}
////------------------------------------------------------------------------
//// FUNCTION: QueryString
//// PARAMETERS: Key to read
//// RETURNS: value of key
//// PURPOSE: Read data passed in via GET mode
//// ER: NOT USED
////------------------------------------------------------------------------
//QueryString.keys = [];
//QueryString.values = [];
//function QueryString(key) {
//   var value = null;
//   for(var i=0;i<QueryString.keys.length;++i){
//      if (QueryString.keys[i]==key) {
//	 value = QueryString.values[i];
//	 break;
//      }
//   }
//   return value;
//}
////------------------------------------------------------------------------
//// FUNCTION: QueryString_Parse
//// PARAMETERS: (URL string)
//// PURPOSE: Parse query string data;  must be called before QueryString
//// ER: NOT USED
////------------------------------------------------------------------------
//function QueryString_Parse() {
//   var query= window.location.search.substring(1);
//   var pairs= query.split("&");  for(var i=0;i>pairs.length;++i){
//      var pos = pairs[i].indexOf("=");
//      if (pos >= 0) {
//	 var argname = pairs[i].substring(0,pos);
//	 var value = pairs[i].substring(pos+1);
//	 QueryString.keys[QueryString.keys.length] = argname;
//	 QueryString.values[QueryString.values.length] = value;
//      }
//   }
//}


//------------------------------------------------------------------------
// FUNCTION: ReadCartComputePrices
// PURPOSE:  Read all rows from the cookies
// RETURNS:  globals iNumberOrdered, and Cart an array with Cart[i] being row-i;
// NOTE THAT:
//	Cart[i].ID_NUM    replaces all subsequent uses of  fields[0];
//	Cart[i].QUANTITY  replaces all subsequent uses of  fields[1]  OR  Integer(fields[1]);
//	Cart[i].PRICEAVG  replaces all subsequent uses of  fields[2]  OR  NumberZ(fields[2]);
//	Cart[i].NAME      replaces all subsequent uses of  fields[3];
//	Cart[i].WEIGHT    replaces all subsequent uses of  fields[4]  OR  NumberZ(fields[4]);
//	Cart[i].LENGTH    replaces all subsequent uses of  fields[5]  OR  NumberZ(fields[5]);
//	Cart[i].WIDTH     replaces all subsequent uses of  fields[6]  OR  NumberZ(fields[6]);
//	Cart[i].HEIGHT    replaces all subsequent uses of  fields[7]  OR  NumberZ(fields[7]);
//	Cart[i].ADDTLINFO replaces all subsequent uses of  fields[8];
//	Cart[i].PRICE     is the entire string from Form.PRICE; but should never be needed outside of this routine;
// Form.PRICE now consists of comma-separated terms such as:
//	3.95			  -- means 3.95 each
//	3.95,2:3.00		  -- means 3.95 for the first, 2nd and subsequent are 3.00 each;
//	3.95,10=2.995		  -- means 3.95 each, or exactly 10 for 29.95;
//	3.95,10=2.99,10:2.99	  -- means 3.95 each, 10 or more are 2.99 each;
//	3.95,2:3.00,4:2.75,8:2.50 -- means 2nd...are 3.00, 4th...are 2.75, 8th and subsequent are 2.50;
//	3.95,2:-30,GRP01	  -- means 2nd and subsequent are 30% off, and this applies across all products in the group GRP01;
//	can have any number of ":" or "=" terms, at most one starting with a letter to name a group;
//	":" and "=" terms may be interspersed, or not, but the ":" terms must appear in increasing order by left-side, and "=" terms likewise;
//	RESTRICTION: exact-quantity ("=") pricing is only permitted within a group where all products have identical prices;
//	anything else would be so bloody hard to explain, it's hard to see anyone wanting it;
// ER: All-at-once reading is essential for a reasonably efficient implementation of Quantity-Discount Pricing,
//	since several passes are needed to compute prices, and those are needed before the pass that displays & does payment-processing;
//	(more than one preliminary pass is needed to support Product-Groups);
// ER: also moved the Shipping & Tax calculation code here, from ManageCart/CheckoutCart, setting globals: fTotal, fShipping, fTax, ZoneSelected, RegionSelected,
//	to further reduce duplication thus lessening the tedium & error-proneness of making modifications.
// Q: To get deterministic n-or-more pricing, independent of the order items added to cart, could sort by price within group?
// Q: !!Can the payment-processors handle prices with more fractional digits than is the currency norm?  (May apply Cents-rounding to PRICEAVG + revise one example?)
//------------------------------------------------------------------------
function ReadCartComputePrices(){
   var Dig="0123456789", Lwr="abcdefghijklmnopqrstuvwxyz", Upr="ABCDEFGHIJKLMNOPQRSTUVWXYZ", Let=Lwr+Upr;	//constants for Is-testing
   function Is(c,pat) {return pat.indexOf(c)!=-1;}
   var i, k;
   var C, G, D, X;				//results from the Pparse routine
   function Pparse(priceparm){			//the Pparse PRICE-parsing routine
      C=0; G=""; D=[]; X=[];
      for(var price=priceparm.split(","), K=0; K<price.length; ++K){
         var T=price[K];
         if(     T.indexOf("=")!=-1) {var x=T.split("="); X.push( {q:Integer(x[0]), p:NumberZ(x[1])} );}
	 else if(T.indexOf(":")!=-1) {var x=T.split(":"); D.push( {q:Integer(x[0]), p:NumberZ(x[1])} );}
	 else if(Is(T[0],Let))       G=T;
	 else                        C=NumberZ(T);
      }
   }
   //====read all rows from the cookies====
   Cart = [];						//init global Cart array
   iNumberOrdered=Integer(GetCookie("NumberOrdered"));	//get the nbr-rows-in-cart cookie
   for(i=1; i<=iNumberOrdered; ++i){
      GetRow(i);					//get fields for row-i
      Pparse(fields[2]);				//parse the PRICE string
      Cart[i]= {
         ID_NUM:           fields[0], 
         QUANTITY: Integer(fields[1]), 
	 PRICE:            fields[2], 
	 NAME:             fields[3], 
	 WEIGHT:   NumberZ(fields[4]), 
	 LENGTH:   NumberZ(fields[5]), 
	 WIDTH:    NumberZ(fields[6]), 
	 HEIGHT:   NumberZ(fields[7]), 
	 ADDTLINFO:        fields[8],
	 C:C, G:G, D:D, X:X, PRICEAVG:null		//the parsed-price fields; consider using ID_NUM as G if no group specified(?)
      }
   }
   //====compute prices====
   for(i=1; i<=iNumberOrdered; ++i){
      if(Cart[i].PRICEAVG!=null) continue;					//skip if row already done (due to groups)
      C=Cart[i].C;  G=Cart[i].G;  D=Cart[i].D;  X=Cart[i].X;			//info from Pparse
      function eEQ(A,B) {return A.q==B.q && A.p==B.p;}											//compare q-p elements
      function aEQ(A,B) {if(A.length!=B.length)return false; for(var k=A.length;k--;)if(!eEQ(A[k],B[k]))return false; return true;}	//compare array of q-p elements
      function pEQ(A,B) {return A.C==B.C && aEQ(A.D,B.D) && aEQ(A.X,B.X);}								//compare parsed prices
      function str(X) {var s="["; for(var i=0;i<X.length;++i)s+="{"+X[i].q+","+X[i].p+"},"; s+="]"; return s;}	//convert X or D to string
      function pp(P)  {return (P>=0 ?P :C*(100+P)/100);}							//convert negative price to a percent-off-wrt-C price
      var q=Cart[i].QUANTITY;										//q is qty of this product
      var Q=q;   if(G!="")for(Q=0, k=1; k<=iNumberOrdered; ++k) if(Cart[k].G==G) Q+= Cart[k].QUANTITY;	//Q is qty across all products in this group
      var g=[i]; if(G!="")for(g=[],k=1; k<=iNumberOrdered; ++k) if(Cart[k].G==G) g.push(k);		//g is array of indices for rows in this group
      var ix=-1; for(k=X.length; k--;) if(X[k].q<=Q) {ix=k; break;}		//find the biggest applicable exact-qty ("=") discount;  requires ordered terms
      var id=-1; for(k=D.length; k--;) if(D[k].q<=Q) {id=k; break;}		//find the biggest applicable n-or-more (":") discount;  requires ordered terms
      DEBUG4("row:"+i+" itm:"+Cart[i].ID_NUM+" PRICE:"+Cart[i].PRICE+" C:"+C+" G:"+G+" X:"+str(X)+" D:"+str(D)+" g:"+g+" ix:"+ix+" id:"+id);
      if(X.length>0){
       //var m=[];for(k=g.length;k--;)if(Cart[g[k]].PRICE!=Cart[i].PRICE)m.push(g[k]);	//sanity-check prices across group, using simple string-comparisons
	 var m=[];for(k=g.length;k--;)if(!pEQ(Cart[g[k]], Cart[i]))m.push(g[k]);	//sanity-check prices across group, comparing C,D,X to permit minor differences
	 if(m.length>0) DEBUG("group:"+G+" has exact-qty discount but PRICE on row:"+i+" conflicts with rows:"+m);	//issue DEBUG alert
	 if(m.length>0) {for(k=g.length; k--;) Cart[g[k]].PRICEAVG=C;  continue;}	//suppress further such DEBUG alerts for the other rows in group
      }
      var A, QQ, q2, I;
      if(ix!=-1){								//---apply an exact-quantity discount, to all rows in group---
	 A=0;  QQ=Q;
	 while(Q!=0 && ix!=-1){							//until all items in group are priced or no more applicable exact-discounts
	    q2= Math.floor(Q / X[ix].q) * X[ix].q;				//q2 is the largest multiple of X[ix].q that's less-than-or-equal-to Q
	    A += q2*pp(X[ix].p);  Q-=q2;					//sell q2 at price X[ix].p, revising Q to reflect remaining qty
	    DEBUG4("sell "+q2+" at:"+pp(X[ix].p)+" Q:"+Q);
	    --ix; while(ix>=0 && X[ix].q>Q) --ix;				//revise ix for the next applicable exact-discount, if any
	 }
	 if(Q>0) A += Q * (id!=-1 ? pp(D[id].p) :C);				//price the rest using the applicable n-or-more-price, or C if none
	 var priceavg= Cents(A) / QQ;
	 for(k=g.length; k--;)  {I=g[k];  Cart[I].PRICEAVG = priceavg;}		//each row in group gets the same price apiece

      }else if(id!=-1){								//---apply an n-or-more discount, to all rows in group---
         var ID, QD=0;								//QD is the count of items already priced
	 for(k=0; k<g.length; ++k){  I=g[k];					//for each row I in group do
	    A=0; q=Cart[I].QUANTITY; C=Cart[I].C;  D=Cart[I].D;
	    if(D.length==0 || D[0].q!=1)  D.unshift( {q:1, p:C} );		//augment D to include a price for the first one(s), to simplify search-loops, etc
	    while(q>0){
	       for(ID=0;;++ID) if(ID+1==D.length||QD+1<D[ID+1].q)break;		//now D[ID].p is the price for the QD+1'th (next) item
	       q2=q;  if(ID+1<D.length) q2=Math.min(q, D[ID+1].q-1-QD);		//q2 is nbr of items to sell at price D[ID].p
	       A+= q2*pp(D[ID].p);  QD+=q2;  q-=q2;				//sell q2 items at D[ID].p, revising QD & q
	       DEBUG4("sell "+q2+" at:"+pp(D[ID].p)+" ID:"+ID+" QD:"+QD);
	       if(q2<=0) {DEBUG("ReadCartComputePrices is broken"); break;}
	    }
            Cart[I].PRICEAVG = Cents(A) / Cart[I].QUANTITY;			//price apiece, to be displayed & passed to PaymentProcessor
	 }
      }else{									//---apply constant price to one row---
         Cart[i].PRICEAVG = C;							//price apiece, to be displayed & passed to PaymentProcessor
      }
   }
   //====total, shipping, tax calculations====
   ZoneSelected=  GetCookie("ZoneSelected");    ZoneChecked=ZoneSelected;				//get zone cookie
   RegionSelected=GetCookie("RegionSelected");  RegionChecked=RegionSelected;				//ER: get region cookie
   if(ZoneSelected==null) ZoneSelected=ZoneDefault;							//use zone-default if none selected;  ER: default was 8
   if(RegionFromZone.length && RegionSelected==null) RegionSelected=RegionFromZone[ZoneSelected][0];	//ER: use RegionFromZone option to set RegionSelected
   if(RegionFromZoneOverrides)                       RegionSelected=RegionFromZone[ZoneSelected][0];
   if(ZoneChecked && RegionFromZoneOvA[ZoneChecked]) RegionSelected=RegionFromZone[ZoneChecked] [0];
   if(RegionSelected==null) RegionSelected=RegionDefault;						//ER: use region-default, when RegionFromZone not being used
   if(RegionFromZone.length && !Element(RegionSelected,RegionFromZone[ZoneSelected])){			//ER: validity-check, the Zone+Region combination is invalid:
      if(ZoneChecked || !RegionChecked){								//ER: if user has picked Zone or neither, then revise Region
         RegionSelected=RegionFromZone[ZoneSelected][0];						//ER: revise RegionSelected to make it legal for Zone
      }else{												//ER: otherwise, revise Zone to achieve consistency
         for(var Z=RegionFromZone.length;Z--;) if(Element(RegionSelected, RegionFromZone[Z])) break;	//ER: find a valid Zone Z
	 if(Z>=0) ZoneSelected=Z;  else DEBUG("RegionFromZone option is invalid");			//ER: revise ZoneSelected to make it legal for Region
      }
   }
   if(RegionChecked) RegionChecked= RegionSelected;							//ER: validity-check RegionChecked
   if(ZoneChecked  ) ZoneChecked  = ZoneSelected;							//ER: validity-check ZoneChecked
   if(TaxRateRegional!=0 && RegionPrompt!="" && !RegionFromZoneOverrides && !(ZoneChecked && RegionFromZoneOvA[ZoneChecked])
   ) {}													//ER: leave Region unchecked if user will have to pick
   else RegionChecked=RegionSelected;									//ER: show as checked a Region that suffices
   if(ShipTable.length>1 && ZonePrompt!="") {}								//ER: leave Zone unchecked if user will have to pick
   else ZoneChecked=ZoneSelected;									//ER: show as checked a Zone that suffices
   //--ER: original version of above was ok for customer selecting Zone first, but would seriously harrass customer trying to select Region before Zone;
   // have fixed:  (2) revised validity-checking above;  and reordered, so setting Checked by PromptNotNeeded is done last, and for Zone after Region;
   // have fixed:  (3) NewZone routine now deletes Region-cookie if illegal;  NewRegion now deletes Zone-cookie if illegal;  (1) was to revise such cookie;
   // 	   	       can continue to regard the presence of a Zone or Region cookie as proof the user has selected (and it hasn't been altered since);
   InitPkgQueue();				//ER: initialize the PkgQueue object (used to compute package-size)
   fTotal=0; fTaxG=0; fTaxP=0; g_TotalQty=0;	//initialize subtotal and tax-subtotals
   var taxrateP=TaxRateRegional, taxrateG=TaxRate;
   if(     RegionSelected==0) {}						//ER: init taxrates based on RegionSelected
   else if(RegionSelected==1) {taxrateP=0;}
   else                       {taxrateP=0; taxrateG=0;}
   for(i=1; i<=iNumberOrdered; ++i){
      //ER: considered using TaxableTotal etc, so as to multiply just once;  not essential, provided the rounding-to-cents is deferred  (tho for tax-included it isn't)
      var ProdID=Cart[i].ID_NUM, QP=Cart[i].QUANTITY*Cart[i].PRICEAVG, taxP, taxG;
      if(     ProdID[0]=="n")  {taxP=0;            taxG=0;}			//"n" indicates neither tax applies
      else if(ProdID[0]=="p")  {taxP=QP*taxrateP;  taxG=0;}			//ER: "p" indicates only the regional-tax applies
      else if(ProdID[0]=="g")  {taxP=0;            taxG=QP*taxrateG;}		//ER: "g" indicates only the more global tax applies
      else 		       {taxP=QP*taxrateP;  taxG=QP*taxrateG;}		//anything else means both taxes apply
      if(DisplayTaxIncluded){							//ER: for tax-included pricing:
         taxP=Cents(taxP);  taxG=Cents(taxG);					//each tax is rounded to cents for each row, and
	 Cart[i].PRICEAVG += (taxP + taxG) / Cart[i].QUANTITY;			//the price-apiece is adjusted to include taxes
      }
      AddPkgQueueEntry(Cart[i].QUANTITY, new Size(Cart[i].LENGTH,Cart[i].WIDTH,Cart[i].HEIGHT), Cart[i].WEIGHT);	//ER: accumulate for package-size
      fTotal+=QP;								//accumulate fTotal (the subtotal before shipping & tax)
      fTaxP+=taxP;  fTaxG+=taxG;						//accumulate tax-subtotals
      g_TotalQty+=Cart[i].QUANTITY;						//accumulate total-quantity
   }
   ComputePackageSize(ZoneSelected);						//ER: compute package-size
   fShipping = ComputeShipping(ZoneSelected);					//ER: removed if-else since function handles weight being zero
   fTaxG=Cents(fTaxG);  fTaxP=Cents(fTaxP);					//ER: round to Cents separately
   fTax=fTaxG+fTaxP;  if(DisplayTaxIncluded) fTax=0;				//ER: sum fTax for the tax line-item;  with tax-included pricing use zero
   g_TotalCost = fTotal + fShipping + fTax;					//compute the grand-total
}


//------------------------------------------------------------------------
// FUNCTION: AddPaymentProcessorFieldsForOneRow
// RECEIVES: PP is the payment-processor-code;
//	     i is the row-nbr, Cart[i] has all other info about current row
// RETURNS: appends to global string vars  sOutput and sDescPP
// ER: made this a function to improve maintainability.
//------------------------------------------------------------------------
function AddPaymentProcessorFieldsForOneRow(PP, i){
   var sEnder="";  if(AppendItemNumToOutput) sEnder=""+i;				//ER: convert i to string once
   var NAME_INFO = Cart[i].NAME + (Cart[i].ADDTLINFO?("; "+Cart[i].ADDTLINFO):"");	//ER: combine NAME and ADDTLINFO into one string
   var NAME_INFO1=NAME_INFO.substring(0,127);						//ER: break into 3 strings according to PayPal limits  (check other PP limits)
   var NAME_INFO2=NAME_INFO.substring(127,327);						//ER: break into 3 strings according to PayPal limits  (check other PP limits)
   var NAME_INFO3=NAME_INFO.substring(327,527);						//ER: break into 3 strings according to PayPal limits  (check other PP limits)
   if(PP=="an" || PP=="wp" || PP=="lp"){						//for an/wp/lp, format Description-of-product as:  ID, Name, Qty:N
      sDescPP += Cart[i].ID_NUM + ", " + NAME_INFO + ", Qty:"+Cart[i].QUANTITY + "\n";
   }else if(PP=="pp"){									//ER: new - PayPal 
      sOutput+=                "<input type=hidden name=\"item_number_"+sEnder+"\"  value=\""+ Cart[i].ID_NUM   + "\">";
      sOutput+=                "<input type=hidden name=\"item_name_"+sEnder+"\"    value=\""+ NAME_INFO1       + "\">";
      sOutput+=                "<input type=hidden name=\"amount_"+sEnder+"\"       value=\""+ moneyFormat(Cart[i].PRICEAVG) + "\">";
      sOutput+=                "<input type=hidden name=\"quantity_"+sEnder+"\"     value=\""+ Cart[i].QUANTITY + "\">";
      if(NAME_INFO2) sOutput+= "<input type=hidden name=\"on0_"+sEnder+"\"          value=\""+ "Info2"          + "\">";
      if(NAME_INFO2) sOutput+= "<input type=hidden name=\"os0_"+sEnder+"\"          value=\""+ NAME_INFO2       + "\">";
      if(NAME_INFO3) sOutput+= "<input type=hidden name=\"on1_"+sEnder+"\"          value=\""+ "Info3"          + "\">";
      if(NAME_INFO3) sOutput+= "<input type=hidden name=\"os1_"+sEnder+"\"          value=\""+ NAME_INFO3       + "\">";
   }else if(PP=="gc"){									//ER: new - Google-Checkout
      sOutput+= "<input type=hidden name=\"item_name_"+sEnder+"\"         value=\""+ Cart[i].ID_NUM   + "\">";
      sOutput+= "<input type=hidden name=\"item_description_"+sEnder+"\"  value=\""+ NAME_INFO        + "\">";
      sOutput+= "<input type=hidden name=\"item_price_"+sEnder+"\"        value=\""+ moneyFormat(Cart[i].PRICEAVG) + "\">";
      sOutput+= "<input type=hidden name=\"item_quantity_"+sEnder+"\"     value=\""+ Cart[i].QUANTITY + "\">";
      sOutput+= "<input type=hidden name=\"item_currency_"+sEnder+"\"     value=\""+ gcCurrency       + "\">";
   }else if(PP=="cgi"){									//ER: was if(HiddenFieldsToCheckout)
      sOutput += "<input type=hidden name=\"" + OutputItemId        + sEnder + "\" value=\"" + Cart[i].ID_NUM    + "\">";
      sOutput += "<input type=hidden name=\"" + OutputItemQuantity  + sEnder + "\" value=\"" + Cart[i].QUANTITY  + "\">";
      sOutput += "<input type=hidden name=\"" + OutputItemPrice     + sEnder + "\" value=\"" + moneyFormat(Cart[i].PRICEAVG)  + "\">";
      sOutput += "<input type=hidden name=\"" + OutputItemName      + sEnder + "\" value=\"" + Cart[i].NAME      + "\">";
      sOutput += "<input type=hidden name=\"" + OutputItemWeight    + sEnder + "\" value=\"" + Cart[i].WEIGHT    + "\">";
      sOutput += "<input type=hidden name=\"" + OutputItemLength    + sEnder + "\" value=\"" + Cart[i].LENGTH    + "\">";
      sOutput += "<input type=hidden name=\"" + OutputItemWidth     + sEnder + "\" value=\"" + Cart[i].WIDTH     + "\">";
      sOutput += "<input type=hidden name=\"" + OutputItemHeight    + sEnder + "\" value=\"" + Cart[i].HEIGHT    + "\">";
      sOutput += "<input type=hidden name=\"" + OutputItemAddtlInfo + sEnder + "\" value=\"" + Cart[i].ADDTLINFO + "\">";
   }
}
//------------------------------------------------------------------------
// FUNCTION: AddPaymentProcessorFieldsFinal
// RECEIVES: PP is the payment-processor-code;  global var sDescPP is the all-in-one-description
// RETURNS: appends to global string var  sOutput
// ER: made this a function to improve maintainability.
//------------------------------------------------------------------------
function AddPaymentProcessorFieldsFinal(PP){
   if(PP=="an"){									//==Authorize.net WebConnect==
      sOutput += "<input type=hidden name=\"x_Version\"      value=\"3.0\">";
      sOutput += "<input type=hidden name=\"x_Show_Form\"    value=\"PAYMENT_FORM\">";
      sOutput += "<input type=hidden name=\"x_Description\"  value=\""+ sDescPP + "\">";
      sOutput += "<input type=hidden name=\"x_Amount\"       value=\""+ moneyFormat(fTotal + fShipping + fTax) + "\">";
   }else if(PP=="wp"){									//==WorldPay==
      sOutput += "<input type=hidden name=\"desc\"           value=\""+ sDescPP + "\">";
      sOutput += "<input type=hidden name=\"amount\"         value=\""+ moneyFormat(fTotal + fShipping + fTax) + "\">";
   }else if(PP=="lp"){									//==LinkPoint==
      sOutput += "<input type=hidden name=\"mode\"           value=\"fullpay\">";
      sOutput += "<input type=hidden name=\"chargetotal\"    value=\""+ moneyFormat(fTotal + fShipping + fTax) + "\">";
      sOutput += "<input type=hidden name=\"tax\"            value=\""+ MoneySymbol + moneyFormat(fTax) + "\">";
      sOutput += "<input type=hidden name=\"subtotal\"       value=\""+ MoneySymbol + moneyFormat(fTotal) + "\">";
      sOutput += "<input type=hidden name=\"shipping\"       value=\""+ MoneySymbol + moneyFormat(fShipping) + "\">";
      sOutput += "<input type=hidden name=\"desc\"           value=\""+ sDescPP + "\">";
   }else if(PP=="pp"){									//==PayPal==		//ER: new
      sOutput+=  "<input type=hidden name=\"tax_cart\"       value=\""+ moneyFormat(fTax)								+ "\">";
      sOutput+=  "<input type=hidden name=\"handling_cart\"  value=\""+ moneyFormat(fShipping)								+ "\">";
      sOutput+=  "<input type=hidden name=\"custom\"         value=\"Shipping: "+SHIPPINGCO+" "+ShipTable[ZoneSelected].zone+" "+sComputeShippingNote	+ "\">";  //ER: yanking did NOT solve SpecialInstructions being lost
      sOutput+=  "<input type=hidden name=\"no_note\"        value=\"1\">";					//ER: use NO_NOTE until PayPal fixes their NOTE-support
   }else if(PP=="gc"){									//==GoogleCheckout==	//ER: new
      if(fTax!=0){									//Google forces us to supply TAX as an item...
        sOutput+="<input type=hidden name=\"item_name_"+(iNumberOrdered+1)+"\"        value=\""+ "TAX"							+ "\">";
        sOutput+="<input type=hidden name=\"item_description_"+(iNumberOrdered+1)+"\" value=\""+ "Tax"+(TaxRateRegional?" for "+RegionTable[RegionSelected]:"")+"\">";
        sOutput+="<input type=hidden name=\"item_price_"+(iNumberOrdered+1)+"\"       value=\""+ moneyFormat(fTax)					+ "\">";
        sOutput+="<input type=hidden name=\"item_quantity_"+(iNumberOrdered+1)+"\"    value=\""+ "1"                                                    + "\">";
        sOutput+="<input type=hidden name=\"item_currency_"+(iNumberOrdered+1)+"\"    value=\""+ gcCurrency                                             + "\">";
      }
      sOutput+=  "<input type=hidden name=\"ship_method_price_1\"     value=\""+ moneyFormat(fShipping)							+ "\">";
      sOutput+=  "<input type=hidden name=\"ship_method_currency_1\"  value=\""+ gcCurrency                                                             + "\">";
      sOutput+=  "<input type=hidden name=\"ship_method_name_1\"      value=\""+ SHIPPINGCO+" "+ShipTable[ZoneSelected].zone+" "+sComputeShippingNote	+ "\">";
      sOutput+=  "<input type=hidden name=\"_charset_\"/>";				//Google says this is required, with no mention of alternatives(?)
   }else if(PP=="cgi"){									//==CUSTOM==  //ER: was if(HiddenFieldsToCheckout)
      sOutput += "<input type=hidden name=\""+OutputOrderSubtotal+"\" value=\""+ MoneySymbol + moneyFormat(fTotal)				+ "\">";
      sOutput += "<input type=hidden name=\""+OutputOrderShipping+"\" value=\""+ MoneySymbol + moneyFormat(fShipping)				+ "\">";
      sOutput += "<input type=hidden name=\""+OutputOrderTax     +"\" value=\""+ MoneySymbol + moneyFormat(fTax)				+ "\">";
      sOutput += "<input type=hidden name=\""+OutputOrderTotal   +"\" value=\""+ MoneySymbol + moneyFormat(fTotal + fShipping + fTax)		+ "\">";
      sOutput += "<input type=hidden name=\""+OutputOrderZone    +"\" value=\""+ ShipTable[ZoneSelected].zone+" "+sComputeShippingNote		+ "\">";
      sOutput += "<input type=hidden name=\""+OutputOrderRegion  +"\" value=\""+ (TaxRateRegional?RegionTable[RegionSelected]:"")		+ "\">";  //ER: new
   }
   DEBUG8(sOutput);
}
//------------------------------------------------------------------------
// FUNCTION: AddTaxSubtotalLines
// RECEIVES: INC string - is either empty-string or strINCLUDEDINTOTAL
// RETURNS: appends to global var sOutput
// ER: made this a function to improve maintainability.
//------------------------------------------------------------------------
function AddTaxSubtotalLines(INC,COL,BEG,END){  if(COL==null)COL=6;  if(BEG==null)BEG="<B>";  if(END==null)END="</B>";
   if(TaxNames.length>=2){
      if(fTaxP) sOutput += 
         "<TR><TD CLASS=\"noptotal\" COLSPAN="+COL+">"+BEG+strTAX+"-"+TaxNames[0]+INC+"&nbsp; "+(TaxRateRegional?RegionTable[RegionSelected]:"")+END+"</TD>" +
         "<TD CLASS=\"noptotal\" ALIGN=RIGHT>"+BEG + MoneySymbol + moneyFormat(fTaxP) +END+"</TD></TR>";
      if(fTaxG) sOutput += 
         "<TR><TD CLASS=\"noptotal\" COLSPAN="+COL+">"+BEG+strTAX+"-"+TaxNames[1]+INC+"&nbsp; "+(TaxRateRegional?RegionTable[RegionSelected]:"")+END+"</TD>" +
         "<TD CLASS=\"noptotal\" ALIGN=RIGHT>"+BEG + MoneySymbol + moneyFormat(fTaxG) +END+"</TD></TR>";
   }else{
      if(fTaxP+fTaxG) sOutput += 
         "<TR><TD CLASS=\"noptotal\" COLSPAN="+COL+">"+BEG+strTAX+INC+"&nbsp; "+(TaxRateRegional?RegionTable[RegionSelected]:"")+END+"</TD>" +
         "<TD CLASS=\"noptotal\" ALIGN=RIGHT>"+BEG + MoneySymbol + moneyFormat(fTaxP+fTaxG) +END+"</TD></TR>";
   }
}


//------------------------------------------------------------------------
// FUNCTION: ManageCart
// PARAMETERS: Null
// PURPOSE: Draw current cart product table on HTML page
//------------------------------------------------------------------------
function ManageCart() {
   var MoreState=GetCookie("MoreState"); if(MoreState==null) MoreState= (DisplayWtColumn?1:0)*2 + (DisplaySzColumn?1:0);	//ER: MoreState, for DynamicWtSzColumns
   //ER: replaced  fWeight-->PkgAsOne.weight;  sTotal-->moneyFormat(fTotal);  sTax-->moneyFormat(fTax);  sShipping-->moneyFormat(fShipping);
   //ER: iNumberOrdered, ZoneSelected, ZoneChecked, RegionSelected, RegionChecked  are now global vars set by ReadCartComputePrices routine;
   //ER: fTotal, fShipping, fTax, fTaxG, fTaxP, g_TotalCost                        are now global vars set by ReadCartComputePrices routine;
   //
   ReadCartComputePrices();	//ER: new
   sDescPP="";			//initialize the All-in-one-Description for cart-less payment-processors
   sOutput = "<TABLE CELLSPACING=0 CELLPADDING=2 BORDER=0 CLASS=\"nopcart\"><TR>" +
      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strILabel+"</B></TD>" +
      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strDLabel+"</B></TD>" +
      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strQLabel+"</B></TD>" +
      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strPLabel+"</B></TD>" +
      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+(MoreState&2?strWLabel:"&nbsp;")+"</B></TD>"+	//ER: was: (DisplayShippingColumn?"<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strSLabel+"</B></TD>":"") +
      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+(MoreState&1?strZLabel:"&nbsp;")+"</B></TD>"+	//ER: new
      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strRLabel+"</B></TD></TR>";
	  
	  
   if(iNumberOrdered==0)sOutput+="<TR><TD COLSPAN=7 CLASS=\"nopentry\"><CENTER><BR><B>"+strCartEmpty+"</B><BR><BR></CENTER></TD></TR>";	//ER: now subject to translation
   for(var i=1; i<=iNumberOrdered; ++i){
      var sCLASS="nopentry"; if(Math.round(i/2)==(i/2)) sCLASS="nopeven";		//ER: to eliminate duplication of code for even/odd background on rows
      sOutput += "<TR><TD CLASS=\""+sCLASS+"\" ALIGN=CENTER>" + Cart[i].ID_NUM + "</TD>";
      if(Cart[i].ADDTLINFO=="") sOutput += "<TD CLASS=\""+sCLASS+"\">" + Cart[i].NAME + "</TD>";
      else                      sOutput += "<TD CLASS=\""+sCLASS+"\">" + Cart[i].NAME + " - <I>"+ Cart[i].ADDTLINFO + "</I></TD>";
      if(DisplayChangeQty)      sOutput += "<TD CLASS=\""+sCLASS+"\" ALIGN=CENTER><INPUT TYPE=TEXT NAME=Q SIZE=2 VALUE=\"" + Cart[i].QUANTITY + "\" onChange=\"ChangeQuantity("+i+", this.value);\"></TD>";
      else                      sOutput += "<TD CLASS=\""+sCLASS+"\" ALIGN=CENTER>" + Cart[i].QUANTITY + "</TD>";
      sOutput += "<TD CLASS=\""+sCLASS+"\" ALIGN=RIGHT>"+ MoneySymbol + moneyFormat(Cart[i].PRICEAVG)+strEA+"</TD>";		//ER: "/ea" now subject to translation
      if(MoreState&2)           sOutput += "<TD CLASS=\""+sCLASS+"\" ALIGN=RIGHT>"+ Integer(Cart[i].WEIGHT)+WTUNITS+"</TD>";	//ER: display WEIGHT column (was "shipping" column)
      else                      sOutput += "<TD CLASS=\""+sCLASS+"\"></TD>";							//ER: N/A->empty-string
      if(MoreState&1)           sOutput += "<TD CLASS=\""+sCLASS+"\" ALIGN=RIGHT>&nbsp; "+ Cart[i].LENGTH+"x"+Cart[i].WIDTH+"x"+Cart[i].HEIGHT+SZUNITS + "</TD>";	//ER: new
      else                      sOutput += "<TD CLASS=\""+sCLASS+"\"></TD>";
      sOutput += "<TD CLASS=\""+sCLASS+"\" ALIGN=RIGHT>&nbsp; <input type=button value=\""+strRButton+"\" onClick=\"RemoveFromCart("+i+")\" class=\"nopbutton\"></TD>";	//ER: was align=CENTER
      sOutput += "</TR>";
	  
      //--ER: ManageCart producing PaymentProcessor-style hidden-fields is new (for ONE-step checkout);  the NOP-Design version only offered "cgi" style here
      AddPaymentProcessorFieldsForOneRow(PaymentProcessor, i);				//ER: add payment-processor form-fields for row-i
   }
   sOutput += "<TR><TD CLASS=\"noptotal\" COLSPAN=6><B>"+strSUB+"</B></TD>";
   sOutput += "<TD CLASS=\"noptotal\" COLSPAN=1 ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fTotal) + "</B></TD></TR>";
   if(DisplayPkgAttrRow && (PkgAsOne.weight+PkgAsOne.size.height) && iNumberOrdered){	//ER: new option controls this line showing + omit if weightless & sizeless
      var MoreLessButton=(MoreState==3?strLButton:strMButton);				//ER: initialize according to MoreState
      var bW=MoreState&2,  sW="&nbsp; " +PkgAsOne.weight+WTUNITS;			//ER: if WEIGHT/SIZE col being shown, show total in that col;  otherwise in 1st
      var bS=MoreState&1,  sS="&nbsp; " +SizeStr(PkgAsOne.size)+SZUNITS;
      sOutput += "<TR><TD CLASS=\"noptotal\" COLSPAN=4><B>"+strWTSZTOT+(bW?"":sW)+(bS?"":sS)+"</B></TD>";	//ER: moved this row, chged CLASS, etc
      sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + (bW?sW:"") +"</B></TD>";				//ER: show package weight
      sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + (bS?sS:"") +"</B></TD>";				//ER: show package size
      if(DynamicWtSzColumns)  sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT>&nbsp; <input type=button value=\""+MoreLessButton+"\" onClick=\"MoreLessInfo()\" class=\"nopbutton\"></TD>";	//ER: new
      else                    sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT></TD>";
      sOutput += "</TR>";
   }
   //Display the Shipping-Zone choices;  ER: Zone Defns are now table-driven; see description up front
   //ER: note: show no button as checked if user will be prompted for Zone (see the setting of ZoneChecked above)
   if(ShipTable.length>1 && (PkgAsOne.weight+PkgAsOne.size.height) && iNumberOrdered){	//ER: if more than one zone then customer must be able to pick
      sOutput += "<TR><TD CLASS=\"nopship\"><B>"+strSHIPPINGZONE+"</B></TD>";		//ER: was:ALIGN=CENTER  was:"UPS<BR>SHIPPING<BR>ZONE" now subject to translation
      sOutput += "<TD CLASS=\"nopship\" COLSPAN=6>";
      for(var z=0; z<ShipTable.length; z++) sOutput+= "<input type=radio name=\"ZONE\" value=\""+z+"\"" +
         (z==ZoneChecked?" checked":"") +						//ER: now adding the "checked" attrib here
         " onClick=\"NewZone(this.value)\">"+ShipTable[z].zone+"<br>";			//ER: ComputeShipping-->NewZone  in onClick
      sOutput += "</TD></TR>";
	  
   }
   if(DisplayShippingRow && (PkgAsOne.weight+PkgAsOne.size.height) && iNumberOrdered){
      sOutput += "<TR><TD CLASS=\"noptotal\" COLSPAN=6><B>" + strSHIP+"&nbsp; " + ShipTable[ZoneSelected].zone+"</B>&nbsp;&nbsp;" + sComputeShippingNote +"</TD>";
      sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fShipping) + "</B></TD></TR>";
   }
   //ER: have overhauled the way the Region-choices are shown & how changes are handled;  now very similar to the Zone-choices...
   //ER: note: RegionFromZoneOverrides means Region is always derived from Zone, so no user-choosing needed
   //ER: note: show no button as checked if user will be prompted for region (see the setting of RegionChecked above)
   if(TaxRateRegional!=0 && !RegionFromZoneOverrides && iNumberOrdered){		//ER: difference iNumberOrdered!=0 vs iNumberOrdered was a mystery==??==
      sOutput += "<TR><TD CLASS=\"nopship\"><B>"+strTAXABLEREGION+"</B></TD>";		//ER: was:ALIGN=CENTER
      sOutput += "<TD CLASS=\"nopship\" COLSPAN=6>";
      for(var R=0; R<RegionTable.length; R++) sOutput+= "<input type=radio name=\"TAX\" value=\""+R+"\"" +
         (R==RegionChecked?" checked":"") +						//ER: add the checked attrib
         " onClick=\"NewRegion(this.value)\">"+RegionTable[R]+"<br>";
      sOutput += "</TD></TR>";
	  	  
   }
   if(DisplayTaxRow && iNumberOrdered && !DisplayTaxIncluded){				//ER: show tax line(s) for NON-tax-included-pricing
      AddTaxSubtotalLines("");
   }
   sOutput += "<TR><TD CLASS=\"noptotal\" COLSPAN=6><B>"+strTOT+"</B></TD>";		//ER: now show the TOTAL line even when TaxRateRegional!=0
   sOutput += "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fTotal + fShipping + fTax) + "</B></TD></TR>";
   if(DisplayTaxRow && iNumberOrdered && DisplayTaxIncluded){				//ER: show tax line(s) for TAX-INCLUDED-pricing (new)
      AddTaxSubtotalLines(" "+strINCLUDEDINTOTAL, 6, "<i>", "</i>");
   }
   if(DisplayTaxRow && gVat && RegionSelected<=1 && ShipTaxName!="")  sOutput +	//ER: show tax included in the shipping-charge, to customer in same country
      "<TR><TD CLASS=\"noptotal\" COLSPAN=6><i>" + ShipTaxName +"</i></TD>" + 
      "<TD CLASS=\"noptotal\" ALIGN=RIGHT>" + MoneySymbol+moneyFormat(gVat) +"</TD></TR>";
   sOutput += "</TABLE>";

   //
   //--ER: ManageCart producing PaymentProcessor-style hidden-fields is new (to enable ONE-step checkout);  the NOP-Design version only offered "cgi" style here
   AddPaymentProcessorFieldsFinal(PaymentProcessor);					//ER: add the final (cart-wide) payment-processor fields
   document.write(sOutput);
   document.close();
}

//------------------------------------------------------------------------
// FUNCTION:	ValidateCart
// PARAMETERS:	Form to validate
// RETURNS:	true/false
// PURPOSE:	Validate the managecart form
//------------------------------------------------------------------------
function ValidateCart(theForm){
   if(isNaN(g_TotalCost)){
      alert(strTotalNaN);		//ER: was NoQtyPrompt
      return false;
   }
   if(g_TotalCost < MinimumOrder){
      alert(MinimumOrderPrompt);
      return false;
   }
   //ER: because of my defaults, now need to use the presence of cookies to tell whether user has made a Zone or Region selection;
   //ER: 1st test was: !RadioChecked(theForm.ZONE)
   //ER: 2nd test was: !RadioChecked(theForm.TAX)  -- actually it was: !RadioChecked(eval("theForm."+OutputOrderTax))  before simplifying OutputOrderTax-->"TAX"
   var N=Integer(GetCookie("NumberOrdered"));  if(N==0) return;		//skip the following if cart is empty;  only needed for the perverse use of MinimumOrder==0
   var ZoneCookie=   GetCookie("ZoneSelected");
   var RegionCookie= GetCookie("RegionSelected");
   if(ZoneCookie==null && (PkgAsOne.weight+PkgAsOne.size.height) && ShipTable.length>1 && ZonePrompt!=""){
      alert(ZonePrompt);
      return false;
   }
   if(RegionCookie==null && TaxRateRegional!=0 && RegionPrompt!="" && !RegionFromZoneOverrides && !(ZoneCookie && RegionFromZoneOvA[ZoneCookie])){
      alert(RegionPrompt);
      return false;
   }
   return true;
}

//------------------------------------------------------------------------
// FUNCTION: CheckoutCart
// PARAMETERS: Null
// PURPOSE: Draw current cart product table on HTML page for checkout;
// NOTE: produces a simpler view of the cart, compared to ManageCart, 
// and one without controls (Remove-from-Cart etc).
//------------------------------------------------------------------------
function CheckoutCart() {
   ReadCartComputePrices();	//ER: new
   sDescPP="";			//initialize the all-in-one-Description for cart-less payment-processors
   sOutput = "<TABLE CELLSPACING=0 CELLPADDING=2 BORDER=0 CLASS=\"nopcart\"><TR>" +
      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strILabel+"</B></TD>" +
      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strDLabel+"</B></TD>" +
      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strQLabel+"</B></TD>" +
      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strPLabel+"</B></TD>" +
      "<TD CLASS=\"nopheader\" ALIGN=CENTER><B>"+strALabel+"</B></TD></TR>";		//ER: strALabel now shown unconditionally
   for(var i=1; i<=iNumberOrdered; ++i){
      var sCLASS="nopentry"; if(Math.round(i/2)==(i/2)) sCLASS="nopeven";		//ER: to eliminate duplication of code for even/odd background on rows
      sOutput += "<TR><TD CLASS=\""+sCLASS+"\" ALIGN=CENTER>" + Cart[i].ID_NUM + "</TD>";
      if (Cart[i].ADDTLINFO=="") sOutput+= "<TD CLASS=\""+sCLASS+"\">" + Cart[i].NAME + "</TD>";
      else                       sOutput+= "<TD CLASS=\""+sCLASS+"\">" + Cart[i].NAME + " - <I>"+ Cart[i].ADDTLINFO + "</I></TD>";
      sOutput+="<TD CLASS=\""+sCLASS+"\" ALIGN=CENTER>" + Cart[i].QUANTITY + "</TD>";
      sOutput+="<TD CLASS=\""+sCLASS+"\" ALIGN=RIGHT>"+MoneySymbol+moneyFormat(Cart[i].PRICEAVG)+strEA+"</TD>";			//ER: "/ea" now subject to translation
      sOutput+="<TD CLASS=\""+sCLASS+"\" ALIGN=RIGHT>"+MoneySymbol+moneyFormat(Cart[i].QUANTITY*Cart[i].PRICEAVG)+"</TD></TR>";	//ER: now shown unconditionally
      AddPaymentProcessorFieldsForOneRow(PaymentProcessor2, i);				//ER: add payment-processor form-fields for row-i
   }
   sOutput+= "<TR><TD CLASS=\"noptotal\" COLSPAN=4><B>"+strSUB+"</B></TD>";
   sOutput+= "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fTotal) + "</B></TD></TR>";
   if(DisplayShippingRow){
      //sOutput+= "<TR><TD CLASS=\"noptotal\" COLSPAN=4><B>"+strWTSZTOT+"</B></TD>";
      //sOutput+= "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + fWeight + WTUNITS + "</B></TD>";
      sOutput+= "<TR><TD CLASS=\"noptotal\" COLSPAN=4><B>" + strSHIP+"&nbsp; "+ShipTable[ZoneSelected].zone+"</B></TD>";	//ER: removed "for"
      sOutput+= "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fShipping) + "</B></TD></TR>";
   }
   if(DisplayTaxRow && !DisplayTaxIncluded){						//ER: removed ||TaxRateRegional!=0;  now only for NON-tax-included pricing
      AddTaxSubtotalLines("", 4);
   }
   sOutput+= "<TR><TD CLASS=\"noptotal\" COLSPAN=4><B>"+strTOT+"</B></TD>";
   sOutput+= "<TD CLASS=\"noptotal\" ALIGN=RIGHT><B>" + MoneySymbol + moneyFormat(fTotal + fShipping + fTax) + "</B></TD></TR>";
   if(DisplayTaxRow && DisplayTaxIncluded){						//ER: show tax line(s) for TAX-INCLUDED-pricing (new)
      AddTaxSubtotalLines(" "+strINCLUDEDINTOTAL, 4, "<i>", "</i>");
   }
   if(DisplayTaxRow && gVat && RegionSelected<=1 && ShipTaxName!="")  sOutput +	//ER: show tax included in the shipping-charge, to customer in same country
      "<TR><TD CLASS=\"noptotal\" COLSPAN=4><i>" + ShipTaxName +"</i></TD>" + 
      "<TD CLASS=\"noptotal\" ALIGN=RIGHT>" + MoneySymbol+moneyFormat(gVat) +"</TD></TR>";
   sOutput+= "</TABLE>";
   AddPaymentProcessorFieldsFinal(PaymentProcessor2);					//ER: add the final (cart-wide) payment-processor fields
   document.write(sOutput);
   document.close();
}

//------------------------------------------------------------------------
// FUNCTION: Print_total
// PARAMETERS: none
// PURPOSE: Display cost currently racked up by shopper, on the HTML page
//------------------------------------------------------------------------
function Print_total(){
   ReadCartComputePrices();	//ER: new
   document.write(moneyFormat(fTotal));
}
//------------------------------------------------------------------------
// FUNCTION: Print_total_products
// PARAMETER: true/false - true to get "item"/"items" appended;  use false in any non-English application
// PURPOSE: Display total number of items currently racked up by shopper, on the HTML page
//------------------------------------------------------------------------
function Print_total_products(Verbose){
   ReadCartComputePrices();
   sOutput= "" + g_TotalQty;
   if(Verbose) sOutput+= (g_TotalQty==1?" item":" items");
   document.write(sOutput);
}
//------------------------------------------------------------------------
// FUNCTION: Cart_is_empty
// RETURNS: true/false
//------------------------------------------------------------------------
function Cart_is_empty(){
   iNumberOrdered=Integer(GetCookie("NumberOrdered"));      //get the nbr-rows-in-cart cookie
   return iNumberOrdered==0;
}
//========================================================================
// END NOP-Design + ER Shopping-Cart
//========================================================================

