New files after 2.1RC merge
authorJanusz Dobrowolski <janusz@frontaccounting.eu>
Mon, 9 Mar 2009 19:45:04 +0000 (19:45 +0000)
committerJanusz Dobrowolski <janusz@frontaccounting.eu>
Mon, 9 Mar 2009 19:45:04 +0000 (19:45 +0000)
106 files changed:
.htaccess [new file with mode: 0644]
admin/attachments.php [new file with mode: 0644]
admin/db/printers_db.inc [new file with mode: 0644]
admin/inst_upgrade.php [new file with mode: 0644]
admin/print_profiles.php [new file with mode: 0644]
admin/printers.php [new file with mode: 0644]
doc/2.1_Beta.txt [new file with mode: 0644]
doc/attachments.txt [new file with mode: 0644]
doc/bank_reconciliation.txt [new file with mode: 0644]
doc/dim_on_invoice.txt [new file with mode: 0644]
doc/extensions.txt [new file with mode: 0644]
doc/license.txt [new file with mode: 0644]
doc/quick_entries.txt [new file with mode: 0644]
doc/recurrent_invoice.txt [new file with mode: 0644]
gl/bank_account_reconcile.php [new file with mode: 0644]
gl/inquiry/tax_inquiry.php [new file with mode: 0644]
gl/manage/gl_quick_entries.php [new file with mode: 0644]
includes/db_pager.inc [new file with mode: 0644]
includes/ui/db_pager_view.inc [new file with mode: 0644]
installed_extensions.php [new file with mode: 0644]
inventory/includes/db/items_codes_db.inc [new file with mode: 0644]
inventory/manage/item_codes.php [new file with mode: 0644]
inventory/manage/sales_kits.php [new file with mode: 0644]
js/reconcile.js [new file with mode: 0644]
lang/new_language_template/locale.inc [new file with mode: 0644]
reporting/includes/Workbook.php [new file with mode: 0644]
reporting/includes/excel_report.inc [new file with mode: 0644]
reporting/includes/printer_class.inc [new file with mode: 0644]
reporting/prn_redirect.php [new file with mode: 0644]
sales/create_recurrent_invoices.php [new file with mode: 0644]
sales/includes/db/sales_points_db.inc [new file with mode: 0644]
sales/manage/recurrent_invoices.php [new file with mode: 0644]
sales/manage/sales_groups.php [new file with mode: 0644]
sales/manage/sales_points.php [new file with mode: 0644]
sql/alter2.1.php [new file with mode: 0644]
sql/alter2.1.sql [new file with mode: 0644]
themes/aqua/images/add.png [new file with mode: 0644]
themes/aqua/images/ajax-loader.gif [new file with mode: 0644]
themes/aqua/images/cancel.png [new file with mode: 0644]
themes/aqua/images/credit.gif [new file with mode: 0644]
themes/aqua/images/delete.gif [new file with mode: 0644]
themes/aqua/images/download.gif [new file with mode: 0644]
themes/aqua/images/edit.gif [new file with mode: 0644]
themes/aqua/images/gl.png [new file with mode: 0644]
themes/aqua/images/help.gif [new file with mode: 0644]
themes/aqua/images/invoice.gif [new file with mode: 0644]
themes/aqua/images/login.gif [new file with mode: 0644]
themes/aqua/images/money.png [new file with mode: 0644]
themes/aqua/images/ok.gif [new file with mode: 0644]
themes/aqua/images/pdf.gif [new file with mode: 0644]
themes/aqua/images/print.png [new file with mode: 0644]
themes/aqua/images/receive.gif [new file with mode: 0644]
themes/aqua/images/remove.png [new file with mode: 0644]
themes/aqua/images/report.png [new file with mode: 0644]
themes/aqua/images/right.gif [new file with mode: 0644]
themes/aqua/images/sort_asc.gif [new file with mode: 0644]
themes/aqua/images/sort_desc.gif [new file with mode: 0644]
themes/aqua/images/sort_none.gif [new file with mode: 0644]
themes/aqua/images/view.gif [new file with mode: 0644]
themes/cool/images/add.png [new file with mode: 0644]
themes/cool/images/ajax-loader.gif [new file with mode: 0644]
themes/cool/images/cancel.png [new file with mode: 0644]
themes/cool/images/credit.gif [new file with mode: 0644]
themes/cool/images/delete.gif [new file with mode: 0644]
themes/cool/images/download.gif [new file with mode: 0644]
themes/cool/images/edit.gif [new file with mode: 0644]
themes/cool/images/gl.png [new file with mode: 0644]
themes/cool/images/help.gif [new file with mode: 0644]
themes/cool/images/invoice.gif [new file with mode: 0644]
themes/cool/images/login.gif [new file with mode: 0644]
themes/cool/images/money.png [new file with mode: 0644]
themes/cool/images/ok.gif [new file with mode: 0644]
themes/cool/images/pdf.gif [new file with mode: 0644]
themes/cool/images/print.png [new file with mode: 0644]
themes/cool/images/receive.gif [new file with mode: 0644]
themes/cool/images/remove.png [new file with mode: 0644]
themes/cool/images/report.png [new file with mode: 0644]
themes/cool/images/right.gif [new file with mode: 0644]
themes/cool/images/sort_asc.gif [new file with mode: 0644]
themes/cool/images/sort_desc.gif [new file with mode: 0644]
themes/cool/images/sort_none.gif [new file with mode: 0644]
themes/cool/images/view.gif [new file with mode: 0644]
themes/default/images/add.png [new file with mode: 0644]
themes/default/images/ajax-loader.gif [new file with mode: 0644]
themes/default/images/cancel.png [new file with mode: 0644]
themes/default/images/credit.gif [new file with mode: 0644]
themes/default/images/delete.gif [new file with mode: 0644]
themes/default/images/download.gif [new file with mode: 0644]
themes/default/images/edit.gif [new file with mode: 0644]
themes/default/images/escape.png [new file with mode: 0644]
themes/default/images/gl.png [new file with mode: 0644]
themes/default/images/help.gif [new file with mode: 0644]
themes/default/images/invoice.gif [new file with mode: 0644]
themes/default/images/login.gif [new file with mode: 0644]
themes/default/images/money.png [new file with mode: 0644]
themes/default/images/ok.gif [new file with mode: 0644]
themes/default/images/pdf.gif [new file with mode: 0644]
themes/default/images/print.png [new file with mode: 0644]
themes/default/images/receive.gif [new file with mode: 0644]
themes/default/images/remove.png [new file with mode: 0644]
themes/default/images/report.png [new file with mode: 0644]
themes/default/images/right.gif [new file with mode: 0644]
themes/default/images/sort_asc.gif [new file with mode: 0644]
themes/default/images/sort_desc.gif [new file with mode: 0644]
themes/default/images/sort_none.gif [new file with mode: 0644]
themes/default/images/view.gif [new file with mode: 0644]

diff --git a/.htaccess b/.htaccess
new file mode 100644 (file)
index 0000000..b923b02
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,11 @@
+# These settings are recommended
+# Maybe you might have problems
+# with other scripts that needs
+# register_globals ON
+php_flag magic_quotes_gpc Off
+php_flag register_globals Off
+
+#Sometimes neccessary to add those
+#
+#AddType application/x-javascript .js
+#AddType text/css .css
diff --git a/admin/attachments.php b/admin/attachments.php
new file mode 100644 (file)
index 0000000..8f517e8
--- /dev/null
@@ -0,0 +1,248 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+$path_to_root="..";
+$page_security = 8;
+
+include_once($path_to_root . "/includes/session.inc");
+
+include_once($path_to_root . "/includes/date_functions.inc");
+include_once($path_to_root . "/includes/ui.inc");
+include_once($path_to_root . "/includes/data_checks.inc");
+
+$view_id = find_submit('view');
+if ($view_id != -1)
+{
+       $row = get_attachment($view_id);
+       if ($row['filename'] != "")
+       {
+               $type = ($row['filetype']) ? $row['filetype'] : 'application/octet-stream';     
+       header("Content-type: ".$type);
+       header('Content-Length: '.$row['filesize']);
+       if ($type == 'application/octet-stream')
+               header('Content-Disposition: attachment; filename='.$row['filename']);
+       else
+                       header("Content-Disposition: inline");
+       echo file_get_contents($comp_path."/".user_company(). "/attachments/".$row['unique_name']);
+       exit();
+       }       
+}
+
+$download_id = find_submit('download');
+if ($download_id != -1)
+{
+       $row = get_attachment($download_id);
+       if ($row['filename'] != "")
+       {
+               $type = ($row['filetype']) ? $row['filetype'] : 'application/octet-stream';     
+       header("Content-type: ".$type);
+       header('Content-Length: '.$row['filesize']);
+       header('Content-Disposition: attachment; filename='.$row['filename']);
+       echo file_get_contents($comp_path."/".user_company(). "/attachments/".$row['unique_name']);
+       exit();
+       }       
+}
+
+$js = "";
+if ($use_popup_windows)
+       $js .= get_js_open_window(800, 500);
+page(_("Attach Documents"), false, false, "", $js);
+
+simple_page_mode(true);
+//----------------------------------------------------------------------------------------
+if (isset($_GET['filterType'])) // catch up external links
+       $_POST['filterType'] = $_GET['filterType'];
+if (isset($_GET['trans_no']))
+       $_POST['trans_no'] = $_GET['trans_no'];
+       
+if ($Mode == 'ADD_ITEM' || $Mode == 'UPDATE_ITEM')
+{
+       if (isset($_FILES['filename']) && $_FILES['filename']['size'] > 0)
+       {
+               //$content = base64_encode(file_get_contents($_FILES['filename']['tmp_name']));
+               $tmpname = $_FILES['filename']['tmp_name'];
+
+               $dir =  $comp_path."/".user_company(). "/attachments";
+               if (!file_exists($dir))
+               {
+                       mkdir ($dir,0777);
+                       $index_file = "<?php\nheader(\"Location: ../index.php\");\n?>";
+                       $fp = fopen($dir."/index.php", "w");
+                       fwrite($fp, $index_file);
+                       fclose($fp);
+               }
+               if ($Mode == 'UPDATE_ITEM' && file_exists($dir."/".$_POST['unique_name']))
+                       unlink($dir."/".$_POST['unique_name']);
+
+               $unique_name = uniqid('');
+               move_uploaded_file($tmpname, $dir."/".$unique_name);
+               //save the file
+               $filename = $_FILES['filename']['name'];
+               $filesize = $_FILES['filename']['size'];
+               $filetype = $_FILES['filename']['type'];
+       }
+       else
+       {
+               $unique_name = $filename = $filetype = "";
+               $filesize = 0;
+       }
+       $date = date2sql(Today());
+       if ($Mode == 'ADD_ITEM')
+       {
+               $sql = "INSERT INTO ".TB_PREF."attachments (type_no, trans_no, description, filename, unique_name,
+                       filesize, filetype, tran_date) VALUES (".$_POST['filterType'].",".$_POST['trans_no'].",".
+                       db_escape($_POST['description']).", '$filename', '$unique_name', '$filesize', '$filetype', '$date')";
+               db_query($sql, "Attachment could not be inserted");             
+               display_notification(_("Attachment has been inserted.")); 
+       }
+       else
+       {
+               $sql = "UPDATE ".TB_PREF."attachments SET
+                       type_no=".$_POST['filterType'].",
+                       trans_no=".$_POST['trans_no'].",
+                       description=".db_escape($_POST['description']).", ";
+               if ($filename != "")
+               {
+                       $sql .= "filename='$filename',
+                       unique_name='$unique_name',
+                       filesize='$filesize',
+                       filetype='$filetype', ";
+               }       
+               $sql .= "tran_date='$date' WHERE id=$selected_id";
+               db_query($sql, "Attachment could not be updated");              
+               display_notification(_("Attachment has been updated.")); 
+       }
+       $Mode = 'RESET';
+}              
+
+if ($Mode == 'Delete')
+{
+       $row = get_attachment($selected_id);
+       $dir =  $comp_path."/".user_company(). "/attachments";
+       if (file_exists($dir."/".$row['unique_name']))
+               unlink($dir."/".$row['unique_name']);
+       $sql = "DELETE FROM ".TB_PREF."attachments WHERE id = $selected_id";
+       db_query($sql, "Could not delete attachment");
+       display_notification(_("Attachment has been deleted.")); 
+       $Mode = 'RESET';
+}
+
+if ($Mode == 'RESET')
+{
+       unset($_POST['trans_no']);
+       unset($_POST['description']);
+       $selected_id = -1;
+}
+
+function viewing_controls()
+{
+    start_form(false, true);
+
+    start_table("class='tablestyle_noborder'");
+
+       systypes_list_row(_("Type:"), 'filterType', null, true);
+
+    end_table(1);
+
+       end_form();
+}
+
+//----------------------------------------------------------------------------------------
+
+function get_attached_documents($type)
+{
+       $sql = "SELECT * FROM ".TB_PREF."attachments WHERE type_no=$type ORDER BY trans_no";
+       return db_query($sql, "Could not retrieve attachments");
+}
+
+function get_attachment($id)
+{
+       $sql = "SELECT * FROM ".TB_PREF."attachments WHERE id=$id";
+       $result = db_query($sql, "Could not retrieve attachments");
+       return db_fetch($result);
+}
+
+function display_rows($type)
+{
+       global $table_style;
+
+       $rows = get_attached_documents($type);
+       $th = array(_("#"), _("Description"), _("Filename"), _("Size"), _("Filetype"), _("Date Uploaded"), "", "", "", "");
+       
+       div_start('transactions');
+       start_form();
+       start_table($table_style);
+       table_header($th);
+       $k = 0;
+       while ($row = db_fetch($rows))
+       {
+               alt_table_row_color($k);
+               
+               label_cell(get_trans_view_str($type, $row['trans_no']));
+               label_cell($row['description']);
+               label_cell($row['filename']);
+               label_cell($row['filesize']);
+               label_cell($row['filetype']);
+               label_cell(sql2date($row['tran_date']));
+               edit_button_cell("Edit".$row['id'], _("Edit"));
+               button_cell("view".$row['id'], _("View"), false, ICON_VIEW);
+               button_cell("download".$row['id'], _("Download"), false, ICON_DOWN);
+               delete_button_cell("Delete".$row['id'], _("Delete"));
+       end_row();
+       }       
+       end_table(1);
+       hidden('filterType', $type);
+       end_form();
+       div_end();
+}
+
+//----------------------------------------------------------------------------------------
+
+viewing_controls();
+
+if (isset($_POST['filterType']))
+       display_rows($_POST['filterType']);
+
+start_form(true);
+
+start_table($table_style2);
+
+if ($selected_id != -1)
+{
+       if ($Mode == 'Edit')
+       {
+               $row = get_attachment($selected_id);
+               $_POST['trans_no']  = $row["trans_no"];
+               $_POST['description']  = $row["description"];
+               hidden('trans_no', $row['trans_no']);
+               hidden('unique_name', $row['unique_name']);
+               label_row(_("Transaction #"), $row['trans_no']);
+       }       
+       hidden('selected_id', $selected_id);
+}
+else
+       text_row_ex(_("Transaction #").':', 'trans_no', 10);
+text_row_ex(_("Description").':', 'description', 40);
+start_row();
+label_cells(_("Attached File") . ":", "<input type='file' id='filename' name='filename'>");
+end_row();
+
+end_table(1);
+if (isset($_POST['filterType']))
+       hidden('filterType', $_POST['filterType']);
+
+submit_add_or_update_center($selected_id == -1, '', true);
+
+end_form();
+
+end_page();
+
+?>
diff --git a/admin/db/printers_db.inc b/admin/db/printers_db.inc
new file mode 100644 (file)
index 0000000..f605f65
--- /dev/null
@@ -0,0 +1,104 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+
+function write_printer_def($id, $name, $descr, $queue, $host, $port, $timeout)
+{
+       if ($id>0)
+               $sql = "UPDATE ".TB_PREF."printers SET description=".db_escape($descr)
+               .",name=".db_escape($name).",queue=".db_escape($queue)
+               .",host=".db_escape($host).",port='$port',timeout='$timeout' "
+               ."WHERE id=$id";
+       else 
+               $sql = "INSERT INTO ".TB_PREF."printers ("
+                       ."name,description,queue,host,port,timeout) "
+                       ."VALUES (".db_escape($name).",".db_escape($descr).","
+                       .db_escape($queue).",".db_escape($host).",'$port','$timeout')";
+
+       return db_query($sql,"could not write printer definition");
+}
+
+function get_all_printers() 
+{
+       $sql = "SELECT * FROM ".TB_PREF."printers";
+       return db_query($sql,"could not get printer definitions");
+}
+
+function get_printer($id)
+{
+               $sql = "SELECT * FROM ".TB_PREF."printers
+                       WHERE id=$id";
+
+               $result = db_query($sql,"could not get printer definition");
+               return  db_fetch($result);
+}
+
+//============================================================================
+// printer profiles functions
+//
+function update_printer_profile($name, $dest)
+{
+       foreach( $dest as $rep => $printer) {
+               if ($printer != '' || $rep == '') {
+                       $sql = "REPLACE INTO ".TB_PREF."print_profiles "
+                       ."(profile, report, printer) VALUES ("
+                       .db_escape($name).","
+                       .db_escape($rep).","
+                       .db_escape($printer).")";
+               } else {
+                       $sql = "DELETE FROM ".TB_PREF."print_profiles WHERE ("
+                               ."report=" . db_escape($rep)
+                               ." AND profile=".db_escape($name).")";
+               }
+               $result = db_query($sql,"could not update printing profile");
+               if(!$result) {
+                       return false;
+               }
+       }
+       return true;
+}
+//
+//     Get destination for report defined in given printing profile.
+//
+function get_report_printer($profile, $report)
+{
+       $sql = "SELECT printer FROM ".TB_PREF."print_profiles WHERE "
+               ."profile=".db_escape($profile)." AND report=";
+
+       $result = db_query($sql.db_escape($report), 'report printer lookup failed');
+
+       if (!$result) return false;
+       $ret = db_fetch($result);
+       if ($ret === false) {
+               $result = db_query($sql."''", 'default report printer lookup failed');
+               if (!$result) return false;
+
+               $ret = db_fetch($result);
+               if (!$ret) return false;
+       }
+       return get_printer($ret['printer']);
+}
+
+function delete_printer_profile($name)
+{
+       $sql="DELETE FROM ".TB_PREF."print_profiles WHERE profile=".db_escape($name);
+       return db_query($sql,"could not delete printing profile");
+}
+//
+// Get all report destinations for given profile.
+//
+function get_print_profile($name)
+{
+       $sql = "SELECT  * FROM ".TB_PREF."print_profiles WHERE profile=".db_escape($name);
+       return db_query($sql,"could not get printing profile");
+}
+
+?>
\ No newline at end of file
diff --git a/admin/inst_upgrade.php b/admin/inst_upgrade.php
new file mode 100644 (file)
index 0000000..4a5106b
--- /dev/null
@@ -0,0 +1,199 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+$page_security = 20;
+$path_to_root="..";
+include_once($path_to_root . "/includes/session.inc");
+
+page(_("Software Upgrade"));
+
+include_once($path_to_root . "/includes/date_functions.inc");
+include_once($path_to_root . "/admin/db/company_db.inc");
+include_once($path_to_root . "/admin/db/maintenance_db.inc");
+include_once($path_to_root . "/includes/ui.inc");
+
+//
+//     Checks $field existence in $table with given field $properties
+//     $table - table name without prefix
+//  $field -  optional field name
+//  $properties - optional properties of field defined by MySQL:
+//             'Type', 'Null', 'Key', 'Default', 'Extra'
+//
+function check_table($pref, $table, $field=null, $properties=null)
+{
+       $fields = db_query("SHOW COLUMNS FROM ".$pref.$table);
+       if (!$fields)
+               return 1;               // no such table or error
+
+       if (!isset($field)) 
+               return 0;               // table exists
+
+       while( $row = db_fetch_assoc($fields)) 
+       {
+               if ($row['Field'] == $field) 
+               {
+                       if (!isset($properties)) 
+                               return 0;
+                       foreach($properties as $property => $value) 
+                       {
+                               if ($row[$property] != $value) 
+                                       return 3;       // failed type/length check
+                       }
+                       return 0; // property check ok.
+               }
+       }
+       return 2; // field not found
+}
+//
+//     Creates table of installer objects sorted by version.
+//
+function get_installers()
+{
+       global $path_to_root;
+
+       $patchdir = $path_to_root."/sql/";
+       $upgrades = array();    
+       $datadir = @opendir($patchdir);
+
+       if ($datadir)
+       {
+               while(false !== ($fname = readdir($datadir)))
+               { // check all php files but index.php
+                       if (!is_dir($patchdir . $fname) && ($fname != 'index.php')
+                               && stristr($fname, '.php') != false)
+                       {
+                               unset($install);
+                               include_once($patchdir . $fname);
+                               if (isset($install)) // add installer if found
+                                       $upgrades[$install->version] =  $install;
+                       }
+               }
+               ksort($upgrades); // sort by file name
+               $upgrades = array_values($upgrades);
+       }
+       return $upgrades;
+}
+//
+//     Apply one differential data set.
+//
+function upgrade_step($index, $conn) 
+{
+       global $path_to_root, $installers;
+
+       $inst = $installers[$index];
+       $sql = $inst->sql;
+       $pref = $conn['tbpref'];
+       $ret = true;
+
+       $force = get_post('force_'.$index);
+       if ($force || get_post('install_'.$index)) 
+       {
+               if (!$inst->installed($pref) || $force) 
+               {
+                       if (!$inst->pre_check($pref)) return false;
+
+                       if ($sql != '')
+                               $ret &= db_import($path_to_root.'/sql/'.$sql, $conn, $force);
+
+                       $ret &= $inst->install($pref, $force);
+               }
+       }
+       return $ret;
+}
+
+function db_open($conn)
+{
+       $db = mysql_connect($conn["host"] ,$conn["dbuser"], $conn["dbpassword"]);
+       if (!$db)
+               return false;
+       if (!mysql_select_db($conn["dbname"], $db))
+               return false;
+       return $db;
+}
+
+$installers = get_installers();
+
+if (get_post('Upgrade')) 
+{
+
+       $ret = true;
+       foreach ($db_connections as $conn) 
+       {
+       // connect to database
+               if (!($db = db_open($conn))) 
+               {
+                       display_error(_("Cannot connect to database for company")
+                               ." '".$conn['name']."'");
+                       continue;
+               }
+       // create security backup               
+               if ($conn['tbpref'] != "")
+                       $filename = $conn['dbname'] . "_" . $conn['tbpref'] . date("Ymd_Hi") . ".sql";
+               else
+                       $filename = $conn['dbname'] . "_" . date("Ymd_Hi") . ".sql";
+
+               db_export($conn, $filename, 'no', 'Security backup before upgrade', $conn['tbpref']);
+       // apply all upgrade data
+               foreach ($installers as $i => $inst) 
+               {
+                       $ret = upgrade_step($i, $conn);
+                       if (!$ret)
+                               display_error(
+                               sprintf(_("Database upgrade to version %s failed for company '%s'."),
+                                       $inst->version, $conn['name'])
+                                       .'<br>'
+                                       ._('You should restore company database from latest backup file'));
+               }
+//             db_close($conn); ?
+               if (!$ret) break;
+       }
+       if($ret)
+       {       // re-read the prefs
+               global $path_to_root;
+               include_once($path_to_root . "/admin/db/users_db.inc");
+               $user = get_user($_SESSION["wa_current_user"]->username);
+               $_SESSION["wa_current_user"]->prefs = new user_prefs($user);
+               display_notification(_('All companies data has been successfully updated'));
+       }       
+       $Ajax->activate('_page_body');
+}
+
+start_form();
+start_table($table_style);
+$th = array(_("Version"), _("Description"), _("Sql file"), _("Install"),
+       _("Force upgrade"));
+table_header($th);
+
+$k = 0; //row colour counter
+foreach($installers as $i => $inst)
+{
+       alt_table_row_color($k);
+       start_row();
+       label_cell($inst->version);
+       label_cell($inst->description);
+       label_cell($inst->sql ? $inst->sql : '<i>'._('None').'</i>', 'align=center');
+// this is checked only for first (site admin) company, 
+// but in fact we should always upgrade all data sets after
+// source upgrade.
+       if ($inst->installed(TB_PREF))
+               label_cell(_("Installed"));
+       else
+               check_cells(null,'install_'.$i, 0);
+       check_cells(null,'force_'.$i, 0);
+       end_row();
+}
+end_table(1);
+submit_center('Upgrade', _('Upgrade system'), true, _('Save database and perform upgrade'), 'process');
+end_form();
+
+end_page();
+
+?>
\ No newline at end of file
diff --git a/admin/print_profiles.php b/admin/print_profiles.php
new file mode 100644 (file)
index 0000000..8052a38
--- /dev/null
@@ -0,0 +1,189 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+$page_security = 15;
+$path_to_root="..";
+include($path_to_root . "/includes/session.inc");
+include($path_to_root . "/admin/db/printers_db.inc");
+include($path_to_root . "/includes/ui.inc");
+
+page(_("Printing Profiles"));
+
+$selected_id = get_post('profile_id','');
+
+//-------------------------------------------------------------------------------------------------
+// Returns array of defined reports
+//
+function get_reports() {
+       global $path_to_root, $comp_path, $go_debug;
+
+if ($go_debug || !isset($_SESSION['reports'])) {       
+       // to save time, store in session.
+               $paths = array (
+                       $path_to_root.'/reporting/',
+                       $comp_path .'/'. user_company() . '/reporting/');
+               $reports = array( '' => _('Default printing destination'));
+
+       foreach($paths as $dirno => $path) {
+               $repdir = opendir($path);
+               while(false !== ($fname = readdir($repdir)))
+               {
+               // reports have filenames in form rep(repid).php 
+               // where repid must contain at least one digit (reports_main.php is not ;)
+                       if (is_file($path.$fname) 
+//                             && preg_match('/.*[^0-9]([0-9]+)[.]php/', $fname, $match))
+                               && preg_match('/rep(.*[0-9]+.*)[.]php/', $fname, $match))
+                       {
+                               $repno = $match[1];
+                               $title = '';
+
+                               $line = file_get_contents($path.$fname);
+                               if (preg_match('/.*(FrontReport\()\s*_\([\'"]([^\'"]*)/', $line, $match)) {
+                                       $title = trim($match[2]);
+                               }
+                               else // for any 3rd party printouts without FrontReport() class use
+                                       if (preg_match('/.*(\$Title).*[\'"](.*)[\'"].+/', $line, $match)) {
+                                               $title = trim($match[2]);
+                                       }
+                               $reports[$repno] = $title;
+                       }
+               }
+       closedir();
+       }
+               ksort($reports);
+               $_SESSION['reports'] = $reports;
+       }
+       return $_SESSION['reports'];
+}
+
+function clear_form() 
+{
+       global $selected_id, $Ajax;
+
+       $selected_id = '';
+       $_POST['name'] = '';
+       $Ajax->activate('_page_body');
+}
+
+function check_delete($name)
+{
+// check if selected profile is used by any user
+       if ($name=='') return 0; // cannot delete system default profile
+       $sql = "SELECT * FROM ".TB_PREF."users WHERE print_profile='$name'";
+       $res = db_query($sql,'cannot check printing profile usage');
+       return db_num_rows($res);
+}
+//-------------------------------------------------------------------------------------------
+if ( get_post('submit'))
+{
+
+       $error = 0;
+
+       if ($_POST['profile_id'] == '' && empty($_POST['name']))
+       {
+               $error = 1;
+               display_error( _("Printing profile name cannot be empty."));
+               set_focus('name');
+       } 
+
+       if (!$error)
+       {
+               $prof = array('' => get_post('Prn')); // store default value/profile name
+               foreach (get_reports() as $rep => $descr) {
+                       $val = get_post('Prn'.$rep);
+                       $prof[$rep] = $val;
+               }
+               if ($_POST['profile_id']=='')
+               $_POST['profile_id'] = get_post('name');
+               
+               update_printer_profile($_POST['profile_id'], $prof);
+               if ($selected_id == '') {
+                       display_notification_centered(_('New printing profile has been created')); 
+                       clear_form();
+               } else {
+                       display_notification_centered(_('Printing profile has been updated'));
+               }
+       }
+}
+
+if(get_post('delete'))
+{
+ if (!check_delete(get_post('name'))) {
+       delete_printer_profile($selected_id);
+       display_notification(_('Selected printing profile has been deleted'));
+       clear_form();
+ }
+}
+
+if(get_post('_profile_id_update')) {
+       $Ajax->activate('_page_body');
+}
+
+start_form();
+start_table();
+print_profiles_list_row(_('Select printing profile'). ':', 'profile_id', null,
+       _('New printing profile'), true);
+end_table();
+echo '<hr>';
+start_table();
+if (get_post('profile_id') == '')
+       text_row(_("Printing Profile Name").':', 'name', null, 30, 30);
+else
+       label_cells(_("Printing Profile Name").':', get_post('profile_id'));
+end_table(1);
+
+$result = get_print_profile(get_post('profile_id'));
+$prints = array();
+while ($myrow = db_fetch($result)) {
+       $prints[$myrow['report']] = $myrow['printer'];
+}
+
+start_table($table_style);
+$th = array(_("Report Id"), _("Description"), _("Printer"));
+table_header($th);
+
+$k = 0;
+$unkn = 0;
+foreach(get_reports() as $rep => $descr)
+{
+       alt_table_row_color($k);
+
+    label_cell($rep=='' ? '-' : $rep, 'align=center');
+    label_cell($descr == '' ? '???<sup>1)</sup>' : _($descr));
+       $_POST['Prn'.$rep] = isset($prints[$rep]) ? $prints[$rep] : '';
+    echo '<td>';
+       printers_list('Prn'.$rep, null, 
+               $rep == '' ? _('Browser support') : _('Default'));
+       echo '</td>';
+       if ($descr == '') $unkn = 1;
+    end_row();
+}
+end_table();
+if ($unkn)
+       display_note('<sup>1)</sup>&nbsp;-&nbsp;'._("no title was found in this report definition file."), 0, 1, '');
+else
+       echo '<br>';
+
+div_start('controls');
+if (get_post('profile_id') == '') {
+       submit_center('submit', _("Add New Profile"), true, '', true);
+} else {
+       submit_center_first('submit', _("Update Profile"), 
+         _('Update printer profile'), true);
+       submit_center_last('delete', _("Delete Profile"), 
+         _('Delete printer profile (only if not used by any user)'), true);
+}
+div_end();
+
+end_form();
+end_page();
+
+?>
diff --git a/admin/printers.php b/admin/printers.php
new file mode 100644 (file)
index 0000000..227c1d6
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+$page_security = 15;
+$path_to_root="..";
+include($path_to_root . "/includes/session.inc");
+
+page(_("Printer Locations"));
+
+include($path_to_root . "/admin/db/printers_db.inc");
+include($path_to_root . "/includes/ui.inc");
+
+simple_page_mode(true);
+//-------------------------------------------------------------------------------------------
+if ($Mode=='ADD_ITEM' || $Mode=='UPDATE_ITEM') 
+{
+
+       $error = 0;
+
+       if (empty($_POST['name']))
+       {
+               $error = 1;
+               display_error( _("Printer name cannot be empty."));
+               set_focus('name');
+       } 
+       elseif (empty($_POST['host'])) 
+       {
+               display_notification_centered( _("You have selected printing to server at user IP."));
+       } 
+       elseif (!check_num('tout', 0, 60)) 
+       {
+               $error = 1;
+               display_error( _("Timeout cannot be less than zero nor longer than 60 (sec)."));
+               set_focus('tout');
+       } 
+
+       if ($error != 1)
+       {
+               write_printer_def($selected_id, get_post('name'), get_post('descr'),
+                       get_post('queue'), get_post('host'), input_num('port',0),
+                       input_num('tout',0));
+
+               display_notification_centered($selected_id==-1? 
+                       _('New printer definition has been created') 
+                       :_('Selected printer definition has been updated'));
+               $Mode = 'RESET';
+       }
+}
+
+if ($Mode == 'Delete')
+{
+       // PREVENT DELETES IF DEPENDENT RECORDS IN print_profiles
+
+       $sql= "SELECT COUNT(*) FROM ".TB_PREF."print_profiles WHERE printer = '$selected_id'";
+       $result = db_query($sql,"check printers relations failed");
+       $myrow = db_fetch_row($result);
+       if ($myrow[0] > 0) 
+       {
+               display_error(_("Cannot delete this printer definition, because print profile have been created using it."));
+       } 
+       else 
+       {
+                       $sql="DELETE FROM ".TB_PREF."printers WHERE id='$selected_id'";
+                       db_query($sql,"could not delete printer definition");
+                       display_notification(_('Selected printer definition has been deleted'));
+       }
+       $Mode = 'RESET';
+}
+
+if ($Mode == 'RESET')
+{
+       $selected_id = -1;
+       unset($_POST);
+}
+//-------------------------------------------------------------------------------------------------
+
+$result = get_all_printers();
+start_form();
+start_table($table_style);
+$th = array(_("Name"), _("Description"), _("Host"), _("Printer Queue"),'','');
+table_header($th);
+
+$k = 0; //row colour counter
+while ($myrow = db_fetch($result)) 
+{
+       alt_table_row_color($k);
+
+    label_cell($myrow['name']);
+    label_cell($myrow['description']);
+    label_cell($myrow['host']);
+    label_cell($myrow['queue']);
+       edit_button_cell("Edit".$myrow['id'], _("Edit"));
+       delete_button_cell("Delete".$myrow['id'], _("Delete"));
+    end_row();
+
+
+} //END WHILE LIST LOOP
+
+end_table();
+end_form();
+echo '<br>';
+
+//-------------------------------------------------------------------------------------------------
+
+start_form();
+
+start_table($table_style2);
+
+if ($selected_id != -1) 
+{
+       if ($Mode == 'Edit') {
+               $myrow = get_printer($selected_id);
+               $_POST['name'] = $myrow['name'];
+               $_POST['descr'] = $myrow['description'];
+               $_POST['queue'] = $myrow['queue'];
+               $_POST['tout'] = $myrow['timeout'];
+               $_POST['host'] = $myrow['host'];
+               $_POST['port'] = $myrow['port'];
+       }
+       hidden('selected_id', $selected_id);
+} else {
+       if(!isset($_POST['host']))
+               $_POST['host'] = 'localhost';
+       if(!isset($_POST['port']))
+               $_POST['port'] = '515';
+}
+
+text_row(_("Printer Name").':', 'name', null, 20, 20);
+text_row(_("Printer Description").':', 'descr', null, 40, 60);
+text_row(_("Host name or IP").':', 'host', null, 30, 40);
+text_row(_("Port").':', 'port', null, 5, 5);
+text_row(_("Printer Queue").':', 'queue', null, 20, 20);
+text_row(_("Timeout").':', 'tout', null, 5, 5);
+
+end_table(1);
+
+submit_add_or_update_center($selected_id == -1, '', true);
+
+end_form();
+
+end_page();
+
+?>
diff --git a/doc/2.1_Beta.txt b/doc/2.1_Beta.txt
new file mode 100644 (file)
index 0000000..aabb6a3
--- /dev/null
@@ -0,0 +1,58 @@
+News in FrontAccounting 2.1 Beta
+--------------------------------
+
+Common
+------
+- Better support for upgrading database changes from early release
+- Accelerator keys in menues
+- Grouping listboxes where appropriate
+- Sortable database paging in inquiries with many records
+- Better layout in long forms (now double sided).
+- Preparing for install of menu extensions (not modules)
+- Document files in doc subdirectory
+- An inactive field in most 'catalog' tables to prepare for making records inactive.
+- Option for graphic links instead of text links in tables.
+- Changed license type to GNU GPL v.3. Stamped in all source files.
+
+Sales
+-----
+- Sales documents. Line descriptions can be edited and printing of documents handles multiple line of descriptions. 
+- Point of Sales definitions for better handling of cash sales
+- Sales Groups for grouping customers.
+- Recurrent Invoices
+- Dimensions can be selected on sales delivery and invoices. 
+- Accumulating of shipping costs and legal text on invoices
+- New printed document layout
+
+Purchasing
+----------
+- New fields in suppliers: phone, fax, tax id, contact and credit limit
+- Supplier credit notes show only invoices for a period
+- 'Credit this' link in supplier inquiry
+- Attachment of scanned documents
+
+Items and Inventory
+-------------------
+- Item Kit Sets for order/sales speed up
+- Foreign Codes registration for barcode scanner entry.
+
+Banking and General Ledger
+--------------------------
+- Bank accounts reconciliation
+- Dimension view now shows balances instead of transactions
+- Quick Entries (preset GL transactions) in Bank Deposit/Payment, Journal Entry and Supplier invoice/credit
+- Rewritten tax report and tax inquiry.
+
+Bugs fixed in this release
+--------------------------
+All bugs up to release 2.0.7 are fixed in this release too.
+
+# [0000097] Can't void freehand customer credit note.
+# [0000098] Unable issue of credit note for invoice with removed item lines.
+# [0000099] New line added to inventory transfer/adjustment sometimes overwrites old one.
+# [0000100] Keep getting left allocated weird results (rounding problems).
+# [0000102] Credit note was stored without entered shippment cost and comment.
+# [0000104] minor language updates in a few sales files 
+# Numeric check on class id added.
+# Bug [0000111] Accounts on Credit Hold can still process delivery notes on sales order and invoicing.
+# Bug [0000112] It shows invoices as overdue still (in red), even though its been payed and allocated. 
diff --git a/doc/attachments.txt b/doc/attachments.txt
new file mode 100644 (file)
index 0000000..704a41a
--- /dev/null
@@ -0,0 +1,6 @@
+Attaching documents
+-------------------
+
+In the Setup tab, Maintenance you can attach scanned documents to transactions. You can do that here, but you
+can also do it after you have entered a supplier invoice or supplier credit note. There will be a direct link
+so you don't have to remember the transaction number.
\ No newline at end of file
diff --git a/doc/bank_reconciliation.txt b/doc/bank_reconciliation.txt
new file mode 100644 (file)
index 0000000..ed9d9ef
--- /dev/null
@@ -0,0 +1,28 @@
+Bank Reconciliation
+-------------------
+1. Select the account you want to reconcile.
+2. Get your bank statement (from the bank)
+3. The first time you reconcile you will need to enter a reconcile date and 
+       a start balance... Or 0 for the start balance if it is actually a new 
+       account.
+4. Enter the end balance from the statement
+5. Check entries that match the entries from your bank statement
+6. If all entries match, 'Difference' should be 0. If it is, you are done 
+       and you have reconciled your account for the period.
+       If you have some transactions unchecked, they will wait for next bank 
+       statement to be reconciled. If you have some transactions on bank statement
+       but absent in system, go to the Bank Payments/Deposits and enter missing 
+       transactions, then repeat step 5.
+7. All checked items are recorded as reconciled in the bank_trans table and 
+       the last ending balance and reconciled date is recorded for that account 
+       in the bank_accounts table.
+8. The next time you reconcile against a bank statement, the ending balance 
+       from the last reconciliation is automatically used as the starting balance 
+       for this reconciliation..
+       You enter the ending balance for this reconciliation and repeat steps 5-7
+9. If you want for some reason to find transactions reconciled with bank 
+       statement on given date select the document date with the selector.
+       If you have a lot of bank statements entered you can skip the bank 
+       statement selector and enter date of bank statement in Reconcile Date 
+       directly. If there is no bank statement reconciled on given date the 
+       selector will display 'New'.
diff --git a/doc/dim_on_invoice.txt b/doc/dim_on_invoice.txt
new file mode 100644 (file)
index 0000000..ee3cf97
--- /dev/null
@@ -0,0 +1,21 @@
+Dimensions on sales delivery notes, sales invoices and sales credit notes
+-------------------------------------------------------------------------
+
+We now have the option to select a dimension on these documents.
+Here is how it works.
+
+If there are 2 levels in company setup, 2 dimension lists are shown with respective types. if 1 level, 
+only one list and of course if none there is no list shown.
+
+If there are dimension(s) set on a customer these will be set as default in the dimension(s) lists.
+
+If you choose a dimension for your invoice it will overwrite eventually dimension set by item. And the dimension(s) 
+are saved on the cust_trans table. 
+To be used when going from delivery to invoice, so there is a match between the 2 links.
+
+These dimension(s) goes to the GL trans. If delivery and there are standard_cost values they will show up on all COGS accounts.
+
+If invoicing the dimensions will go to the SALES accounts, so it is easy to monitor a dimension (project or whatever).
+
+Credit notes goes the other way round. The same procedure but opposite.
\ No newline at end of file
diff --git a/doc/extensions.txt b/doc/extensions.txt
new file mode 100644 (file)
index 0000000..08d93b4
--- /dev/null
@@ -0,0 +1,15 @@
+Extensions to FrontAccounting
+-----------------------------
+
+We have prepared for installation of Extensions. Like installed modules.
+In the root directory there is a file, installed_extensions.php. This file contains an array over installed extensions, $installed_extensions.
+There is an example array in the file.
+
+app_file = the application to be put inside the /applications directory
+name = array key name of the application
+title = menu title
+folder = directory where the extension resides. Use same structure inside this as the core extensions.
+
+The Extension will be placed as the second last menu, before 'Setup'.
+
+At present there is no automatic installation of the extensions. They will have to be done manuelly.
\ No newline at end of file
diff --git a/doc/license.txt b/doc/license.txt
new file mode 100644 (file)
index 0000000..10926e8
--- /dev/null
@@ -0,0 +1,675 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+
diff --git a/doc/quick_entries.txt b/doc/quick_entries.txt
new file mode 100644 (file)
index 0000000..1b9dfd5
--- /dev/null
@@ -0,0 +1,69 @@
+Quick Entries
+-------------
+
+Preparing
+---------
+
+Go into the Quick Entries on Banking and General Ledger tab, Maintenance.
+
+Enter/Modify existing entries. Choose a name for your quick entry and default amount
+used as a base for calculations. Determine type of entry i.e. page for which
+the quick entry is designed for. Last two input fields defines default base 
+amount used at the start of calculations, and description of base amount used
+as remainder during quick entry usage.
+
+If you press the Edit link on a Quick Entry you get a new form below where you 
+can edit the sequence of operations performed on base amount (Quick Entry Lines).
+
+Every operation defines how the amount posted to selected account is calculated.
+Currently following type of operations are defined:
+
+a) Post constant amount to selected GL account.
+b) Post percent part of base amount to selected GL account
+c) Post taxes for selected item tax type, using base amount as net value.
+d) Post taxes for selected item tax type, using base amount as value with tax included.
+e) Post remainder from previous calculations to selected GL acount.
+
+After most operations the base amount can be increased, decreased or not changed 
+depending on option selected. For every operation you can also select a dimension(s) 
+if you want to keep track of a dimension.
+
+You normally don't need more lines for this quick entry if this is type Phone 
+bill, gazolin or other kind of simple bills. Later on you can create 
+more sophisticated entry lines.
+
+If tax should be includes in the base amount you can first select a line
+'Tax included, reduce base and select the correct item tax type.
+And a line with 'Remainder' put on the account you want to put the rest on.
+That's it!
+
+Operation
+---------
+
+In the Bank Payments / Deposits form there are an option for selecting Quick 
+Entries, if you have defined some. After that a list shows up with the quick 
+entries for the respective Payments / deposits and an amount field.
+Here you enter the base amount for calculations performed during the quick 
+entry to make ready for you. Then press the 'Go' button to enter GL lines.
+
+Advanced
+--------
+
+The quick entries can do a lot more than this simple kind of entry.
+You can define them to be used in the GL entry form as well by 
+selecting type 'Journal'. Then they will show up in the Journal Entry page.
+The constraints on negative values has been taken away, so you can create 
+very sophisticated lines. F.i. if you want to post import VAT for something 
+bought overseas, you can enter a VAT line with incoming VAT. This will be 
+the same sign as the amount. You select the 'Post percent part' and enter 
+the percent amount in the amount field, f.i. 9 for 9% VAT. And another line 
+with the outgoing VAT with the same percent and amount (remember to set 
+this as -9 for correct posting).
+You can also use the 'Post percent part' for depreciations,
+create fixed amount lines (Post Amount) etc. etc. You can have as many lines as you want 
+or need. Remember nothing is posted before you press the Process button in 
+the bank payments/deposits/journal entry/supplier invoice/credit forms. You can always delete the lines 
+or leave the form and nothing has been performed. You might have entered 
+something wrong on one or more quick entry lines and want 
+to go to Quick Entries again and change something.
\ No newline at end of file
diff --git a/doc/recurrent_invoice.txt b/doc/recurrent_invoice.txt
new file mode 100644 (file)
index 0000000..e0382fe
--- /dev/null
@@ -0,0 +1,35 @@
+Recurrent Invoices
+------------------
+
+There are two ways of using recurrent invoices. You can either select a group or single customer/branches.
+
+If you are going to use groups, start by creating the groups you want to later put on the branches, Fi. Large, Medium and Small
+
+Go into the branches. If you want any of a custumers branch to belong to a group, mark it and update the branch.
+
+Definition.
+-----------
+You are now ready to make some recurrent invoice definitions, by clicking the 'Recurrent Invoices' in the Sales tab, Maintenance section.
+
+The recurrent invoices uses the Template Orders, that are shown under Template Delivery/Template Invoice.
+
+First enter a Discription of the item. Select the appropriate template. If this should be adressed to a single customer/branch, select a customer and a branch.
+If it should be based on a group, set the customer to an empty value and select the group you want to send the recurrent invoices to.
+
+Now it is time to consider the interval of the recurrency. If you select days and set the monthly value to 0, it will be due after every days interval.
+If you select a monthly value, it will be sent on a monthly base and if you select a days value it will be due on that day in the month. A value of -1 would 
+be the last day in the previous month. A monthly value of 3 indicated a due every quarter of a year and 12 every year and so on.
+
+The begin date is the date to start the recurrency and end date is the close date of the recurrency.
+
+Create and print the recurrent invoices.
+---------------------------------------
+Before creating recurring invoices, check that the price factor in company setup is set. If you have foreign customers the price is automatically calculated.
+Go to 'Create and Print Recurring Invoices'. The due items are marked and a link 'Create invoices' are there. 
+Pressing this link creates all the invoices belonging to this group or single customer/branch.
+An info line tells you how many and invoice-numbers that are created.
+There is also a link to Print the Invoices.
+
+That's it. Happy Recurrent Invoicing.
+
+PS. If you need more groups on a customer, create new branches with this different group. Maybe call the branch something similar as the group to easily find it.
\ No newline at end of file
diff --git a/gl/bank_account_reconcile.php b/gl/bank_account_reconcile.php
new file mode 100644 (file)
index 0000000..a195292
--- /dev/null
@@ -0,0 +1,282 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+/* Author Rob Mallon */
+$page_security = 8;
+$path_to_root="..";
+include($path_to_root . "/includes/db_pager.inc");
+include_once($path_to_root . "/includes/session.inc");
+
+include_once($path_to_root . "/includes/date_functions.inc");
+include_once($path_to_root . "/includes/ui.inc");
+include_once($path_to_root . "/includes/data_checks.inc");
+
+include_once($path_to_root . "/gl/includes/gl_db.inc");
+include_once($path_to_root . "/includes/banking.inc");
+
+$js = "";
+if ($use_popup_windows)
+       $js .= get_js_open_window(800, 500);
+if ($use_date_picker)
+       $js .= get_js_date_picker();
+
+add_js_file('reconcile.js');
+
+page(_("Reconcile Bank Account"), false, false, "", $js);
+
+check_db_has_bank_accounts(_("There are no bank accounts defined in the system."));
+
+function check_date() {
+       if (!is_date(get_post('reconcile_date'))) {
+               display_error(_("Invalid reconcile date format"));
+               set_focus('reconcile_date');
+               return false;
+       }
+       return true;
+}
+//
+//     This function can be used directly in table pager 
+//     if we would like to change page layout.
+//
+function rec_checkbox($row)
+{
+       $name = "rec_" .$row['id'];
+       $hidden = 'last['.$row['id'].']';
+       $value = $row['reconciled'] != '';
+
+// save also in hidden field for testing during 'Reconcile'
+       return checkbox(null, $name, $value, true, _('Reconcile this transaction'))
+               . hidden($hidden, $value, false);
+}
+
+function systype_name($dummy, $type)
+{
+       return systypes::name($type);
+}
+
+function trans_view($trans)
+{
+       return get_trans_view_str($trans["type"], $trans["trans_no"]);
+}
+
+function gl_view($row)
+{
+       return get_gl_view_str($row["type"], $row["trans_no"]);
+}
+
+function fmt_debit($row)
+{
+       $value = $row["amount"];
+       return $value>=0 ? price_format($value) : '';
+}
+
+function fmt_credit($row)
+{
+       $value = -$row["amount"];
+       return $value>0 ? price_format($value) : '';
+}
+
+function fmt_person($row)
+{
+       return payment_person_types::person_name($row["person_type_id"],$row["person_id"]);
+}
+
+$update_pager = false;
+function update_data()
+{
+       global $Ajax, $update_pager;
+       
+       unset($_POST["beg_balance"]);
+       unset($_POST["end_balance"]);
+       $Ajax->activate('summary');
+       $update_pager = true;
+}
+//---------------------------------------------------------------------------------------------
+// Update db record if respective checkbox value has changed.
+//
+function change_tpl_flag($reconcile_id)
+{
+       global  $Ajax;
+
+       if (!check_date()) 
+               return false;
+
+       if (get_post('bank_date')=='')  // new reconciliation
+               $Ajax->activate('bank_date');
+
+       $_POST['bank_date'] = date2sql(get_post('reconcile_date'));
+       $reconcile_value = check_value("rec_".$reconcile_id) 
+                                               ? ("'".$_POST['bank_date'] ."'") : 'NULL';
+       $sql = "UPDATE ".TB_PREF."bank_trans SET reconciled=$reconcile_value WHERE id=$reconcile_id";
+
+       db_query($sql, "Can't change reconciliation status");
+       // save last reconcilation status (date, end balance)
+    $sql2="UPDATE ".TB_PREF."bank_accounts SET last_reconciled_date='"
+                       .date2sql($_POST["reconcile_date"])."',
+           ending_reconcile_balance=".input_num("end_balance")
+                       ." WHERE id=".$_POST["bank_account"];
+
+       $result = db_query($sql2,"Error updating reconciliation information");
+       $Ajax->activate('reconciled');
+       $Ajax->activate('difference');
+       return true;
+}
+
+if (!isset($_POST['reconcile_date'])) { // init page
+       $_POST['reconcile_date'] = Today();
+//     $_POST['bank_date'] = date2sql(Today());
+}
+
+if (list_updated('bank_account')) {
+    $Ajax->activate('bank_date');
+       update_data();
+}
+if (list_updated('bank_date')) {
+       $_POST['reconcile_date'] = 
+               get_post('bank_date')=='' ? Today() : sql2date($_POST['bank_date']);
+       update_data();
+}
+if (get_post('_reconcile_date_changed')) {
+       $_POST['bank_date'] = check_date() ? date2sql(get_post('reconcile_date')) : '';
+    $Ajax->activate('bank_date');
+       update_data();
+}
+
+$id = find_submit('_rec_');
+if ($id != -1) 
+       change_tpl_flag($id);
+
+if (isset($_POST['Reconcile'])) {
+       set_focus('bank_date');
+       foreach($_POST['last'] as $id => $value)
+               if ($value != check_value('rec_'.$id))
+                       if(!change_tpl_flag($id)) break;
+    $Ajax->activate('_page_body');
+}
+
+//------------------------------------------------------------------------------------------------
+start_form();
+start_table("class='tablestyle_noborder'");
+start_row();
+bank_accounts_list_cells(_("Account:"), 'bank_account', null, true);
+
+bank_reconciliation_list_cells(_("Bank Statement:"), get_post('bank_account'),
+       'bank_date', null, true, _("New"));
+end_row();
+end_table();
+
+$date = date2sql(get_post('reconcile_date'));
+
+$sql = "SELECT MAX(reconciled) as last_date,
+                SUM(IF(reconciled<='$date', amount, 0)) as end_balance,
+                SUM(IF(reconciled<'$date', amount, 0)) as beg_balance,
+                SUM(amount) as total
+       FROM ".TB_PREF."bank_trans trans
+       WHERE bank_act=".$_POST['bank_account'];
+//     ." AND trans.reconciled IS NOT NULL";
+
+$result = db_query($sql,"Cannot retrieve reconciliation data");
+
+if ($row = db_fetch($result)) {
+       $_POST["reconciled"] = price_format($row["end_balance"]-$row["beg_balance"]);
+       $total = $row["total"];
+       if (!isset($_POST["beg_balance"])) { // new selected account/statement
+               $_POST["last_date"] = sql2date($row["last_date"]);
+               $_POST["beg_balance"] = price_format($row["beg_balance"]);
+               $_POST["end_balance"] = price_format($row["end_balance"]);
+               if (get_post('bank_date')) {
+                       // if it is the last updated bank statement retrieve ending balance
+                       $sql = "SELECT ending_reconcile_balance
+                               FROM ".TB_PREF."bank_accounts WHERE id=".$_POST['bank_account']
+                               . " AND last_reconciled_date='".$_POST['bank_date']."'";
+                       $result = db_query($sql,"Cannot retrieve last reconciliation");
+                       $row = db_fetch($result);
+                       if($row) {
+                               $_POST["end_balance"] = price_format($row["ending_reconcile_balance"]);
+                       }
+               }
+       } 
+}
+
+echo "<hr>";
+
+div_start('summary');
+
+start_table($table_style);
+$th = array(_("Reconcile Date"), _("Beginning<br>Balance"), 
+       _("Ending<br>Balance"), _("Account<br>Total"),_("Reconciled<br>Amount"), _("Difference"));
+table_header($th);
+start_row();
+
+date_cells("", "reconcile_date", _('Date of bank statement to reconcile'), 
+       null, 0, 0, 0, null, true);
+
+amount_cells_ex("", "beg_balance", 15);
+
+amount_cells_ex("", "end_balance", 15);
+
+$reconciled = input_num('reconciled');
+$difference = input_num("end_balance") - input_num("beg_balance") - $reconciled;
+
+amount_cell($total);
+amount_cell($reconciled, false, '', "reconciled");
+amount_cell($difference, false, '', "difference");
+
+end_row();
+end_table();
+div_end();
+echo "<hr>";
+//------------------------------------------------------------------------------------------------
+
+if (!isset($_POST['bank_account']))
+    $_POST['bank_account'] = "";
+
+$sql = "SELECT type, trans_no, ref, trans_date, 
+                               amount, person_id, person_type_id, reconciled, id
+               FROM ".TB_PREF."bank_trans
+               WHERE ".TB_PREF."bank_trans.bank_act = '" . $_POST['bank_account'] . "'
+                       AND (reconciled IS NULL OR reconciled='". $date ."')
+               ORDER BY trans_date,".TB_PREF."bank_trans.id";
+// or  ORDER BY reconciled desc, trans_date,".TB_PREF."bank_trans.id";
+
+$act = get_bank_account($_POST["bank_account"]);
+display_heading($act['bank_account_name']." - ".$act['bank_curr_code']);
+
+       $cols =
+       array(
+               _("Type") => array('fun'=>'systype_name', 'ord'=>''),
+               _("#") => array('fun'=>'trans_view', 'ord'=>''),
+               _("Reference"), 
+               _("Date") => 'date',
+               _("Debit") => array('align'=>'right', 'fun'=>'fmt_debit'), 
+               _("Credit") => array('align'=>'right','insert'=>true, 'fun'=>'fmt_credit'), 
+           _("Person/Item") => array('fun'=>'fmt_person'), 
+               array('insert'=>true, 'fun'=>'gl_view'),
+               "X"=>array('insert'=>true, 'fun'=>'rec_checkbox')
+          );
+       $table =& new_db_pager('trans_tbl', $sql, $cols);
+       if ($update_pager) {
+               $table->set_sql($sql);
+               $table->set_columns($cols);
+       }
+       $table->width = "60%";
+       display_db_pager($table);
+
+br(1);
+submit_center('Reconcile', _("Reconcile"), true, '', null);
+
+end_form();
+
+//------------------------------------------------------------------------------------------------
+
+end_page();
+
+?>
\ No newline at end of file
diff --git a/gl/inquiry/tax_inquiry.php b/gl/inquiry/tax_inquiry.php
new file mode 100644 (file)
index 0000000..df9cd14
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+$page_security = 8;
+$path_to_root="../..";
+include_once($path_to_root . "/includes/session.inc");
+
+
+include_once($path_to_root . "/includes/date_functions.inc");
+include_once($path_to_root . "/includes/ui.inc");
+include_once($path_to_root . "/includes/data_checks.inc");
+
+include_once($path_to_root . "/gl/includes/gl_db.inc");
+
+$js = '';
+set_focus('account');
+if ($use_popup_windows)
+       $js .= get_js_open_window(800, 500);
+if ($use_date_picker)
+       $js .= get_js_date_picker();
+
+page(_("Tax Inquiry"), false, false, '', $js);
+
+//----------------------------------------------------------------------------------------------------
+// Ajax updates
+//
+if (get_post('Show')) 
+{
+       $Ajax->activate('trans_tbl');
+}
+
+if (get_post('TransFromDate') == "" && get_post('TransToDate') == "")
+{
+       $date = Today();
+       $row = get_company_prefs();
+       $edate = add_months($date, -$row['tax_last']);
+       $edate = end_month($edate);
+       $bdate = add_months($edate, -$row['tax_prd'] + 1);
+       $_POST["TransFromDate"] = begin_month($bdate);
+       $_POST["TransToDate"] = $edate;
+}      
+
+//----------------------------------------------------------------------------------------------------
+
+function tax_inquiry_controls()
+{
+       global $table_style2;
+
+    start_form();
+
+    //start_table($table_style2);
+    start_table("class='tablestyle_noborder'");
+       start_row();
+
+       date_cells(_("from:"), 'TransFromDate', '', null, -30);
+       date_cells(_("to:"), 'TransToDate');
+       submit_cells('Show',_("Show"),'','', true);
+
+    end_row();
+
+       end_table();
+
+    end_form();
+}
+
+//----------------------------------------------------------------------------------------------------
+
+function show_results()
+{
+       global $path_to_root, $table_style;
+
+    /*Now get the transactions  */
+       div_start('trans_tbl');
+       start_table($table_style);
+
+       $th = array(_("Type"), _("Description"), _("Amount"), _("Outputs")."/"._("Inputs"));
+       table_header($th);
+       $k = 0;
+       $total = 0;
+       $bdate = date2sql($_POST['TransFromDate']);
+       $edate  = date2sql($_POST['TransToDate']);
+
+       $taxes = get_tax_summary($_POST['TransFromDate'], $_POST['TransToDate']);
+
+       while ($tx = db_fetch($taxes))
+       {
+
+               $payable = $tx['payable'];
+               $collectible = $tx['collectible'];
+               $net = $collectible + $payable;
+               $total += $net;
+               alt_table_row_color($k);
+               label_cell($tx['name'] . " " . $tx['rate'] . "%");
+               label_cell(_("Charged on sales") . " (" . _("Output Tax")."):");
+               amount_cell($payable);
+               amount_cell($tx['net_output']);
+               end_row();
+               alt_table_row_color($k);
+               label_cell($tx['name'] . " " . $tx['rate'] . "%");
+               label_cell(_("Paid on purchases") . " (" . _("Input Tax")."):");
+               amount_cell($collectible);
+               amount_cell($tx['net_input']);
+               end_row();
+               alt_table_row_color($k);
+               label_cell("<b>".$tx['name'] . " " . $tx['rate'] . "%</b>");
+               label_cell("<b>"._("Net payable or collectible") . ":</b>");
+               amount_cell($net, true);
+               label_cell("");
+               end_row();
+       }       
+       alt_table_row_color($k);
+       label_cell("");
+       label_cell("<b>"._("Total payable or refund") . ":</b>");
+       amount_cell($total, true);
+       label_cell("");
+       end_row();
+
+       end_table(2);
+       div_end();
+}
+
+//----------------------------------------------------------------------------------------------------
+
+tax_inquiry_controls();
+
+show_results();
+
+//----------------------------------------------------------------------------------------------------
+
+end_page();
+
+?>
diff --git a/gl/manage/gl_quick_entries.php b/gl/manage/gl_quick_entries.php
new file mode 100644 (file)
index 0000000..137cfaa
--- /dev/null
@@ -0,0 +1,343 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+$page_security = 3;
+$path_to_root="../..";
+include($path_to_root . "/includes/session.inc");
+
+page(_("Quick Entries"));
+
+include($path_to_root . "/gl/includes/gl_db.inc");
+
+include($path_to_root . "/includes/ui.inc");
+
+simple_page_mode(true);
+simple_page_mode2(true);
+
+function simple_page_mode2($numeric_id = true)
+{
+       global $Ajax, $Mode2, $selected_id2;
+
+       $default = $numeric_id ? -1 : '';
+       $selected_id2 = get_post('selected_id2', $default);
+       foreach (array('ADD_ITEM2', 'UPDATE_ITEM2', 'RESET2') as $m) {
+               if (isset($_POST[$m])) {
+                       $Ajax->activate('_page_body');
+                       if ($m == 'RESET2') 
+                               $selected_id2 = $default;
+                       $Mode2 = $m; return;
+               }
+       }
+       foreach (array('BEd', 'BDel') as $m) {
+               foreach ($_POST as $p => $pvar) {
+                       if (strpos($p, $m) === 0) {
+//                             $selected_id2 = strtr(substr($p, strlen($m)), array('%2E'=>'.'));
+                               unset($_POST['_focus']); // focus on first form entry
+                               $selected_id2 = quoted_printable_decode(substr($p, strlen($m)));
+                               $Ajax->activate('_page_body');
+                               $Mode2 = $m;
+                               return;
+                       }
+               }
+       }
+       $Mode2 = '';
+}
+
+function submit_add_or_update_center2($add=true, $title=false, $async=false)
+{
+       echo "<center>";
+       if ($add)
+               submit('ADD_ITEM2', _("Add new"), true, $title, $async);
+       else {
+               submit('UPDATE_ITEM2', _("Update"), true, $title, $async);
+               submit('RESET2', _("Cancel"), true, $title, $async);
+       }
+       echo "</center>";
+}
+
+//-----------------------------------------------------------------------------------
+
+function can_process() 
+{
+
+       if (strlen($_POST['description']) == 0) 
+       {
+               display_error( _("The Quick Entry description cannot be empty."));
+               set_focus('description');
+               return false;
+       }
+       if (strlen($_POST['base_desc']) == 0) 
+       {
+               display_error( _("The base amount description cannot be empty."));
+               set_focus('base_desc');
+               return false;
+       }
+
+       return true;
+}
+
+//-----------------------------------------------------------------------------------
+
+if ($Mode=='ADD_ITEM' || $Mode=='UPDATE_ITEM') 
+{
+
+       if (can_process())      
+       {       
+
+               if ($selected_id != -1) 
+               {
+                       update_quick_entry($selected_id, $_POST['description'], $_POST['type'],
+                                input_num('base_amount'), $_POST['base_desc']);
+                       display_notification(_('Selected quick entry has been updated'));
+               } 
+               else 
+               {
+                       add_quick_entry($_POST['description'], $_POST['type'], 
+                               input_num('base_amount'), $_POST['base_desc']);
+                       display_notification(_('New quick entry has been added'));
+               }
+               $Mode = 'RESET';
+       }
+}
+
+if ($Mode2=='ADD_ITEM2' || $Mode2=='UPDATE_ITEM2') 
+{
+       if ($selected_id2 != -1) 
+       {
+               update_quick_entry_line($selected_id2, $selected_id, $_POST['actn'], $_POST['dest_id'], input_num('amount', 0), 
+                       $_POST['dimension_id'], $_POST['dimension2_id']);
+               display_notification(_('Selected quick entry line has been updated'));
+       } 
+       else 
+       {
+               add_quick_entry_line($selected_id, $_POST['actn'], $_POST['dest_id'], input_num('amount', 0), 
+                       $_POST['dimension_id'], $_POST['dimension2_id']);
+               display_notification(_('New quick entry line has been added'));
+       }
+       $Mode2 = 'RESET2';
+}
+
+//-----------------------------------------------------------------------------------
+
+if ($Mode == 'Delete')
+{
+       if (!has_quick_entry_lines($selected_id))
+       {
+               delete_quick_entry($selected_id);
+               display_notification(_('Selected quick entry has been deleted'));
+               $Mode = 'RESET';
+       }
+       else
+       {
+               display_error( _("The Quick Entry has Quick Entry Lines. Cannot be deleted."));
+               set_focus('description');
+       }
+}
+
+if ($Mode2 == 'BDel')
+{
+       delete_quick_entry_line($selected_id2);
+       display_notification(_('Selected quick entry line has been deleted'));
+       $Mode2 = 'RESET2';
+}
+//-----------------------------------------------------------------------------------
+if ($Mode == 'RESET')
+{
+       $selected_id = -1;
+       $_POST['description'] = $_POST['type'] = '';
+       $_POST['base_desc']= _('Base Amount');
+       $_POST['base_amount'] = price_format(0);
+}
+if ($Mode2 == 'RESET2')
+{
+       $selected_id2 = -1;
+       $_POST['actn'] = $_POST['dest_id'] = $_POST['amount'] = 
+               $_POST['dimension_id'] = $_POST['dimension2_id'] = '';
+}
+//-----------------------------------------------------------------------------------
+
+$result = get_quick_entries();
+start_form();
+start_table($table_style);
+$th = array(_("Description"), _("Type"), "", "");
+table_header($th);
+
+$k = 0;
+while ($myrow = db_fetch($result)) 
+{
+       alt_table_row_color($k);
+       $type_text = $quick_entry_types[$myrow["type"]];
+       label_cell($myrow['description']);
+       label_cell($type_text);
+       edit_button_cell("Edit".$myrow["id"], _("Edit"));
+       delete_button_cell("Delete".$myrow["id"], _("Delete"));
+       end_row();
+}
+
+end_table(1);
+end_form();
+//-----------------------------------------------------------------------------------
+
+start_form();
+
+start_table($table_style2);
+
+if ($selected_id != -1) 
+{
+       //if ($Mode == 'Edit') 
+       //{
+               //editing an existing status code
+               $myrow = get_quick_entry($selected_id);
+
+               $_POST['id']  = $myrow["id"];
+               $_POST['description']  = $myrow["description"];
+               $_POST['type']  = $myrow["type"];
+               $_POST['base_desc']  = $myrow["base_desc"];
+               $_POST['base_amount']  = price_format($myrow["base_amount"]);
+               hidden('selected_id', $selected_id);
+       //}
+} 
+
+text_row_ex(_("Description").':', 'description', 50, 60);
+
+quick_entry_types_list_row(_("Entry Type").':', 'type');
+
+text_row_ex(_("Base Amount Description").':', 'base_desc', 50, 60, '',_('Base Amount'));
+
+amount_row(_("Default Base Amount").':', 'base_amount', price_format(0));
+
+end_table(1);
+
+submit_add_or_update_center($selected_id == -1, '', true);
+
+end_form();
+
+
+if ($selected_id != -1)
+{
+       display_heading(_("Quick Entry Lines") . " - " . $_POST['description']);
+       $result = get_quick_entry_lines($selected_id);
+       start_form();
+       start_table($table_style2);
+       $dim = get_company_pref('use_dimension');
+       if ($dim == 2)
+               $th = array(_("Post"), _("Account/Tax Type"), _("Amount"), _("Dimension"), _("Dimension")." 2", "", "");
+       else if ($dim == 1)     
+               $th = array(_("Post"), _("Account/Tax Type"), _("Amount"), _("Dimension"), "", "");
+       else    
+               $th = array(_("Post"), _("Account/Tax Type"), _("Amount"), "", "");
+
+       table_header($th);
+       $k = 0;
+       while ($myrow = db_fetch($result)) 
+       {
+               alt_table_row_color($k);
+               
+               label_cell($quick_actions[$myrow['action']]);
+
+               $act_type = strtolower(substr($myrow['action'], 0, 1));
+
+               if ($act_type == 't') 
+               {
+                       label_cells($myrow['tax_name'], '');
+               } 
+               else 
+               {
+                       label_cell($myrow['dest_id'].' '.$myrow['account_name']);
+                       if ($act_type == '=') 
+                               label_cell('');
+                       elseif ($act_type == '%') 
+                               label_cell(number_format2($myrow['amount'], user_exrate_dec()), "nowrap align=right ");
+                       else
+                               amount_cell($myrow['amount']);
+               }               
+               if ($dim >= 1)
+                       label_cell(get_dimension_string($myrow['dimension_id'], true));
+               if ($dim > 1)
+                       label_cell(get_dimension_string($myrow['dimension2_id'], true));
+               edit_button_cell("BEd".$myrow["id"], _("Edit"));
+               delete_button_cell("BDel".$myrow["id"], _("Delete"));
+               end_row();
+       }
+       end_table(1);
+       hidden('selected_id', $selected_id);
+       hidden('selected_id2', $selected_id2);
+       hidden('description', $_POST['description']);
+       hidden('type', $_POST['type']);
+       end_form();
+       start_form();
+
+       div_start('edit_line');
+       start_table($table_style2);
+
+       if ($selected_id2 != -1) 
+       {
+               if ($Mode2 == 'BEd') 
+               {
+                       //editing an existing status code
+                       $myrow = get_quick_entry_line($selected_id2);
+
+                       $_POST['id']  = $myrow["id"];
+                       $_POST['dest_id']  = $myrow["dest_id"];
+                       $_POST['actn']  = $myrow["action"];
+                       $_POST['amount']  = $myrow["amount"];
+                       $_POST['dimension_id']  = $myrow["dimension_id"];
+                       $_POST['dimension2_id']  = $myrow["dimension2_id"];
+               }
+       } 
+
+       quick_actions_list_row(_("Posted").":",'actn', null, true);
+       if (list_updated('actn'))
+               $Ajax->activate('edit_line');
+
+       $actn = strtolower(substr($_POST['actn'],0,1));
+
+       if ($actn == 't') 
+       {
+               item_tax_types_list_row(_("Item Tax Type").":",'dest_id', null);
+       } 
+       else 
+       {
+               gl_all_accounts_list_row(_("Account").":", 'dest_id', null);
+               if ($actn != '=') 
+               {
+                       if ($actn == '%') 
+                               small_amount_row(_("Part").":", 'amount', price_format(0), null, "%", user_exrate_dec());
+                       else
+                               amount_row(_("Amount").":", 'amount', price_format(0));
+               }
+       }
+       if ($dim >= 1) 
+               dimensions_list_row(_("Dimension").":", 'dimension_id', null, true, " ", false, 1);
+       if ($dim > 1) 
+               dimensions_list_row(_("Dimension")." 2:", 'dimension2_id', null, true, " ", false, 2);
+       
+       end_table(1);
+       if ($dim < 2)
+               hidden('dimension2_id', 0);
+       if ($dim < 1)
+               hidden('dimension_id', 0);
+       div_end();
+
+       hidden('selected_id', $selected_id);
+       hidden('selected_id2', $selected_id2);
+       hidden('description', $_POST['description']);
+       hidden('type', $_POST['type']);
+
+       submit_add_or_update_center2($selected_id2 == -1, '', true);
+
+       end_form();
+}              
+//------------------------------------------------------------------------------------
+
+end_page();
+
+?>
diff --git a/includes/db_pager.inc b/includes/db_pager.inc
new file mode 100644 (file)
index 0000000..819b475
--- /dev/null
@@ -0,0 +1,425 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+//
+//     Controler part of database table pager with column sort.
+//     To display actual html object call display_db_pager($name) inside
+//  any form.
+//
+//     View definition you will find in the following file:
+include_once($path_to_root."/includes/ui/db_pager_view.inc");
+
+class db_pager {
+       var $sql;
+       var $name;
+       var $columns;           // column definitions (head, type, order)
+
+       var $marker;            // marker check function
+       var $marker_txt;        
+       var $marker_class;
+       var $notice_class;
+       var $width;                     // table width (default '95%')
+       var $header_fun;        // additional row between title and body
+       var $header_class;
+       var $footer_fun;
+       var $footer_class;
+       var $data = array();
+
+       var $curr_page,
+               $max_page,
+           $last_page, 
+           $prev_page, 
+           $next_page,
+           $first_page;
+           
+       var $page_len,
+           $rec_count;
+       
+       var $select,
+               $where,
+           $from,
+               $group,
+               $order;
+       var     $extra_where;
+       
+       var $ready = false; // this var is false after change in sql before first
+                                               // and before first query.
+
+       //  db_pager constructor
+       //  accepts $sql like 'SELECT ...[FROM ...][WHERE ...][GROUP ...][ORDER ...]'
+       //      $name is base name for pager controls
+       function db_pager($sql, $name, $page_len=0) 
+       {
+               global $table_style;
+               $this->width = "95%";
+               if ($page_len == 0) $page_len = user_query_size();
+               $this->name = $name;
+               $this->page_len = $page_len;
+               $this->set_sql($sql);
+       }
+       //
+       //      Parse base sql select query.
+       //
+       function set_sql($sql)
+       {
+               if ($sql != $this->sql) {
+                   $this->sql = $sql;
+                   $this->ready = false;
+               $parts = preg_split('/\sORDER\s*BY\s/si', $sql, 2);
+                       if(count($parts) == 2) {
+                               $sql = $parts[0];
+                               $this->order = $parts[1];
+                       }
+               $parts = preg_split('/\sGROUP\s*BY\s/si', $sql, 2);
+                       if(count($parts) == 2) {
+                               $sql = $parts[0];
+                               $this->group = $parts[1];
+                       }
+               $parts = preg_split('/\sWHERE\s/si', $sql, 2);
+                       if(count($parts) == 2) {
+                               $sql = $parts[0];
+                               $this->where = $parts[1];
+                       }
+               $parts = preg_split('/\sFROM\s/si', $sql, 2);
+                       if(count($parts) == 2) {
+                               $sql = $parts[0];
+                               $this->from = $parts[1];
+                       }
+                       $this->select = $sql;
+               }
+       }
+       //
+       //      Set additional constraint on record set
+       //
+       function set_where($where = null)
+       {
+               if ($where) {
+               if (!is_array($where))
+                         $where = array($where);
+
+                   if (count($where) == count($this->extra_where) &&
+                               !count(array_diff($this->extra_where, $where)))
+                                return;
+               }
+               $this->extra_where = $where;
+               $this->ready = false;
+       }
+       //
+       //      Set query result page
+       //
+       function change_page($page=null) 
+       {
+           $this->set_page($page);
+           $this->query();
+           return true;
+       }
+       //
+       //      Change sort column direction 
+       //      in order asc->desc->none->asc
+       //
+       function sort_table($col) 
+       {
+           $ord = $this->columns[$col]['ord'];
+           $ord = ($ord == '') ? 'asc' : (($ord == 'asc') ? 'desc' : '');
+           $this->columns[$col]['ord'] = $ord;
+           $this->set_page(1);
+           $this->query();
+           return true;
+       }
+       //
+       // Query database
+       //
+       function query() 
+       {
+               global $Ajax;
+
+               $Ajax->activate("_{$this->name}_span");
+           $this->data = array();
+           if (!$this->_init()) 
+                 return false;
+
+           if ($this->rec_count == 0) return true;
+
+           $sql = $this->_sql_gen(false);
+
+           $result = db_query($sql, 'Error browsing database: '.$sql );
+
+           if ($result) {
+               // setting field names for subsequent queries
+                       $c = 0;
+                 // add result field names to column defs for 
+                 // col value retrieve and sort purposes 
+                       for ($c = $i = 0; $c < count($this->columns); $c++) {
+                               if (!(isset($this->columns[$c]['insert']) && $this->columns[$c]['insert']))
+                                       $this->columns[$c]['name']= mysql_field_name($result, $i++);
+                       }
+                 
+                       while ($row = db_fetch_assoc($result))
+                               $this->data[] = $row;
+                 
+               } else 
+                 return false;
+               return true;
+       }           
+       //
+       //      Calculates page numbers for html controls.
+       //
+       function set_page($to) 
+       {
+           switch($to) {
+               case 'next':
+                   $page = $this->curr_page+1; break;
+               case 'prev':
+                   $page = $this->curr_page-1; break;
+               case 'last':
+                   $page = $this->last_page; break;
+               default:
+                   if (is_numeric($to)) {
+                        $page = $to; break;
+                   }
+               case 'first':
+                   $page = 1; break;
+           }
+         if ($page < 1) 
+           $page = 1;
+         $max = $this->max_page;
+         if ($page > $max) 
+           $page = $max;
+         $this->curr_page = $page;
+         $this->next_page = ($page < $max) ? $page+1 : null;
+         $this->prev_page = ($page > 1) ? ($page-1) : null;
+         $this->last_page = ($page < $max) ? $max : null;
+         $this->first_page = ($page != 1) ? 1: null;
+       }
+       //
+       //      Set column definitions
+       //  $flds: array( fldname1, fldname2=>type,...)
+       function set_columns($flds)
+       {
+               $this->columns = array();
+               if (!is_array($flds)) {
+                       $flds = array($flds);
+               }
+               foreach ($flds as $colnum=>$coldef) {
+                       if (is_string($colnum)) {       // 'colname'=>params
+                         $h = $colnum;
+                         $c = $coldef;
+                       } else {                        //  n=>params
+                               if (is_array($coldef)) {
+                                       $h = '';
+                                       $c = $coldef;
+                               } else {
+                                       $h = $coldef;
+                                       $c = 'text';
+                               }
+                       }
+                       if (is_string($c))                      // params is simple column type
+                         $c = array('type'=>$c);
+
+                       if (!isset($c['type']))
+                         $c['type'] = 'text';
+
+                       switch($c['type']) {
+                         case 'insert':
+                         default:
+                               $c['head'] = $h; break;
+                         case 'skip':          // skip the column (no header)
+                               unset($c['head']);      // paranoid code
+                       }
+                       $this->columns[] = $c;  
+               }
+       }
+       //
+       // Generate db query from base sql
+       // $count==false - for current page data retrieval 
+       // $count==true  - for total records count
+       //
+       function _sql_gen($count=false) 
+       {
+               $select = $this->select;
+               $from = $this->from;
+               $where = $this->where;
+               $group = $this->group;          
+               $order = $this->order;
+
+               if(count($this->extra_where)) {
+                   $where .= ($where=='' ? '' : ' AND ')
+                               .implode( $this->extra_where, ' AND ');
+               }
+               if ($where) $where = " WHERE ($where)";
+
+               if ($count) {
+                       $group = $group == '' ? "*" : "DISTINCT $group";
+
+                       return "SELECT COUNT($group) FROM $from $where";
+               }
+
+               $sql = "$select FROM $from $where";
+               if ($group) $sql.= " GROUP BY $group";
+           $ord = array();
+
+           foreach ($this->columns as $col) {
+               if (isset($col['ord'])) {
+                       if ( $col['ord'] != '' && isset($col['name'])) {
+                           $ord[] = $col['name'] .' '. $col['ord'];
+                           }
+                       }
+           }
+                               
+           if (count($ord)) {
+                       $sql .= " ORDER BY " . implode($ord, ',');
+               } else {
+                       if($order)
+                               $sql .= " ORDER BY $order"; // original base query order
+               }
+
+           $page_len = $this->page_len;
+           $offset = ($this->curr_page - 1) * $page_len;
+
+           $sql .= " LIMIT $offset, $page_len";
+
+               return $sql;
+               
+       }
+       //
+       //      Initialization after changing record set
+       //
+       function _init() 
+       {
+               global $go_debug;
+               
+           if ($this->ready == false ) {
+                       $sql = $this->_sql_gen(true);
+                       $result = db_query($sql, 'Error reading record set');
+                       if ($result == false) 
+                               return false;
+                       $row = db_fetch_row($result);
+                       $this->rec_count = $row[0];
+                       $this->max_page = $this->page_len ?
+                               ceil($this->rec_count/$this->page_len) : 0;
+               
+                       if ($go_debug) { // FIX - need column name parsing, but for now:
+                               // check if field names are set explicite in col def
+                               // for all initially ordered columns
+                           foreach ($this->columns as $col) {
+                               if (isset($col['ord']) && $col['ord'] != '' 
+                                               &&  !isset($col['name'])) {
+                                                       display_warning("Result field names must be set
+                                                               for all intially ordered db_pager columns.");
+                               }
+                               }
+               }
+                       $this->set_page(1);
+                       $this->ready = true;
+           }
+       return true;
+       }
+       //
+       //      Set current page in response to user control.
+       //
+       function select_records() 
+       {
+               global $Ajax;
+               
+               $page = find_submit($this->name.'_page_', false);
+               $sort = find_submit($this->name.'_sort_', true);
+               if ($page) {
+                       $this->change_page($page);
+                       if ($page == 'next' && !$this->next_page ||
+                               $page == 'last' && !$this->last_page)
+                                       set_focus($this->name.'_page_prev');
+                       if ($page == 'prev' && !$this->prev_page ||
+                               $page == 'first' && !$this->first_page)
+                                       set_focus($this->name.'_page_next');
+               } elseif ($sort != -1) {
+                       $this->sort_table($sort);
+               } else
+                       $this->query();
+       }
+       //
+       //      Set check function to mark some rows.
+       //      
+       function set_marker($func, $notice='', $markercl='overduebg', $msgclass='overduefg' )
+       {
+               $this->marker = $func;
+               $this->marker_txt = $notice;
+               $this->marker_class = $markercl;
+               $this->notice_class = $msgclass;
+       }
+       //
+       //      Set handler to display additional row between titles and pager body.
+       //      Return array of column contents.
+       //
+       function set_header($func, $headercl='inquirybg')
+       {
+               $this->header_fun = $func;
+               $this->header_class = $headercl;
+       }
+       //
+       //      Set handler to display additional row between pager body and navibar.
+       //      Return array of column contents.
+       //
+       function set_footer($func, $footercl='inquirybg')
+       {
+               $this->footer_fun = $func;
+               $this->footer_class = $footercl;
+       }
+};
+//-----------------------------------------------------------------------------
+//     Creates new db_pager $_SESSION object on first page call.
+//  Retrieves from $_SESSION var on subsequent $_POST calls
+//
+//  $name - base name for pager controls and $_SESSION object name
+//  $sql  - base sql for data inquiry. Order of fields implies
+//             pager columns order.
+//     $coldef - array of column definitions. Example definitions
+//             Column with title 'User name' and default text format:
+//                             'User name'
+//             Skipped field from sql query. Data for the field is not displayed:
+//                             'dummy' => 'skip'
+//             Column without title, data retrieved form row data with function func():
+//                             array('fun'=>'func')
+//             Inserted column with title 'Some', formated with function rowfun().
+//     formated as date:
+//                             'Some' => array('type'=>'date, 'insert'=>true, 'fun'=>'rowfun')
+//             Column with name 'Another', formatted as date, 
+// sortable with ascending start order (available orders: asc,desc, '').
+//                             'Another' => array('type'=>'date', 'ord'=>'asc')
+//
+//     All available column format types you will find in db_pager_view.inc file.
+//             If query result has more fields than count($coldef), rest of data is ignored
+//  during display, but can be used in format handlers for 'spec' and 'insert' 
+//     type columns.
+
+function &new_db_pager($name, $sql, $coldef, $page_len = 0)  {
+
+    if ($_SERVER['REQUEST_METHOD'] == 'GET')
+               unset($_SESSION[$name]); // kill old pager if any exists on first page call
+
+       if (!isset($_SESSION[$name])) {
+           $_SESSION[$name] =& new db_pager($sql, $name, $page_len);
+               $_SESSION[$name]->set_sql($sql);
+               $_SESSION[$name]->set_columns($coldef);
+       }
+       
+       $ret = &$_SESSION[$name];
+
+    return $ret;
+}
+//
+//     Force pager initialization.
+//
+function refresh_pager($name)
+{
+       if (isset($_SESSION[$name]))
+               $_SESSION[$name]->ready = false;
+}
+?>
\ No newline at end of file
diff --git a/includes/ui/db_pager_view.inc b/includes/ui/db_pager_view.inc
new file mode 100644 (file)
index 0000000..7fe7754
--- /dev/null
@@ -0,0 +1,193 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+//--------------------------------------------------------------------------------------------------
+function pager_link($link_text, $url, $icon=false)
+{
+       global $path_to_root;
+       
+       $link = access_string($link_text);
+       if (user_graphic_links() && $icon)
+               $link[0] = set_icon($icon, $link[0]);
+
+       $href = $path_to_root . $url;
+       return "<a href='$href'$link[1]>" . $link[0] . "</a>";
+}
+
+function navi_button($name, $value, $enabled=true, $icon = false) {
+       global $path_to_root;
+       return "<button ". ($enabled ? '':'disabled')
+               ." class=\"navibutton\" type=\"submit\""
+           ." name=\"$name\"  id=\"$name\" value=\"$value\">"
+               .($icon ? "<img src='$path_to_root/themes/".user_theme()."/images/".$icon."'>":'')
+               ."<span>$value</span></button>\n";
+}
+
+function navi_button_cell($name, $value, $enabled=true, $align='left') {
+       label_cell(navi_button($name, $value, $enabled), "align='$align'");
+}
+//-----------------------------------------------------------------------------
+//
+//    Sql paged table view. Call this function inside form.
+//
+function display_db_pager(&$pager) {
+    global     $table_style, $use_popup_windows, $use_date_picker, $path_to_root;
+
+       $pager->select_records();
+
+       div_start("_{$pager->name}_span");
+    $headers = array();
+
+       foreach($pager->columns as $num_col=>$col) {
+       if (isset($col['head'])) {
+                       if (!isset($col['ord'])) 
+                               $headers[] = $col['head'];
+                       else {
+                               $icon = (($col['ord'] == 'desc') ? 'sort_desc.gif' : 
+                                       ($col['ord'] == 'asc' ? 'sort_asc.gif' : 'sort_none.gif'));
+                               $headers[] = navi_button($pager->name.'_sort_'.$num_col, 
+                                       $col['head'], true, $icon);
+                       }
+               }
+       }
+    /* show a table of records returned by the sql */
+    start_table("$table_style width=$pager->width");
+    table_header($headers, 'nowrap');
+
+       if($pager->header_fun) {        // if set header handler
+               start_row("class='{$pager->header_class}'");
+               $fun = $pager->header_fun;
+               if (method_exists($pager, $fun)) { 
+                       $h = $pager->$fun($pager);
+               } elseif (function_exists($fun)) {
+                       $h = $fun($pager);
+               }
+               
+               foreach($h as $c) {      // draw header columns
+                       $pars = isset($c[1]) ? $c[1] : '';
+                       label_cell($c[0], $pars);
+               }
+               end_row();
+       }
+
+       $cc = 0; //row colour counter
+       foreach($pager->data as $line_no => $row) {     
+
+               $marker = $pager->marker;
+           if ($marker && $marker($row)) 
+               start_row("class='$pager->marker_class'");
+           else        
+                       alt_table_row_color($cc);
+           foreach ($pager->columns as $k=>$col) {
+                  $coltype = $col['type'];
+                  $cell = isset($col['name']) ? $row[$col['name']] : '';
+
+                  if (isset($col['fun'])) { // use data input function if defined
+                   $fun = $col['fun']; 
+                   if (method_exists($pager, $fun)) { 
+                               $cell = $pager->$fun($row, $cell);
+                       } elseif (function_exists($fun)) {
+                               $cell = $fun($row, $cell);
+                       } else
+                               $cell = '';
+                  }
+
+                  switch($coltype) { // format column
+                   case 'time':
+                         label_cell($cell, "width=40"); break;
+                   case 'date':
+                         label_cell(sql2date($cell), "align='center' nowrap"); break;
+                   case 'dstamp':      // time stamp displayed as date
+                         label_cell(sql2date(substr($cell, 0, 10)), "align='center' nowrap"); break;
+                   case 'tstamp':      // time stamp - FIX user format
+                         label_cell(sql2date(substr($cell, 0, 10)).
+                         ' '. substr($cell, 10), "align='center'"); break;
+                   case 'percent':
+                         percent_cell($cell); break;
+                   case 'amount':
+                         if ($cell=='')
+                               label_cell('');
+                         else
+                               amount_cell($cell, false); break;
+                   case 'qty':
+                         if ($cell=='')
+                               label_cell('');
+                         else
+                               qty_cell($cell, false, isset($col['dec']) ? $col['dec'] : null); break;
+                   case 'rate':
+                               label_cell(number_format2($cell, user_exrate_dec()), "align=center"); break;
+                   default:
+//                 case 'text':
+                         if (isset( $col['align']))
+                                 label_cell($cell, "align='" . $col['align'] . "'");
+                         else
+                                 label_cell($cell);
+                   case 'skip':        // column not displayed
+                 }
+         }
+           end_row();
+       }
+       //end of while loop
+
+       if($pager->footer_fun) {        // if set footer handler
+               start_row("class='{$pager->footer_class}'");
+               $fun = $pager->footer_fun;
+               if (method_exists($pager, $fun)) { 
+                       $h = $pager->$fun($pager);
+               } elseif (function_exists($fun)) {
+                       $h = $fun($pager);
+               }
+               
+               foreach($h as $c) {      // draw footer columns
+                       $pars = isset($c[1]) ? $c[1] : '';
+                       label_cell($c[0], $pars);
+               }
+               end_row();
+       }
+
+       start_row("class='navibar'");
+       $colspan = count($pager->columns);
+        if($pager->rec_count) {
+               echo "<td colspan=$colspan class='navibar' style='border:none;padding:3px;'>";
+               echo "<div style='float:right;'>";
+               $but_pref = $pager->name.'_page_';
+           start_table();
+               start_row();
+               echo navi_button_cell($but_pref.'first', _('First'), $pager->first_page, 'right');
+               echo navi_button_cell($but_pref.'prev', _('Prev'), $pager->prev_page,'right');
+               echo navi_button_cell($but_pref.'next', _('Next'), $pager->next_page,'right');
+               echo navi_button_cell($but_pref.'last', _('Last'), $pager->last_page, 'right');
+               end_row(); 
+               end_table();
+               echo "</div>";
+               $from = ($pager->curr_page-1)*$pager->page_len+1;
+               $to = $from + $pager->page_len - 1;
+               if ($to > $pager->rec_count)
+                 $to = $pager->rec_count;
+               $all = $pager->rec_count;
+               echo sprintf( _('Records %d-%d of %d'), $from, $to, $all);
+               echo "</td>";
+       } else {
+         label_cell( _('No records'), "colspan=$colspan class='navibar'");
+       }
+       end_row();
+
+       end_table();
+
+   if (isset($pager->marker_txt))
+               display_note($pager->marker_txt, 0, 1, "class='$pager->notice_class'");
+
+  div_end();
+  return true;
+}
+
+
+?>
\ No newline at end of file
diff --git a/installed_extensions.php b/installed_extensions.php
new file mode 100644 (file)
index 0000000..0022561
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+/* How to make new entries here
+
+-- if adding extensions at the beginning of the list, make sure it's index is set to 0 (it has ' 0 => ')
+-- 'app_file' is the application file name to be put into folder applications
+-- 'name' is the name of the extension module. Will become the index of the application
+-- 'title' is the Menu Title
+-- 'folder' is the folder where the extension files exist
+
+*/
+
+$installed_extensions = array ();
+
+// example
+/*
+$installed_extensions = array (
+       0 => array ('app_file' => 'organizer.php', 'name' => 'organizer', 'title' => 'Organizer', 'folder' => 'organizer'),
+       array ('app_file' => 'payroll.php', 'name' => 'payroll', 'title' => 'Payroll', 'folder' => 'payroll')
+       );
+*/     
+?>
\ No newline at end of file
diff --git a/inventory/includes/db/items_codes_db.inc b/inventory/includes/db/items_codes_db.inc
new file mode 100644 (file)
index 0000000..d262ee7
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+/*
+       item_codes table is used to store both multiply foreign codes and 
+       sale kits definition.
+*/
+function update_item_code($id, $item_code, $stock_id, $description, $category, $qty, $foreign=0)
+{
+       $sql = "UPDATE ".TB_PREF."item_codes SET
+               item_code = ".db_escape($item_code).",
+               stock_id = ".db_escape($stock_id).",
+               description = ".db_escape($description).",
+               category_id = $category,
+               quantity = ".db_escape($qty).",
+               is_foreign = ".db_escape($foreign)."
+               WHERE ";
+                       
+       if ($id == -1) // update with unknown $id i.e. from items table editor
+               $sql .= "item_code = ".db_escape($item_code)
+               ." AND stock_id = ".db_escape($stock_id);
+       else
+               $sql .= "id = $id";
+
+       db_query($sql,"an item code could not be updated");
+}
+
+function add_item_code($item_code, $stock_id, $description, $category, $qty, $foreign=0)
+{
+       $sql = "INSERT INTO ".TB_PREF."item_codes
+                       (item_code, stock_id, description, category_id, quantity, is_foreign) 
+                       VALUES( ".db_escape($item_code).",".db_escape($stock_id).",
+                       ".db_escape($description).",$category,".db_escape($qty).",".$foreign.")";
+
+       db_query($sql,"an item code could not be added");
+}
+
+function delete_item_code($id)
+{
+       $sql="DELETE FROM ".TB_PREF."item_codes WHERE id='$id'";
+       db_query($sql,"an item code could not be deleted");
+}
+
+function get_item_code($id)
+{
+       $sql="SELECT * FROM ".TB_PREF."item_codes WHERE id='$id'";
+
+       $result = db_query($sql,"item code could not be retrieved");
+
+       return db_fetch($result);
+}
+
+function get_all_item_codes($stock_id, $foreign=1)
+{
+       $sql="SELECT i.*, c.description as cat_name FROM "
+               .TB_PREF."item_codes as i,"
+               .TB_PREF."stock_category as c
+               WHERE stock_id='$stock_id'
+               AND i.category_id=c.category_id
+               AND i.is_foreign=$foreign";
+
+       $result = db_query($sql,"all item codes could not be retrieved");
+
+       return $result;
+}
+
+function delete_item_kit($item_code)
+{
+       $sql="DELETE FROM ".TB_PREF."item_codes WHERE item_code='$item_code'";
+       db_query($sql,"an item kit could not be deleted");
+}
+
+function get_item_kit($item_code)
+{
+       $sql="SELECT DISTINCT kit.*, item.units, comp.description as comp_name 
+               FROM "
+               .TB_PREF."item_codes kit,"
+               .TB_PREF."item_codes comp
+               LEFT JOIN "
+               .TB_PREF."stock_master item
+               ON 
+                       item.stock_id=comp.item_code
+               WHERE
+                       kit.stock_id=comp.item_code
+                       AND kit.item_code='$item_code'";
+
+       $result = db_query($sql,"item kit could not be retrieved");
+
+       return $result;
+}
+
+function get_item_code_dflts($stock_id)
+{
+       $sql = "SELECT units, decimals, description, category_id
+               FROM ".TB_PREF."stock_master,".TB_PREF."item_units
+               WHERE stock_id='$stock_id'";
+
+       $result = db_query($sql,"item code defaults could not be retrieved");
+       return db_fetch($result);
+}
+//
+//     Check if kit contains given item, optionally recursive.
+//
+function check_item_in_kit($old_id, $kit_code, $item_code, $recurse=false)
+{
+       $result = get_item_kit($kit_code);
+       if ($result != 0)
+       {
+               while ($myrow = db_fetch($result))
+               {
+                       if ($myrow['id'] == $old_id) 
+                               continue;
+                               
+                       if ($myrow['stock_id'] == $item_code)
+                       {
+                               return 1;
+                       }
+
+                       if ($recurse && $myrow['item_code'] != $myrow['stock_id']
+                               && check_item_in_kit($old_id, $item_code, $myrow['stock_id'], true))
+                       {
+                               return 1;
+                       }
+               }
+       }
+       return 0;
+}
+
+function get_kit_props($kit_code)
+{
+       $sql = "SELECT description, category_id FROM ".TB_PREF."item_codes "
+               . " WHERE item_code='$kit_code'";
+       $res = db_query($sql, "kit name query failed");
+       return db_fetch($res);
+}
+
+function update_kit_props($kit_code, $name, $category)
+{
+       $sql = "UPDATE ".TB_PREF."item_codes SET description="
+               . db_escape($name).",category_id=".db_escape($category)         
+               . " WHERE item_code='$kit_code'";
+       db_query($sql, "kit name update failed");
+}
+
+function get_where_used($item_code)
+{
+       $sql = "SELECT item_code, description FROM "
+               .TB_PREF."item_codes "
+               . " WHERE stock_id='$item_code'
+                       AND item_code!='$item_code'";
+       return db_query($sql, "where used query failed");
+}
+?>
\ No newline at end of file
diff --git a/inventory/manage/item_codes.php b/inventory/manage/item_codes.php
new file mode 100644 (file)
index 0000000..4b9fcd7
--- /dev/null
@@ -0,0 +1,187 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+$page_security = 11;
+$path_to_root="../..";
+include_once($path_to_root . "/includes/session.inc");
+
+page(_("Foreign Item Codes"));
+
+include_once($path_to_root . "/includes/date_functions.inc");
+include_once($path_to_root . "/includes/ui.inc");
+include_once($path_to_root . "/includes/manufacturing.inc");
+include_once($path_to_root . "/includes/data_checks.inc");
+
+check_db_has_purchasable_items(_("There are no inventory items defined in the system."));
+
+simple_page_mode(true);
+//--------------------------------------------------------------------------------------------------
+
+if ($Mode=='ADD_ITEM' || $Mode=='UPDATE_ITEM')
+{
+
+       $input_error = 0;
+       if ($_POST['stock_id'] == "" || !isset($_POST['stock_id']))
+       {
+       $input_error = 1;
+       display_error( _("There is no item selected."));
+               set_focus('stock_id');
+       }
+       elseif (!check_num('quantity', 0))
+       {
+       $input_error = 1;
+       display_error( _("The price entered was not numeric."));
+               set_focus('quantity');
+       }
+       elseif ($_POST['description'] == '')
+       {
+       $input_error = 1;
+       display_error( _("Item code description cannot be empty."));
+               set_focus('description');
+       }
+       elseif($selected_id == -1)
+       {
+               $kit = get_item_kit($_POST['item_code']);
+       if (db_num_rows($kit)) {
+                       $input_error = 1;
+               display_error( _("This item code is already assigned to stock item or sale kit."));
+                       set_focus('item_code');
+               }
+       }
+       
+       if ($input_error == 0)
+       {
+       if ($Mode == 'ADD_ITEM') 
+               {
+                       add_item_code($_POST['item_code'], $_POST['stock_id'],
+                               $_POST['description'], $_POST['category_id'], $_POST['quantity'], 1); 
+
+               display_notification(_("New item code has been added."));
+               } else
+               {
+                       update_item_code($selected_id, $_POST['item_code'], $_POST['stock_id'],
+                               $_POST['description'], $_POST['category_id'], $_POST['quantity'], 1); 
+
+               display_notification(_("Item code has been updated."));
+               }
+               $Mode = 'RESET';
+       }
+}
+
+//--------------------------------------------------------------------------------------------------
+
+if ($Mode == 'Delete')
+{
+       delete_item_code($selected_id);
+       
+       display_notification(_("Item code has been sucessfully deleted."));
+       $Mode = 'RESET';
+}
+
+if ($Mode == 'RESET')
+{
+       $selected_id = -1;
+       unset($_POST);
+}
+
+if (list_updated('stock_id')) 
+       $Ajax->activate('_page_body');
+
+//--------------------------------------------------------------------------------------------------
+
+start_form(false, true);
+
+if (!isset($_POST['stock_id']))
+       $_POST['stock_id'] = get_global_stock_item();
+
+echo "<center>" . _("Item:"). "&nbsp;";
+stock_purchasable_items_list('stock_id', $_POST['stock_id'], false, true);
+
+echo "<hr></center>";
+
+set_global_stock_item($_POST['stock_id']);
+
+$result = get_item_code_dflts($_POST['stock_id']);
+$dec = $result['decimals'];
+$units = $result['units'];
+$dflt_desc = $result['description'];
+$dflt_cat = $result['category_id'];
+
+$result = get_all_item_codes($_POST['stock_id']);
+div_start('code_table');
+       start_table("$table_style width=60%");
+
+       $th = array(_("EAN/UPC Code"), _("Quantity"), _("Units"), 
+               _("Description"),_("Category"), "", "");
+
+        table_header($th);
+
+        $k = $j = 0; //row colour counter
+
+        while ($myrow = db_fetch($result))
+        {
+                       alt_table_row_color($k);
+
+            label_cell($myrow["item_code"]);
+            qty_cell($myrow["quantity"], $dec);
+            label_cell($units);
+            label_cell($myrow["description"]);
+            label_cell($myrow["cat_name"]);
+                       edit_button_cell("Edit".$myrow['id'], _("Edit"));
+                       edit_button_cell("Delete".$myrow['id'], _("Delete"));
+            end_row();
+
+            $j++;
+            If ($j == 12)
+            {
+               $j = 1;
+                       table_header($th);
+            } //end of page full new headings
+        } //end of while loop
+
+       end_table();
+div_end();
+
+//-----------------------------------------------------------------------------------------------
+
+if ($Mode =='Edit')
+{
+       $myrow = get_item_code($selected_id);
+       $_POST['item_code'] = $myrow["item_code"];
+    $_POST['quantity'] = $myrow["quantity"];
+    $_POST['description'] = $myrow["description"];
+    $_POST['category_id'] = $myrow["category_id"];
+}
+ else {
+    $_POST['quantity'] = 1;
+    $_POST['description'] = $dflt_desc;
+    $_POST['category_id'] = $dflt_cat;
+}
+
+echo "<br>";
+hidden('selected_id', $selected_id);
+start_table($table_style2);
+
+hidden('code_id', $selected_id);
+
+text_row(_("UPC/EAN code:"), 'item_code', null, 20, 21);
+qty_row(_("Quantity:"), 'quantity', null, '', $units, $dec);
+text_row(_("Description:"), 'description', null, 50, 200);
+stock_categories_list_row(_("Category:"), 'category_id', null);
+
+end_table(1);
+
+submit_add_or_update_center($selected_id == -1, '', true);
+
+end_form();
+end_page();
+
+?>
diff --git a/inventory/manage/sales_kits.php b/inventory/manage/sales_kits.php
new file mode 100644 (file)
index 0000000..6040fcd
--- /dev/null
@@ -0,0 +1,254 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+$page_security = 11;
+$path_to_root="../..";
+include_once($path_to_root . "/includes/session.inc");
+
+page(_("Sales Kits & Alias Codes"));
+
+include_once($path_to_root . "/includes/date_functions.inc");
+include_once($path_to_root . "/includes/ui.inc");
+include_once($path_to_root . "/includes/data_checks.inc");
+
+include_once($path_to_root . "/includes/manufacturing.inc");
+
+check_db_has_stock_items(_("There are no items defined in the system."));
+
+simple_page_mode(true);
+/*
+if (isset($_GET['item_code']))
+{
+       $_POST['item_code'] = $_GET['item_code'];
+       $selected_kit =  $_GET['item_code'];
+}
+*/
+//--------------------------------------------------------------------------------------------------
+function display_kit_items($selected_kit)
+{
+       global $table_style;
+
+       $result = get_item_kit($selected_kit);
+div_start('bom');
+       start_table("$table_style width=60%");
+       $th = array(_("Stock Item"), _("Description"), _("Quantity"), _("Units"),
+               '','');
+       table_header($th);
+
+       $k = 0;
+       while ($myrow = db_fetch($result))
+       {
+
+               alt_table_row_color($k);
+
+               label_cell($myrow["stock_id"]);
+               label_cell($myrow["comp_name"]);
+        qty_cell($myrow["quantity"], false, 
+                       $myrow["units"] == '' ? 0 : get_qty_dec($myrow["comp_name"]));
+        label_cell($myrow["units"] == '' ? _('kit') : $myrow["units"]);
+               edit_button_cell("Edit".$myrow['id'], _("Edit"));
+               delete_button_cell("Delete".$myrow['id'], _("Delete"));
+        end_row();
+
+       } //END WHILE LIST LOOP
+       end_table();
+div_end();
+}
+
+//--------------------------------------------------------------------------------------------------
+
+function update_component($kit_code, $selected_item)
+{
+       global $Mode, $Ajax, $selected_kit;
+       
+       if (!check_num('quantity', 0))
+       {
+               display_error(_("The quantity entered must be numeric and greater than zero."));
+               set_focus('quantity');
+               return;
+       }
+       elseif ($_POST['description'] == '')
+       {
+       display_error( _("Item code description cannot be empty."));
+               set_focus('description');
+               return;
+       }
+       elseif ($selected_item == -1)   // adding new item or new alias/kit
+       {
+               if (get_post('item_code') == '') { // New kit/alias definition
+                       $kit = get_item_kit($_POST['item_code']);
+               if (db_num_rows($kit)) {
+                               $input_error = 1;
+                       display_error( _("This item code is already assigned to stock item or sale kit."));
+                               set_focus('kit_code');
+                               return;
+                       }
+                       if (get_post('kit_code') == '') {
+                       display_error( _("Kit/alias code cannot be empty."));
+                               set_focus('kit_code');
+                               return;
+                       }
+               }
+       }
+
+       if (check_item_in_kit($selected_item, $kit_code, $_POST['component'], true)) {
+               display_error(_("The selected component contains directly or on any lower level the kit under edition. Recursive kits are not allowed."));
+               set_focus('component');
+               return;
+       }
+
+               /*Now check to see that the component is not already in the kit */
+       if (check_item_in_kit($selected_item, $kit_code, $_POST['component'])) {
+               display_error(_("The selected component is already in this kit. You can modify it's quantity but it cannot appear more than once in the same kit."));
+               set_focus('component');
+               return;
+       }
+       if ($selected_item == -1) { // new item alias/kit
+               if ($_POST['item_code']=='') {
+                       $kit_code = $_POST['kit_code'];
+                       $selected_kit = $_POST['item_code'] = $kit_code;
+                       $msg = _("New alias code has been created.");
+               } 
+                else
+                       $msg =_("New component has been added to selected kit.");
+
+               add_item_code( $kit_code, get_post('component'), get_post('description'),
+                        get_post('category'), input_num('quantity'), 0);
+               display_notification($msg);
+
+       } else {
+               $props = get_kit_props($_POST['item_code']);
+               update_item_code($selected_item, $kit_code, get_post('component'),
+                       $props['description'], $props['category_id'], input_num('quantity'), 0);
+               display_notification(_("Component of selected kit has been updated."));
+       }
+       $Mode = 'RESET';
+       $Ajax->activate('_page_body');
+}
+
+//--------------------------------------------------------------------------------------------------
+
+if (get_post('update_name')) {
+       update_kit_props(get_post('item_code'), get_post('description'), get_post('category'));
+       display_notification(_('Kit common properties has been updated'));
+       $Ajax->activate('_page_body');
+}
+
+if ($Mode=='ADD_ITEM' || $Mode=='UPDATE_ITEM')
+       update_component($_POST['item_code'], $selected_id);
+
+if ($Mode == 'Delete')
+{
+       // Before removing last component from selected kit check 
+       // if selected kit is not included in any other kit. 
+       // 
+       $other_kits = get_where_used($_POST['item_code']);
+       $num_kits = db_num_rows($other_kits);
+
+       $kit = get_item_kit($_POST['item_code']);
+       if ((db_num_rows($kit) == 1) && $num_kits) {
+
+               $msg = _("This item cannot be deleted because it is the last item in the kit used by following kits")
+                       .':<br>';
+
+               while($num_kits--) {
+                       $kit = db_fetch($other_kits);
+                       $msg .= "'".$kit[0]."'";
+                       if ($num_kits) $msg .= ',';
+               }
+               display_error($msg);
+       } else {
+               delete_item_code($selected_id);
+               display_notification(_("The component item has been deleted from this bom"));
+               $Mode = 'RESET';
+       }
+}
+
+if ($Mode == 'RESET')
+{
+       $selected_id = -1;
+       unset($_POST['quantity']);
+       unset($_POST['component']);
+}
+
+//--------------------------------------------------------------------------------------------------
+
+start_form(false, true);
+
+echo "<center>" . _("Select a sale kit:") . "&nbsp;";
+sales_kits_list('item_code', null, _('New kit'), true);
+echo "</center><br>";
+$props = get_kit_props($_POST['item_code']);
+
+if (isset($_POST['_item_code_update'])) {
+       if (get_post('item_code') == '')
+               $_POST['description'] = '';
+       $Ajax->activate('_page_body');
+}
+
+$selected_kit = $_POST['item_code'];
+//----------------------------------------------------------------------------------
+if (get_post('item_code') == '') {
+// New sales kit entry
+       start_table($table_style2);
+       text_row(_("Alias/kit code:"), 'kit_code', null, 20, 21);
+} else
+{ // Kit selected so display bom or edit component
+       $_POST['description'] = $props['description'];
+       $_POST['category'] = $props['category_id'];
+       start_table($table_style2);
+       text_row(_("Description:"), 'description', null, 50, 200);
+       stock_categories_list_row(_("Category:"), 'category', null);
+       submit_row('update_name', _("Update"), false, 'align=center colspan=2', _('Update kit/alias name'), true);
+       end_row();
+       end_table(1);
+       display_kit_items($selected_kit);
+       echo '<br>';
+       start_table($table_style2);
+}
+
+       if ($Mode == 'Edit') {
+               $myrow = get_item_code($selected_id);
+               $_POST['component'] = $myrow["stock_id"];
+               $_POST['quantity'] = number_format2($myrow["quantity"], get_qty_dec($myrow["stock_id"]));
+       }
+       hidden("selected_id", $selected_id);
+       
+       sales_local_items_list_row(_("Component:"),'component', null, false, true);
+
+//     if (get_post('description') == '')
+//             $_POST['description'] = get_kit_name($_POST['component']);
+       if (get_post('item_code') == '') { // new kit/alias
+               $_POST['description'] = $props['description'];
+               $_POST['category'] = $props['category_id'];
+               text_row(_("Description:"), 'description', null, 50, 200);
+               stock_categories_list_row(_("Category:"), 'category', null);
+       }
+       $res = get_item_edit_info(get_post('component'));
+       $dec =  $res["decimals"] == '' ? 0 : $res["decimals"];
+       $units = $res["units"] == '' ? _('kits') : $res["units"];
+       if (list_updated('component')) 
+       {
+               $_POST['quantity'] = number_format2(1, $dec);
+               $Ajax->activate('quantity');
+               $Ajax->activate('category');
+       }
+       
+       qty_row(_("Quantity:"), 'quantity', number_format2(1, $dec), '', $units, $dec);
+
+       end_table(1);
+       submit_add_or_update_center($selected_id == -1, '', true);
+       end_form();
+//----------------------------------------------------------------------------------
+
+end_page();
+
+?>
diff --git a/js/reconcile.js b/js/reconcile.js
new file mode 100644 (file)
index 0000000..5cac50e
--- /dev/null
@@ -0,0 +1,38 @@
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+function focus_amount(i) {
+    save_focus(i);
+       i.setAttribute('_last', get_amount(i.name));
+}
+
+function blur_amount(i) {
+       var change = get_amount(i.name);
+
+       price_format(i.name, change, user.pdec);
+       change = change-i.getAttribute('_last');
+       if (i.name=='beg_balance')
+               change = -change;
+
+       price_format('difference', get_amount('difference',1,1)+change, user.pdec);
+}
+
+var balances = {
+       '.amount': function(e) {
+               e.onblur = function() {
+                       blur_amount(this);
+                 };
+               e.onfocus = function() {
+                       focus_amount(this);
+               };
+       }
+}
+
+Behaviour.register(balances);
diff --git a/lang/new_language_template/locale.inc b/lang/new_language_template/locale.inc
new file mode 100644 (file)
index 0000000..0ff96cc
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+/*
+       This file contains templates for all country specific functions.
+       If your locale needs special functionality provided by hook functions
+       copy this file to respective lang/xx_XX directory and edit templates below.
+       You can safely remove not used function templates.
+*/
+?>
\ No newline at end of file
diff --git a/reporting/includes/Workbook.php b/reporting/includes/Workbook.php
new file mode 100644 (file)
index 0000000..1f53c2a
--- /dev/null
@@ -0,0 +1,8373 @@
+<?php
+/*
+*  Module written/ported by Xavier Noguer <xnoguer@rezebra.com>
+*
+*  The majority of this is _NOT_ my code.  I simply ported it from the
+*  PERL Spreadsheet::WriteExcel module.
+*
+*  The author of the Spreadsheet::WriteExcel module is John McNamara
+*  <jmcnamara@cpan.org>
+*
+*  I _DO_ maintain this code, and John McNamara has nothing to do with the
+*  porting of this code to PHP.  Any questions directly related to this
+*  class library should be directed to me.
+*
+*  License Information:
+*
+*    Spreadsheet_Excel_Writer:  A library for generating Excel Spreadsheets
+*    Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
+*
+*    This library is free software; you can redistribute it and/or
+*    modify it under the terms of the GNU Lesser General Public
+*    License as published by the Free Software Foundation; either
+*    version 2.1 of the License, or (at your option) any later version.
+*
+*    This library is distributed in the hope that it will be useful,
+*    but WITHOUT ANY WARRANTY; without even the implied warranty of
+*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+*    Lesser General Public License for more details.
+*
+*    You should have received a copy of the GNU Lesser General Public
+*    License along with this library; if not, write to the Free Software
+*    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+*/
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_ADD token identifier for character "+"
+*/
+define('SPREADSHEET_EXCEL_WRITER_ADD', "+");
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_SUB token identifier for character "-"
+*/
+define('SPREADSHEET_EXCEL_WRITER_SUB', "-");
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_MUL token identifier for character "*"
+*/
+define('SPREADSHEET_EXCEL_WRITER_MUL', "*");
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_DIV token identifier for character "/"
+*/
+define('SPREADSHEET_EXCEL_WRITER_DIV', "/");
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_OPEN token identifier for character "("
+*/
+define('SPREADSHEET_EXCEL_WRITER_OPEN', "(");
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_CLOSE token identifier for character ")"
+*/
+define('SPREADSHEET_EXCEL_WRITER_CLOSE', ")");
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_COMA token identifier for character ","
+*/
+define('SPREADSHEET_EXCEL_WRITER_COMA', ",");
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_SEMICOLON token identifier for character ";"
+*/
+define('SPREADSHEET_EXCEL_WRITER_SEMICOLON', ";");
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_GT token identifier for character ">"
+*/
+define('SPREADSHEET_EXCEL_WRITER_GT', ">");
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_LT token identifier for character "<"
+*/
+define('SPREADSHEET_EXCEL_WRITER_LT', "<");
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_LE token identifier for character "<="
+*/
+define('SPREADSHEET_EXCEL_WRITER_LE', "<=");
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_GE token identifier for character ">="
+*/
+define('SPREADSHEET_EXCEL_WRITER_GE', ">=");
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_EQ token identifier for character "="
+*/
+define('SPREADSHEET_EXCEL_WRITER_EQ', "=");
+
+/**
+* @const SPREADSHEET_EXCEL_WRITER_NE token identifier for character "<>"
+*/
+define('SPREADSHEET_EXCEL_WRITER_NE', "<>");
+
+$encoding_string='';
+/**
+* Class for creating OLE streams for Excel Spreadsheets
+*
+* @author Xavier Noguer <xnoguer@rezebra.com>
+* @package Spreadsheet_WriteExcel
+*/
+class OLEwriter
+{
+    /**
+    * Filename for the OLE stream
+    * @var string
+    * @see _initialize()
+    */
+    var $_OLEfilename;
+
+    /**
+    * Filehandle for the OLE stream
+    * @var resource
+    */
+    var $_filehandle;
+
+    /**
+    * Name of the temporal file in case OLE stream goes to stdout
+    * @var string
+    */
+    var $_tmp_filename;
+
+    /**
+    * Variable for preventing closing two times
+    * @var integer
+    */
+    var $_fileclosed;
+
+    /**
+    * Size of the data to be written to the OLE stream
+    * @var integer
+    */
+    var $_biffsize;
+
+    /**
+    * Real data size to be written to the OLE stream
+    * @var integer
+    */
+    var $_booksize;
+
+    /**
+    * Number of big blocks in the OLE stream
+    * @var integer
+    */
+    var $_big_blocks;
+
+    /**
+    * Number of list blocks in the OLE stream
+    * @var integer
+    */
+    var $_list_blocks;
+
+    /**
+    * Number of big blocks in the OLE stream
+    * @var integer
+    */
+    var $_root_start;
+
+    /**
+    * Constructor for the OLEwriter class
+    *
+    * @param string $OLEfilename the name of the file for the OLE stream
+    */
+    function OLEwriter($OLEfilename)
+    {
+        $this->_OLEfilename  = $OLEfilename;
+        $this->_filehandle   = '';
+        $this->_tmp_filename = '';
+        $this->_fileclosed   = 0;
+        $this->_biff_only    = 0;
+        //$this->_size_allowed = 0;
+        $this->_biffsize     = 0;
+        $this->_booksize     = 0;
+        $this->_big_blocks   = 0;
+        $this->_list_blocks  = 0;
+        $this->_root_start   = 0;
+        //$this->_block_count  = 4;
+        $this->_initialize();
+    }
+
+    /**
+    * Check for a valid filename and store the filehandle.
+    * Filehandle "-" writes to STDOUT
+    *
+    * @access private
+    */
+    function _initialize()
+    {
+        $OLEfile = $this->_OLEfilename;
+
+        if (($OLEfile == '-') or ($OLEfile == '')) {
+            $this->_tmp_filename = tempnam("/tmp", "OLEwriter");
+            $fh = fopen($this->_tmp_filename, "wb");
+            if ($fh == false) {
+                die("Can't create temporary file.");
+            }
+        } else {
+            // Create a new file, open for writing (in binmode)
+            $fh = fopen($OLEfile, "wb");
+            if ($fh == false) {
+                die("Can't open $OLEfile. It may be in use or protected.");
+            }
+        }
+
+        // Store filehandle
+        $this->_filehandle = $fh;
+    }
+
+
+    /**
+    * Set the size of the data to be written to the OLE stream.
+    * The maximun size comes from this:
+    *   $big_blocks = (109 depot block x (128 -1 marker word)
+    *                 - (1 x end words)) = 13842
+    *   $maxsize    = $big_blocks * 512 bytes = 7087104
+    *
+    * @access public
+    * @see Spreadsheet_Excel_Writer_Workbook::store_OLE_file()
+    * @param integer $biffsize The size of the data to be written to the OLE stream
+    * @return integer 1 for success
+    */
+    function setSize($biffsize)
+    {
+        $maxsize = 7087104; // TODO: extend max size
+
+        if ($biffsize > $maxsize) {
+            die("Maximum file size, $maxsize, exceeded.");
+        }
+
+        $this->_biffsize = $biffsize;
+        // Set the min file size to 4k to avoid having to use small blocks
+        if ($biffsize > 4096) {
+            $this->_booksize = $biffsize;
+        } else {
+            $this->_booksize = 4096;
+        }
+        //$this->_size_allowed = 1;
+        return(1);
+    }
+
+
+    /**
+    * Calculate various sizes needed for the OLE stream
+    *
+    * @access private
+    */
+    function _calculateSizes()
+    {
+        $datasize = $this->_booksize;
+        if ($datasize % 512 == 0) {
+            $this->_big_blocks = $datasize/512;
+        } else {
+            $this->_big_blocks = floor($datasize/512) + 1;
+        }
+        // There are 127 list blocks and 1 marker blocks for each big block
+        // depot + 1 end of chain block
+        $this->_list_blocks = floor(($this->_big_blocks)/127) + 1;
+        $this->_root_start  = $this->_big_blocks;
+    }
+
+    /**
+    * Write root entry, big block list and close the filehandle.
+    * This routine is used to explicitly close the open filehandle without
+    * having to wait for DESTROY.
+    *
+    * @access public
+    * @see Spreadsheet_Excel_Writer_Workbook::store_OLE_file()
+    */
+    function close()
+    {
+        //return if not $this->{_size_allowed};
+        $this->_writePadding();
+        $this->_writePropertyStorage();
+        $this->_writeBigBlockDepot();
+        // Close the filehandle
+        fclose($this->_filehandle);
+        if (($this->_OLEfilename == '-') or ($this->_OLEfilename == '')) {
+            $fh = fopen($this->_tmp_filename, "rb");
+            if ($fh == false) {
+                die("Can't read temporary file.");
+            }
+            fpassthru($fh);
+            @unlink($this->_tmp_filename);
+        }
+        $this->_fileclosed = 1;
+    }
+
+
+    /**
+    * Write BIFF data to OLE file.
+    *
+    * @param string $data string of bytes to be written
+    */
+    function write($data)
+    {
+        fwrite($this->_filehandle, $data, strlen($data));
+    }
+
+
+    /**
+    * Write OLE header block.
+    */
+    function writeHeader()
+    {
+        $this->_calculateSizes();
+        $root_start      = $this->_root_start;
+        $num_lists       = $this->_list_blocks;
+        $id              = pack("nnnn", 0xD0CF, 0x11E0, 0xA1B1, 0x1AE1);
+        $unknown1        = pack("VVVV", 0x00, 0x00, 0x00, 0x00);
+        $unknown2        = pack("vv",   0x3E, 0x03);
+        $unknown3        = pack("v",    -2);
+        $unknown4        = pack("v",    0x09);
+        $unknown5        = pack("VVV",  0x06, 0x00, 0x00);
+        $num_bbd_blocks  = pack("V",    $num_lists);
+        $root_startblock = pack("V",    $root_start);
+        $unknown6        = pack("VV",   0x00, 0x1000);
+        $sbd_startblock  = pack("V",    -2);
+        $unknown7        = pack("VVV",  0x00, -2 ,0x00);
+        $unused          = pack("V",    -1);
+
+        fwrite($this->_filehandle, $id);
+        fwrite($this->_filehandle, $unknown1);
+        fwrite($this->_filehandle, $unknown2);
+        fwrite($this->_filehandle, $unknown3);
+        fwrite($this->_filehandle, $unknown4);
+        fwrite($this->_filehandle, $unknown5);
+        fwrite($this->_filehandle, $num_bbd_blocks);
+        fwrite($this->_filehandle, $root_startblock);
+        fwrite($this->_filehandle, $unknown6);
+        fwrite($this->_filehandle, $sbd_startblock);
+        fwrite($this->_filehandle, $unknown7);
+
+        for ($i=1; $i <= $num_lists; $i++) {
+            $root_start++;
+            fwrite($this->_filehandle, pack("V",$root_start));
+        }
+        for ($i = $num_lists; $i <=108; $i++) {
+            fwrite($this->_filehandle, $unused);
+        }
+    }
+
+
+    /**
+    * Write big block depot.
+    *
+    * @access private
+    */
+    function _writeBigBlockDepot()
+    {
+        $num_blocks   = $this->_big_blocks;
+        $num_lists    = $this->_list_blocks;
+        $total_blocks = $num_lists *128;
+        $used_blocks  = $num_blocks + $num_lists +2;
+
+        $marker       = pack("V", -3);
+        $end_of_chain = pack("V", -2);
+        $unused       = pack("V", -1);
+
+        for ($i = 1; $i < $num_blocks; $i++) {
+            fwrite($this->_filehandle, pack("V",$i));
+        }
+
+        fwrite($this->_filehandle, $end_of_chain);
+        fwrite($this->_filehandle, $end_of_chain);
+        for ($i = 0; $i < $num_lists; $i++) {
+            fwrite($this->_filehandle, $marker);
+        }
+
+        for ($i = $used_blocks; $i <= $total_blocks; $i++) {
+            fwrite($this->_filehandle, $unused);
+        }
+    }
+
+    /**
+    * Write property storage. TODO: add summary sheets
+    *
+    * @access private
+    */
+    function _writePropertyStorage()
+    {
+        //$rootsize = -2;
+        /***************  name         type   dir start size */
+        $this->_writePps("Root Entry", 0x05,   1,   -2, 0x00);
+        $this->_writePps("Book",       0x02,  -1, 0x00, $this->_booksize);
+        $this->_writePps('',           0x00,  -1, 0x00, 0x0000);
+        $this->_writePps('',           0x00,  -1, 0x00, 0x0000);
+    }
+
+/**
+* Write property sheet in property storage
+*
+* @param string  $name  name of the property storage.
+* @param integer $type  type of the property storage.
+* @param integer $dir   dir of the property storage.
+* @param integer $start start of the property storage.
+* @param integer $size  size of the property storage.
+* @access private
+*/
+    function _writePps($name, $type, $dir, $start, $size)
+    {
+        $length  = 0;
+        $rawname = '';
+
+        if ($name != '') {
+            $name = $name . "\0";
+            $name_length = strlen($name);
+            for ($i = 0; $i < $name_length; $i++) {
+                // Simulate a Unicode string
+                $rawname .= pack("H*",dechex(ord($name{$i}))).pack("C",0);
+            }
+            $length = strlen($name) * 2;
+        }
+
+        $zero            = pack("C",  0);
+        $pps_sizeofname  = pack("v",  $length);    // 0x40
+        $pps_type        = pack("v",  $type);      // 0x42
+        $pps_prev        = pack("V",  -1);         // 0x44
+        $pps_next        = pack("V",  -1);         // 0x48
+        $pps_dir         = pack("V",  $dir);       // 0x4c
+
+        $unknown1        = pack("V",  0);
+
+        $pps_ts1s        = pack("V",  0);          // 0x64
+        $pps_ts1d        = pack("V",  0);          // 0x68
+        $pps_ts2s        = pack("V",  0);          // 0x6c
+        $pps_ts2d        = pack("V",  0);          // 0x70
+        $pps_sb          = pack("V",  $start);     // 0x74
+        $pps_size        = pack("V",  $size);      // 0x78
+
+
+        fwrite($this->_filehandle, $rawname);
+        for ($i = 0; $i < (64 -$length); $i++) {
+            fwrite($this->_filehandle, $zero);
+        }
+        fwrite($this->_filehandle, $pps_sizeofname);
+        fwrite($this->_filehandle, $pps_type);
+        fwrite($this->_filehandle, $pps_prev);
+        fwrite($this->_filehandle, $pps_next);
+        fwrite($this->_filehandle, $pps_dir);
+        for ($i = 0; $i < 5; $i++) {
+            fwrite($this->_filehandle, $unknown1);
+        }
+        fwrite($this->_filehandle, $pps_ts1s);
+        fwrite($this->_filehandle, $pps_ts1d);
+        fwrite($this->_filehandle, $pps_ts2d);
+        fwrite($this->_filehandle, $pps_ts2d);
+        fwrite($this->_filehandle, $pps_sb);
+        fwrite($this->_filehandle, $pps_size);
+        fwrite($this->_filehandle, $unknown1);
+    }
+
+    /**
+    * Pad the end of the file
+    *
+    * @access private
+    */
+    function _writePadding()
+    {
+        $biffsize = $this->_biffsize;
+        if ($biffsize < 4096) {
+            $min_size = 4096;
+        } else {
+            $min_size = 512;
+        }
+        if ($biffsize % $min_size != 0) {
+            $padding  = $min_size - ($biffsize % $min_size);
+            for ($i = 0; $i < $padding; $i++) {
+                fwrite($this->_filehandle, "\0");
+            }
+        }
+    }
+}
+
+/**
+* Class for writing Excel BIFF records.
+*
+* From "MICROSOFT EXCEL BINARY FILE FORMAT" by Mark O'Brien (Microsoft Corporation):
+*
+* BIFF (BInary File Format) is the file format in which Excel documents are
+* saved on disk.  A BIFF file is a complete description of an Excel document.
+* BIFF files consist of sequences of variable-length records. There are many
+* different types of BIFF records.  For example, one record type describes a
+* formula entered into a cell; one describes the size and location of a
+* window into a document; another describes a picture format.
+*
+* @author   Xavier Noguer <xnoguer@php.net>
+* @category FileFormats
+* @package  Spreadsheet_Excel_Writer
+*/
+
+class Spreadsheet_Excel_Writer_BIFFwriter
+{
+    /**
+    * The BIFF/Excel version (5).
+    * @var integer
+    */
+    var $_BIFF_version = 0x0500;
+
+    /**
+    * The byte order of this architecture. 0 => little endian, 1 => big endian
+    * @var integer
+    */
+    var $_byte_order;
+
+    /**
+    * The string containing the data of the BIFF stream
+    * @var string
+    */
+    var $_data;
+
+    /**
+    * The size of the data in bytes. Should be the same as strlen($this->_data)
+    * @var integer
+    */
+    var $_datasize;
+
+    /**
+    * The maximun length for a BIFF record. See _addContinue()
+    * @var integer
+    * @see _addContinue()
+    */
+    var $_limit;
+
+    /**
+    * Constructor
+    *
+    * @access public
+    */
+    function Spreadsheet_Excel_Writer_BIFFwriter()
+    {
+        $this->_byte_order = '';
+        $this->_data       = '';
+        $this->_datasize   = 0;
+        $this->_limit      = 2080;
+        // Set the byte order
+        $this->_setByteOrder();
+    }
+
+    /**
+    * Determine the byte order and store it as class data to avoid
+    * recalculating it for each call to new().
+    *
+    * @access private
+    */
+    function _setByteOrder()
+    {
+        // Check if "pack" gives the required IEEE 64bit float
+        $teststr = pack("d", 1.2345);
+        $number  = pack("C8", 0x8D, 0x97, 0x6E, 0x12, 0x83, 0xC0, 0xF3, 0x3F);
+        if ($number == $teststr) {
+            $byte_order = 0;    // Little Endian
+        } elseif ($number == strrev($teststr)){
+            $byte_order = 1;    // Big Endian
+        } else {
+            // Give up. I'll fix this in a later version.
+            die("Required floating point format ".
+                                     "not supported on this platform.");
+        }
+        $this->_byte_order = $byte_order;
+    }
+
+    /**
+    * General storage function
+    *
+    * @param string $data binary data to prepend
+    * @access private
+    */
+    function _prepend($data)
+    {
+        if (strlen($data) > $this->_limit) {
+            $data = $this->_addContinue($data);
+        }
+        $this->_data      = $data.$this->_data;
+        $this->_datasize += strlen($data);
+    }
+
+    /**
+    * General storage function
+    *
+    * @param string $data binary data to append
+    * @access private
+    */
+    function _append($data)
+    {
+        if (strlen($data) > $this->_limit) {
+            $data = $this->_addContinue($data);
+        }
+        $this->_data      = $this->_data.$data;
+        $this->_datasize += strlen($data);
+    }
+
+    /**
+    * Writes Excel BOF record to indicate the beginning of a stream or
+    * sub-stream in the BIFF file.
+    *
+    * @param  integer $type Type of BIFF file to write: 0x0005 Workbook,
+    *                       0x0010 Worksheet.
+    * @access private
+    */
+    function _storeBof($type)
+    {
+        $record  = 0x0809;        // Record identifier
+
+        // According to the SDK $build and $year should be set to zero.
+        // However, this throws a warning in Excel 5. So, use magic numbers.
+        if ($this->_BIFF_version == 0x0500) {
+            $length  = 0x0008;
+            $unknown = '';
+            $build   = 0x096C;
+            $year    = 0x07C9;
+        } elseif ($this->_BIFF_version == 0x0600) {
+            $length  = 0x0010;
+            $unknown = pack("VV", 0x00000041, 0x00000006); //unknown last 8 bytes for BIFF8
+            $build   = 0x0DBB;
+            $year    = 0x07CC;
+        }
+        $version = $this->_BIFF_version;
+
+        $header  = pack("vv",   $record, $length);
+        $data    = pack("vvvv", $version, $type, $build, $year);
+        $this->_prepend($header . $data . $unknown);
+    }
+
+    /**
+    * Writes Excel EOF record to indicate the end of a BIFF stream.
+    *
+    * @access private
+    */
+    function _storeEof()
+    {
+        $record    = 0x000A;   // Record identifier
+        $length    = 0x0000;   // Number of bytes to follow
+        $header    = pack("vv", $record, $length);
+        $this->_append($header);
+    }
+
+    /**
+    * Excel limits the size of BIFF records. In Excel 5 the limit is 2084 bytes. In
+    * Excel 97 the limit is 8228 bytes. Records that are longer than these limits
+    * must be split up into CONTINUE blocks.
+    *
+    * This function takes a long BIFF record and inserts CONTINUE records as
+    * necessary.
+    *
+    * @param  string  $data The original binary data to be written
+    * @return string        A very convenient string of continue blocks
+    * @access private
+    */
+    function _addContinue($data)
+    {
+        $limit  = $this->_limit;
+        $record = 0x003C;         // Record identifier
+
+        // The first 2080/8224 bytes remain intact. However, we have to change
+        // the length field of the record.
+        $tmp = substr($data, 0, 2).pack("v", $limit-4).substr($data, 4, $limit - 4);
+
+        $header = pack("vv", $record, $limit);  // Headers for continue records
+
+        // Retrieve chunks of 2080/8224 bytes +4 for the header.
+        $data_length = strlen($data);
+        for ($i = $limit; $i <  ($data_length - $limit); $i += $limit) {
+            $tmp .= $header;
+            $tmp .= substr($data, $i, $limit);
+        }
+
+        // Retrieve the last chunk of data
+        $header  = pack("vv", $record, strlen($data) - $i);
+        $tmp    .= $header;
+        $tmp    .= substr($data, $i, strlen($data) - $i);
+
+        return $tmp;
+    }
+}
+
+/*
+FIXME: change prefixes
+*/
+define("OP_BETWEEN",    0x00);
+define("OP_NOTBETWEEN", 0x01);
+define("OP_EQUAL",      0x02);
+define("OP_NOTEQUAL",   0x03);
+define("OP_GT",         0x04);
+define("OP_LT",         0x05);
+define("OP_GTE",        0x06);
+define("OP_LTE",        0x07);
+
+/**
+* Baseclass for generating Excel DV records (validations)
+*
+* @author   Herman Kuiper
+* @category FileFormats
+* @package  Spreadsheet_Excel_Writer
+*/
+class Spreadsheet_Excel_Writer_Validator
+{
+   var $_type;
+   var $_style;
+   var $_fixedList;
+   var $_blank;
+   var $_incell;
+   var $_showprompt;
+   var $_showerror;
+   var $_title_prompt;
+   var $_descr_prompt;
+   var $_title_error;
+   var $_descr_error;
+   var $_operator;
+   var $_formula1;
+   var $_formula2;
+    /**
+    * The parser from the workbook. Used to parse validation formulas also
+    * @var Spreadsheet_Excel_Writer_Parser
+    */
+    var $_parser;
+
+    function Spreadsheet_Excel_Writer_Validator(&$parser)
+    {
+        $this->_parser       = $parser;
+        $this->_type         = 0x01; // FIXME: add method for setting datatype
+        $this->_style        = 0x00;
+        $this->_fixedList    = false;
+        $this->_blank        = false;
+        $this->_incell       = false;
+        $this->_showprompt   = false;
+        $this->_showerror    = true;
+        $this->_title_prompt = "\x00";
+        $this->_descr_prompt = "\x00";
+        $this->_title_error  = "\x00";
+        $this->_descr_error  = "\x00";
+        $this->_operator     = 0x00; // default is equal
+        $this->_formula1    = '';
+        $this->_formula2    = '';
+    }
+
+   function setPrompt($promptTitle = "\x00", $promptDescription = "\x00", $showPrompt = true)
+   {
+      $this->_showprompt = $showPrompt;
+      $this->_title_prompt = $promptTitle;
+      $this->_descr_prompt = $promptDescription;
+   }
+
+   function setError($errorTitle = "\x00", $errorDescription = "\x00", $showError = true)
+   {
+      $this->_showerror = $showError;
+      $this->_title_error = $errorTitle;
+      $this->_descr_error = $errorDescription;
+   }
+
+   function allowBlank()
+   {
+      $this->_blank = true;
+   }
+
+   function onInvalidStop()
+   {
+      $this->_style = 0x00;
+   }
+
+    function onInvalidWarn()
+    {
+        $this->_style = 0x01;
+    }
+
+    function onInvalidInfo()
+    {
+        $this->_style = 0x02;
+    }
+
+    function setFormula1($formula)
+    {
+        // Parse the formula using the parser in Parser.php
+        $this->_parser->parse($formula);
+
+        $this->_formula1 = $this->_parser->toReversePolish();
+        return true;
+    }
+
+    function setFormula2($formula)
+    {
+        // Parse the formula using the parser in Parser.php
+        $this->_parser->parse($formula);
+
+        $this->_formula2 = $this->_parser->toReversePolish();
+        return true;
+    }
+
+    function _getOptions()
+    {
+        $options = $this->_type;
+        $options |= $this->_style << 3;
+        if ($this->_fixedList) {
+            $options |= 0x80;
+        }
+        if ($this->_blank) {
+            $options |= 0x100;
+        }
+        if (!$this->_incell) {
+            $options |= 0x200;
+        }
+        if ($this->_showprompt) {
+            $options |= 0x40000;
+        }
+        if ($this->_showerror) {
+            $options |= 0x80000;
+        }
+      $options |= $this->_operator << 20;
+
+      return $options;
+   }
+
+   function _getData()
+   {
+      $title_prompt_len = strlen($this->_title_prompt);
+      $descr_prompt_len = strlen($this->_descr_prompt);
+      $title_error_len = strlen($this->_title_error);
+      $descr_error_len = strlen($this->_descr_error);
+
+      $formula1_size = strlen($this->_formula1);
+      $formula2_size = strlen($this->_formula2);
+
+      $data  = pack("V", $this->_getOptions());
+      $data .= pack("vC", $title_prompt_len, 0x00) . $this->_title_prompt;
+      $data .= pack("vC", $title_error_len, 0x00) . $this->_title_error;
+      $data .= pack("vC", $descr_prompt_len, 0x00) . $this->_descr_prompt;
+      $data .= pack("vC", $descr_error_len, 0x00) . $this->_descr_error;
+
+      $data .= pack("vv", $formula1_size, 0x0000) . $this->_formula1;
+      $data .= pack("vv", $formula2_size, 0x0000) . $this->_formula2;
+
+      return $data;
+   }
+}
+
+/**
+* Class for generating Excel XF records (formats)
+*
+* @author   Xavier Noguer <xnoguer@rezebra.com>
+* @category FileFormats
+* @package  Spreadsheet_Excel_Writer
+*/
+
+class Spreadsheet_Excel_Writer_Format
+{
+    /**
+    * The index given by the workbook when creating a new format.
+    * @var integer
+    */
+    var $_xf_index;
+
+    /**
+    * Index to the FONT record.
+    * @var integer
+    */
+    var $font_index;
+
+    /**
+    * The font name (ASCII).
+    * @var string
+    */
+    var $_font_name;
+
+    /**
+    * Height of font (1/20 of a point)
+    * @var integer
+    */
+    var $_size;
+
+    /**
+    * Bold style
+    * @var integer
+    */
+    var $_bold;
+
+    /**
+    * Bit specifiying if the font is italic.
+    * @var integer
+    */
+    var $_italic;
+
+    /**
+    * Index to the cell's color
+    * @var integer
+    */
+    var $_color;
+
+    /**
+    * The text underline property
+    * @var integer
+    */
+    var $_underline;
+
+    /**
+    * Bit specifiying if the font has strikeout.
+    * @var integer
+    */
+    var $_font_strikeout;
+
+    /**
+    * Bit specifiying if the font has outline.
+    * @var integer
+    */
+    var $_font_outline;
+
+    /**
+    * Bit specifiying if the font has shadow.
+    * @var integer
+    */
+    var $_font_shadow;
+
+    /**
+    * 2 bytes specifiying the script type for the font.
+    * @var integer
+    */
+    var $_font_script;
+
+    /**
+    * Byte specifiying the font family.
+    * @var integer
+    */
+    var $_font_family;
+
+    /**
+    * Byte specifiying the font charset.
+    * @var integer
+    */
+    var $_font_charset;
+
+    /**
+    * An index (2 bytes) to a FORMAT record (number format).
+    * @var integer
+    */
+    var $_num_format;
+
+    /**
+    * Bit specifying if formulas are hidden.
+    * @var integer
+    */
+    var $_hidden;
+
+    /**
+    * Bit specifying if the cell is locked.
+    * @var integer
+    */
+    var $_locked;
+
+    /**
+    * The three bits specifying the text horizontal alignment.
+    * @var integer
+    */
+    var $_text_h_align;
+
+    /**
+    * Bit specifying if the text is wrapped at the right border.
+    * @var integer
+    */
+    var $_text_wrap;
+
+    /**
+    * The three bits specifying the text vertical alignment.
+    * @var integer
+    */
+    var $_text_v_align;
+
+    /**
+    * 1 bit, apparently not used.
+    * @var integer
+    */
+    var $_text_justlast;
+
+    /**
+    * The two bits specifying the text rotation.
+    * @var integer
+    */
+    var $_rotation;
+
+    /**
+    * The cell's foreground color.
+    * @var integer
+    */
+    var $_fg_color;
+
+    /**
+    * The cell's background color.
+    * @var integer
+    */
+    var $_bg_color;
+
+    /**
+    * The cell's background fill pattern.
+    * @var integer
+    */
+    var $_pattern;
+
+    /**
+    * Style of the bottom border of the cell
+    * @var integer
+    */
+    var $_bottom;
+
+    /**
+    * Color of the bottom border of the cell.
+    * @var integer
+    */
+    var $_bottom_color;
+
+    /**
+    * Style of the top border of the cell
+    * @var integer
+    */
+    var $_top;
+
+    /**
+    * Color of the top border of the cell.
+    * @var integer
+    */
+    var $_top_color;
+
+    /**
+    * Style of the left border of the cell
+    * @var integer
+    */
+    var $_left;
+
+    /**
+    * Color of the left border of the cell.
+    * @var integer
+    */
+    var $_left_color;
+
+    /**
+    * Style of the right border of the cell
+    * @var integer
+    */
+    var $_right;
+
+    /**
+    * Color of the right border of the cell.
+    * @var integer
+    */
+    var $_right_color;
+
+    /**
+    * Constructor
+    *
+    * @access private
+    * @param integer $index the XF index for the format.
+    * @param array   $properties array with properties to be set on initialization.
+    */
+    function Spreadsheet_Excel_Writer_Format($BIFF_version, $index = 0, $properties =  array())
+    {
+        $this->_xf_index       = $index;
+        $this->_BIFF_version   = $BIFF_version;
+        $this->font_index      = 0;
+        $this->_font_name      = 'Arial';
+        $this->_size           = 10;
+        $this->_bold           = 0x0190;
+        $this->_italic         = 0;
+        $this->_color          = 0x7FFF;
+        $this->_underline      = 0;
+        $this->_font_strikeout = 0;
+        $this->_font_outline   = 0;
+        $this->_font_shadow    = 0;
+        $this->_font_script    = 0;
+        $this->_font_family    = 0;
+        $this->_font_charset   = 0;
+
+        $this->_num_format     = 0;
+
+        $this->_hidden         = 0;
+        $this->_locked         = 0;
+
+        $this->_text_h_align   = 0;
+        $this->_text_wrap      = 0;
+        $this->_text_v_align   = 2;
+        $this->_text_justlast  = 0;
+        $this->_rotation       = 0;
+
+        $this->_fg_color       = 0x40;
+        $this->_bg_color       = 0x41;
+
+        $this->_pattern        = 0;
+
+        $this->_bottom         = 0;
+        $this->_top            = 0;
+        $this->_left           = 0;
+        $this->_right          = 0;
+        $this->_diag           = 0;
+
+        $this->_bottom_color   = 0x40;
+        $this->_top_color      = 0x40;
+        $this->_left_color     = 0x40;
+        $this->_right_color    = 0x40;
+        $this->_diag_color     = 0x40;
+
+        // Set properties passed to Spreadsheet_Excel_Writer_Workbook::addFormat()
+        foreach ($properties as $property => $value)
+        {
+            if (method_exists($this, 'set'.ucwords($property))) {
+                $method_name = 'set'.ucwords($property);
+                $this->$method_name($value);
+            }
+        }
+    }
+
+
+    /**
+    * Generate an Excel BIFF XF record (style or cell).
+    *
+    * @param string $style The type of the XF record ('style' or 'cell').
+    * @return string The XF record
+    */
+    function getXf($style)
+    {
+        // Set the type of the XF record and some of the attributes.
+        if ($style == 'style') {
+            $style = 0xFFF5;
+        } else {
+            $style   = $this->_locked;
+            $style  |= $this->_hidden << 1;
+        }
+
+        // Flags to indicate if attributes have been set.
+        $atr_num     = ($this->_num_format != 0)?1:0;
+        $atr_fnt     = ($this->font_index != 0)?1:0;
+        $atr_alc     = ($this->_text_wrap)?1:0;
+        $atr_bdr     = ($this->_bottom   ||
+                        $this->_top      ||
+                        $this->_left     ||
+                        $this->_right)?1:0;
+        $atr_pat     = (($this->_fg_color != 0x40) ||
+                        ($this->_bg_color != 0x41) ||
+                        $this->_pattern)?1:0;
+        $atr_prot    = $this->_locked | $this->_hidden;
+
+        // Zero the default border colour if the border has not been set.
+        if ($this->_bottom == 0) {
+            $this->_bottom_color = 0;
+        }
+        if ($this->_top  == 0) {
+            $this->_top_color = 0;
+        }
+        if ($this->_right == 0) {
+            $this->_right_color = 0;
+        }
+        if ($this->_left == 0) {
+            $this->_left_color = 0;
+        }
+        if ($this->_diag == 0) {
+            $this->_diag_color = 0;
+        }
+
+        $record         = 0x00E0;              // Record identifier
+        if ($this->_BIFF_version == 0x0500) {
+            $length         = 0x0010;              // Number of bytes to follow
+        }
+        if ($this->_BIFF_version == 0x0600) {
+            $length         = 0x0014;
+        }
+
+        $ifnt           = $this->font_index;   // Index to FONT record
+        $ifmt           = $this->_num_format;  // Index to FORMAT record
+        if ($this->_BIFF_version == 0x0500) {
+            $align          = $this->_text_h_align;       // Alignment
+            $align         |= $this->_text_wrap     << 3;
+            $align         |= $this->_text_v_align  << 4;
+            $align         |= $this->_text_justlast << 7;
+            $align         |= $this->_rotation      << 8;
+            $align         |= $atr_num                << 10;
+            $align         |= $atr_fnt                << 11;
+            $align         |= $atr_alc                << 12;
+            $align         |= $atr_bdr                << 13;
+            $align         |= $atr_pat                << 14;
+            $align         |= $atr_prot               << 15;
+
+            $icv            = $this->_fg_color;       // fg and bg pattern colors
+            $icv           |= $this->_bg_color      << 7;
+
+            $fill           = $this->_pattern;        // Fill and border line style
+            $fill          |= $this->_bottom        << 6;
+            $fill          |= $this->_bottom_color  << 9;
+
+            $border1        = $this->_top;            // Border line style and color
+            $border1       |= $this->_left          << 3;
+            $border1       |= $this->_right         << 6;
+            $border1       |= $this->_top_color     << 9;
+
+            $border2        = $this->_left_color;     // Border color
+            $border2       |= $this->_right_color   << 7;
+
+            $header      = pack("vv",       $record, $length);
+            $data        = pack("vvvvvvvv", $ifnt, $ifmt, $style, $align,
+                                            $icv, $fill,
+                                            $border1, $border2);
+        } elseif ($this->_BIFF_version == 0x0600) {
+            $align          = $this->_text_h_align;       // Alignment
+            $align         |= $this->_text_wrap     << 3;
+            $align         |= $this->_text_v_align  << 4;
+            $align         |= $this->_text_justlast << 7;
+
+            $used_attrib    = $atr_num              << 2;
+            $used_attrib   |= $atr_fnt              << 3;
+            $used_attrib   |= $atr_alc              << 4;
+            $used_attrib   |= $atr_bdr              << 5;
+            $used_attrib   |= $atr_pat              << 6;
+            $used_attrib   |= $atr_prot             << 7;
+
+            $icv            = $this->_fg_color;      // fg and bg pattern colors
+            $icv           |= $this->_bg_color      << 7;
+
+            $border1        = $this->_left;          // Border line style and color
+            $border1       |= $this->_right         << 4;
+            $border1       |= $this->_top           << 8;
+            $border1       |= $this->_bottom        << 12;
+            $border1       |= $this->_left_color    << 16;
+            $border1       |= $this->_right_color   << 23;
+            $diag_tl_to_rb = 0; // FIXME: add method
+            $diag_tr_to_lb = 0; // FIXME: add method
+            $border1       |= $diag_tl_to_rb        << 30;
+            $border1       |= $diag_tr_to_lb        << 31;
+
+            $border2        = $this->_top_color;    // Border color
+            $border2       |= $this->_bottom_color   << 7;
+            $border2       |= $this->_diag_color     << 14;
+            $border2       |= $this->_diag           << 21;
+            $border2       |= $this->_pattern        << 26;
+
+            $header      = pack("vv",       $record, $length);
+
+            $rotation      = 0x00;
+            $biff8_options = 0x00;
+            $data  = pack("vvvC", $ifnt, $ifmt, $style, $align);
+            $data .= pack("CCC", $rotation, $biff8_options, $used_attrib);
+            $data .= pack("VVv", $border1, $border2, $icv);
+        }
+
+        return($header . $data);
+    }
+
+    /**
+    * Generate an Excel BIFF FONT record.
+    *
+    * @return string The FONT record
+    */
+    function getFont()
+    {
+        $dyHeight   = $this->_size * 20;    // Height of font (1/20 of a point)
+        $icv        = $this->_color;        // Index to color palette
+        $bls        = $this->_bold;         // Bold style
+        $sss        = $this->_font_script;  // Superscript/subscript
+        $uls        = $this->_underline;    // Underline
+        $bFamily    = $this->_font_family;  // Font family
+        $bCharSet   = $this->_font_charset; // Character set
+        $encoding   = 0;                    // TODO: Unicode support
+
+        $cch        = strlen($this->_font_name); // Length of font name
+        $record     = 0x31;                      // Record identifier
+        if ($this->_BIFF_version == 0x0500) {
+            $length     = 0x0F + $cch;            // Record length
+        } elseif ($this->_BIFF_version == 0x0600) {
+            $length     = 0x10 + $cch;
+        }
+        $reserved   = 0x00;                // Reserved
+        $grbit      = 0x00;                // Font attributes
+        if ($this->_italic) {
+            $grbit     |= 0x02;
+        }
+        if ($this->_font_strikeout) {
+            $grbit     |= 0x08;
+        }
+        if ($this->_font_outline) {
+            $grbit     |= 0x10;
+        }
+        if ($this->_font_shadow) {
+            $grbit     |= 0x20;
+        }
+
+        $header  = pack("vv",         $record, $length);
+        if ($this->_BIFF_version == 0x0500) {
+            $data    = pack("vvvvvCCCCC", $dyHeight, $grbit, $icv, $bls,
+                                          $sss, $uls, $bFamily,
+                                          $bCharSet, $reserved, $cch);
+        } elseif ($this->_BIFF_version == 0x0600) {
+            $data    = pack("vvvvvCCCCCC", $dyHeight, $grbit, $icv, $bls,
+                                           $sss, $uls, $bFamily,
+                                           $bCharSet, $reserved, $cch, $encoding);
+        }
+        return($header . $data . $this->_font_name);
+    }
+
+    /**
+    * Returns a unique hash key for a font.
+    * Used by Spreadsheet_Excel_Writer_Workbook::_storeAllFonts()
+    *
+    * The elements that form the key are arranged to increase the probability of
+    * generating a unique key. Elements that hold a large range of numbers
+    * (eg. _color) are placed between two binary elements such as _italic
+    *
+    * @return string A key for this font
+    */
+    function getFontKey()
+    {
+        $key  = "$this->_font_name$this->_size";
+        $key .= "$this->_font_script$this->_underline";
+        $key .= "$this->_font_strikeout$this->_bold$this->_font_outline";
+        $key .= "$this->_font_family$this->_font_charset";
+        $key .= "$this->_font_shadow$this->_color$this->_italic";
+        $key  = str_replace(' ', '_', $key);
+        return ($key);
+    }
+
+    /**
+    * Returns the index used by Spreadsheet_Excel_Writer_Worksheet::_XF()
+    *
+    * @return integer The index for the XF record
+    */
+    function getXfIndex()
+    {
+        return($this->_xf_index);
+    }
+
+    /**
+    * Used in conjunction with the set_xxx_color methods to convert a color
+    * string into a number. Color range is 0..63 but we will restrict it
+    * to 8..63 to comply with Gnumeric. Colors 0..7 are repeated in 8..15.
+    *
+    * @access private
+    * @param string $name_color name of the color (i.e.: 'blue', 'red', etc..). Optional.
+    * @return integer The color index
+    */
+    function _getColor($name_color = '')
+    {
+        $colors = array(
+                        'aqua'    => 0x0F,
+                        'cyan'    => 0x0F,
+                        'black'   => 0x08,
+                        'blue'    => 0x0C,
+                        'brown'   => 0x10,
+                        'magenta' => 0x0E,
+                        'fuchsia' => 0x0E,
+                        'gray'    => 0x17,
+                        'grey'    => 0x17,
+                        'green'   => 0x11,
+                        'lime'    => 0x0B,
+                        'navy'    => 0x12,
+                        'orange'  => 0x35,
+                        'purple'  => 0x14,
+                        'red'     => 0x0A,
+                        'silver'  => 0x16,
+                        'white'   => 0x09,
+                        'yellow'  => 0x0D
+                       );
+
+        // Return the default color, 0x7FFF, if undef,
+        if ($name_color == '') {
+            return(0x7FFF);
+        }
+
+        // or the color string converted to an integer,
+        if (isset($colors[$name_color])) {
+            return($colors[$name_color]);
+        }
+
+        // or the default color if string is unrecognised,
+        if (preg_match("/\D/",$name_color)) {
+            return(0x7FFF);
+        }
+
+        // or an index < 8 mapped into the correct range,
+        if ($name_color < 8) {
+            return($name_color + 8);
+        }
+
+        // or the default color if arg is outside range,
+        if ($name_color > 63) {
+            return(0x7FFF);
+        }
+
+        // or an integer in the valid range
+        return($name_color);
+    }
+
+    /**
+    * Set cell alignment.
+    *
+    * @access public
+    * @param string $location alignment for the cell ('left', 'right', etc...).
+    */
+    function setAlign($location)
+    {
+        if (preg_match("/\d/",$location)) {
+            return;                      // Ignore numbers
+        }
+
+        $location = strtolower($location);
+
+        if ($location == 'left') {
+            $this->_text_h_align = 1;
+        }
+        if ($location == 'centre') {
+            $this->_text_h_align = 2;
+        }
+        if ($location == 'center') {
+            $this->_text_h_align = 2;
+        }
+        if ($location == 'right') {
+            $this->_text_h_align = 3;
+        }
+        if ($location == 'fill') {
+            $this->_text_h_align = 4;
+        }
+        if ($location == 'justify') {
+            $this->_text_h_align = 5;
+        }
+        if ($location == 'merge') {
+            $this->_text_h_align = 6;
+        }
+        if ($location == 'equal_space') { // For T.K.
+            $this->_text_h_align = 7;
+        }
+        if ($location == 'top') {
+            $this->_text_v_align = 0;
+        }
+        if ($location == 'vcentre') {
+            $this->_text_v_align = 1;
+        }
+        if ($location == 'vcenter') {
+            $this->_text_v_align = 1;
+        }
+        if ($location == 'bottom') {
+            $this->_text_v_align = 2;
+        }
+        if ($location == 'vjustify') {
+            $this->_text_v_align = 3;
+        }
+        if ($location == 'vequal_space') { // For T.K.
+            $this->_text_v_align = 4;
+        }
+    }
+
+    /**
+    * Set cell horizontal alignment.
+    *
+    * @access public
+    * @param string $location alignment for the cell ('left', 'right', etc...).
+    */
+    function setHAlign($location)
+    {
+        if (preg_match("/\d/",$location)) {
+            return;                      // Ignore numbers
+        }
+    
+        $location = strtolower($location);
+    
+        if ($location == 'left') {
+            $this->_text_h_align = 1;
+        }
+        if ($location == 'centre') {
+            $this->_text_h_align = 2;
+        }
+        if ($location == 'center') {
+            $this->_text_h_align = 2;
+        }
+        if ($location == 'right') {
+            $this->_text_h_align = 3;
+        }
+        if ($location == 'fill') {
+            $this->_text_h_align = 4;
+        }
+        if ($location == 'justify') {
+            $this->_text_h_align = 5;
+        }
+        if ($location == 'merge') {
+            $this->_text_h_align = 6;
+        }
+        if ($location == 'equal_space') { // For T.K.
+            $this->_text_h_align = 7;
+        }
+    }
+
+    /**
+    * Set cell vertical alignment.
+    *
+    * @access public
+    * @param string $location alignment for the cell ('top', 'vleft', 'vright', etc...).
+    */
+    function setVAlign($location)
+    {
+        if (preg_match("/\d/",$location)) {
+            return;                      // Ignore numbers
+        }
+    
+        $location = strtolower($location);
+        if ($location == 'top') {
+            $this->_text_v_align = 0;
+        }
+        if ($location == 'vcentre') {
+            $this->_text_v_align = 1;
+        }
+        if ($location == 'vcenter') {
+            $this->_text_v_align = 1;
+        }
+        if ($location == 'bottom') {
+            $this->_text_v_align = 2;
+        }
+        if ($location == 'vjustify') {
+            $this->_text_v_align = 3;
+        }
+        if ($location == 'vequal_space') { // For T.K.
+            $this->_text_v_align = 4;
+        }
+    }
+
+    /**
+    * This is an alias for the unintuitive setAlign('merge')
+    *
+    * @access public
+    */
+    function setMerge()
+    {
+        $this->setAlign('merge');
+    }
+
+    /**
+    * Sets the boldness of the text.
+    * Bold has a range 100..1000.
+    * 0 (400) is normal. 1 (700) is bold.
+    *
+    * @access public
+    * @param integer $weight Weight for the text, 0 maps to 400 (normal text),
+                             1 maps to 700 (bold text). Valid range is: 100-1000.
+                             It's Optional, default is 1 (bold).
+    */
+    function setBold($weight = 1)
+    {
+        if ($weight == 1) {
+            $weight = 0x2BC;  // Bold text
+        }
+        if ($weight == 0) {
+            $weight = 0x190;  // Normal text
+        }
+        if ($weight <  0x064) {
+            $weight = 0x190;  // Lower bound
+        }
+        if ($weight >  0x3E8) {
+            $weight = 0x190;  // Upper bound
+        }
+        $this->_bold = $weight;
+    }
+
+
+    /************************************
+    * FUNCTIONS FOR SETTING CELLS BORDERS
+    */
+
+    /**
+    * Sets the width for the bottom border of the cell
+    *
+    * @access public
+    * @param integer $style style of the cell border. 1 => thin, 2 => thick.
+    */
+    function setBottom($style)
+    {
+        $this->_bottom = $style;
+    }
+
+    /**
+    * Sets the width for the top border of the cell
+    *
+    * @access public
+    * @param integer $style style of the cell top border. 1 => thin, 2 => thick.
+    */
+    function setTop($style)
+    {
+        $this->_top = $style;
+    }
+
+    /**
+    * Sets the width for the left border of the cell
+    *
+    * @access public
+    * @param integer $style style of the cell left border. 1 => thin, 2 => thick.
+    */
+    function setLeft($style)
+    {
+        $this->_left = $style;
+    }
+
+    /**
+    * Sets the width for the right border of the cell
+    *
+    * @access public
+    * @param integer $style style of the cell right border. 1 => thin, 2 => thick.
+    */
+    function setRight($style)
+    {
+        $this->_right = $style;
+    }
+
+
+    /**
+    * Set cells borders to the same style
+    *
+    * @access public
+    * @param integer $style style to apply for all cell borders. 1 => thin, 2 => thick.
+    */
+    function setBorder($style)
+    {
+        $this->setBottom($style);
+        $this->setTop($style);
+        $this->setLeft($style);
+        $this->setRight($style);
+    }
+
+
+    /*******************************************
+    * FUNCTIONS FOR SETTING CELLS BORDERS COLORS
+    */
+
+    /**
+    * Sets all the cell's borders to the same color
+    *
+    * @access public
+    * @param mixed $color The color we are setting. Either a string (like 'blue'),
+    *                     or an integer (range is [8...63]).
+    */
+    function setBorderColor($color)
+    {
+        $this->setBottomColor($color);
+        $this->setTopColor($color);
+        $this->setLeftColor($color);
+        $this->setRightColor($color);
+    }
+
+    /**
+    * Sets the cell's bottom border color
+    *
+    * @access public
+    * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
+    */
+    function setBottomColor($color)
+    {
+        $value = $this->_getColor($color);
+        $this->_bottom_color = $value;
+    }
+
+    /**
+    * Sets the cell's top border color
+    *
+    * @access public
+    * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
+    */
+    function setTopColor($color)
+    {
+        $value = $this->_getColor($color);
+        $this->_top_color = $value;
+    }
+
+    /**
+    * Sets the cell's left border color
+    *
+    * @access public
+    * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
+    */
+    function setLeftColor($color)
+    {
+        $value = $this->_getColor($color);
+        $this->_left_color = $value;
+    }
+
+    /**
+    * Sets the cell's right border color
+    *
+    * @access public
+    * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
+    */
+    function setRightColor($color)
+    {
+        $value = $this->_getColor($color);
+        $this->_right_color = $value;
+    }
+
+
+    /**
+    * Sets the cell's foreground color
+    *
+    * @access public
+    * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
+    */
+    function setFgColor($color)
+    {
+        $value = $this->_getColor($color);
+        $this->_fg_color = $value;
+        if ($this->_pattern == 0) { // force color to be seen
+            $this->_pattern = 1;
+        }
+    }
+
+    /**
+    * Sets the cell's background color
+    *
+    * @access public
+    * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
+    */
+    function setBgColor($color)
+    {
+        $value = $this->_getColor($color);
+        $this->_bg_color = $value;
+        if ($this->_pattern == 0) { // force color to be seen
+            $this->_pattern = 1;
+        }
+    }
+
+    /**
+    * Sets the cell's color
+    *
+    * @access public
+    * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
+    */
+    function setColor($color)
+    {
+        $value = $this->_getColor($color);
+        $this->_color = $value;
+    }
+
+    /**
+    * Sets the fill pattern attribute of a cell
+    *
+    * @access public
+    * @param integer $arg Optional. Defaults to 1. Meaningful values are: 0-18,
+    *                     0 meaning no background.
+    */
+    function setPattern($arg = 1)
+    {
+        $this->_pattern = $arg;
+    }
+
+    /**
+    * Sets the underline of the text
+    *
+    * @access public
+    * @param integer $underline The value for underline. Possible values are:
+    *                          1 => underline, 2 => double underline.
+    */
+    function setUnderline($underline)
+    {
+        $this->_underline = $underline;
+    }
+
+    /**
+    * Sets the font style as italic
+    *
+    * @access public
+    */
+    function setItalic()
+    {
+        $this->_italic = 1;
+    }
+
+    /**
+    * Sets the font size
+    *
+    * @access public
+    * @param integer $size The font size (in pixels I think).
+    */
+    function setSize($size)
+    {
+        $this->_size = $size;
+    }
+
+    /**
+    * Sets text wrapping
+    *
+    * @access public
+    */
+    function setTextWrap()
+    {
+        $this->_text_wrap = 1;
+    }
+
+    /**
+    * Sets the orientation of the text
+    *
+    * @access public
+    * @param integer $angle The rotation angle for the text (clockwise). Possible
+                            values are: 0, 90, 270 and -1 for stacking top-to-bottom.
+    */
+    function setTextRotation($angle)
+    {
+        switch ($angle)
+        {
+            case 0:
+                $this->_rotation = 0;
+                break;
+            case 90:
+                $this->_rotation = 3;
+                break;
+            case 270:
+                $this->_rotation = 2;
+                break;
+            case -1:
+                $this->_rotation = 1;
+                break;
+            default :
+                $this->_rotation = 0;
+                break;
+        }
+    }
+
+    /**
+    * Sets the numeric format.
+    * It can be date, time, currency, etc...
+    *
+    * @access public
+    * @param integer $num_format The numeric format.
+    */
+    function setNumFormat($num_format)
+    {
+        $this->_num_format = $num_format;
+    }
+
+    /**
+    * Sets font as strikeout.
+    *
+    * @access public
+    */
+    function setStrikeOut()
+    {
+        $this->_font_strikeout = 1;
+    }
+
+    /**
+    * Sets outlining for a font.
+    *
+    * @access public
+    */
+    function setOutLine()
+    {
+        $this->_font_outline = 1;
+    }
+
+    /**
+    * Sets font as shadow.
+    *
+    * @access public
+    */
+    function setShadow()
+    {
+        $this->_font_shadow = 1;
+    }
+
+    /**
+    * Sets the script type of the text
+    *
+    * @access public
+    * @param integer $script The value for script type. Possible values are:
+    *                        1 => superscript, 2 => subscript.
+    */
+    function setScript($script)
+    {
+        $this->_font_script = $script;
+    }
+
+     /**
+     * Locks a cell.
+     *
+     * @access public
+     */
+     function setLocked()
+     {
+         $this->_locked = 1;
+     }
+
+    /**
+    * Unlocks a cell. Useful for unprotecting particular cells of a protected sheet.
+    *
+    * @access public
+    */
+    function setUnLocked()
+    {
+        $this->_locked = 0;
+    }
+
+    /**
+    * Sets the font family name.
+    *
+    * @access public
+    * @param string $fontfamily The font family name. Possible values are:
+    *                           'Times New Roman', 'Arial', 'Courier'.
+    */
+    function setFontFamily($font_family)
+    {
+        $this->_font_name = $font_family;
+    }
+}
+
+/**
+* Class for parsing Excel formulas
+*
+* @author   Xavier Noguer <xnoguer@rezebra.com>
+* @category FileFormats
+* @package  Spreadsheet_Excel_Writer
+*/
+
+class Spreadsheet_Excel_Writer_Parser
+{
+    /**
+    * The index of the character we are currently looking at
+    * @var integer
+    */
+    var $_current_char;
+
+    /**
+    * The token we are working on.
+    * @var string
+    */
+    var $_current_token;
+
+    /**
+    * The formula to parse
+    * @var string
+    */
+    var $_formula;
+
+    /**
+    * The character ahead of the current char
+    * @var string
+    */
+    var $_lookahead;
+
+    /**
+    * The parse tree to be generated
+    * @var string
+    */
+    var $_parse_tree;
+
+    /**
+    * The byte order. 1 => big endian, 0 => little endian.
+    * @var integer
+    */
+    var $_byte_order;
+
+    /**
+    * Array of external sheets
+    * @var array
+    */
+    var $_ext_sheets;
+
+    /**
+    * Array of sheet references in the form of REF structures
+    * @var array
+    */
+    var $_references;
+
+    /**
+    * The BIFF version for the workbook
+    * @var integer
+    */
+    var $_BIFF_version;
+
+    /**
+    * The class constructor
+    *
+    * @param integer $byte_order The byte order (Little endian or Big endian) of the architecture
+                                 (optional). 1 => big endian, 0 (default) little endian.
+    */
+    function Spreadsheet_Excel_Writer_Parser($byte_order, $biff_version)
+    {
+        $this->_current_char  = 0;
+        $this->_BIFF_version  = $biff_version;
+        $this->_current_token = '';       // The token we are working on.
+        $this->_formula       = '';       // The formula to parse.
+        $this->_lookahead     = '';       // The character ahead of the current char.
+        $this->_parse_tree    = '';       // The parse tree to be generated.
+        $this->_initializeHashes();      // Initialize the hashes: ptg's and function's ptg's
+        $this->_byte_order = $byte_order; // Little Endian or Big Endian
+        $this->_ext_sheets = array();
+        $this->_references = array();
+    }
+
+    /**
+    * Initialize the ptg and function hashes.
+    *
+    * @access private
+    */
+    function _initializeHashes()
+    {
+        // The Excel ptg indices
+        $this->ptg = array(
+            'ptgExp'       => 0x01,
+            'ptgTbl'       => 0x02,
+            'ptgAdd'       => 0x03,
+            'ptgSub'       => 0x04,
+            'ptgMul'       => 0x05,
+            'ptgDiv'       => 0x06,
+            'ptgPower'     => 0x07,
+            'ptgConcat'    => 0x08,
+            'ptgLT'        => 0x09,
+            'ptgLE'        => 0x0A,
+            'ptgEQ'        => 0x0B,
+            'ptgGE'        => 0x0C,
+            'ptgGT'        => 0x0D,
+            'ptgNE'        => 0x0E,
+            'ptgIsect'     => 0x0F,
+            'ptgUnion'     => 0x10,
+            'ptgRange'     => 0x11,
+            'ptgUplus'     => 0x12,
+            'ptgUminus'    => 0x13,
+            'ptgPercent'   => 0x14,
+            'ptgParen'     => 0x15,
+            'ptgMissArg'   => 0x16,
+            'ptgStr'       => 0x17,
+            'ptgAttr'      => 0x19,
+            'ptgSheet'     => 0x1A,
+            'ptgEndSheet'  => 0x1B,
+            'ptgErr'       => 0x1C,
+            'ptgBool'      => 0x1D,
+            'ptgInt'       => 0x1E,
+            'ptgNum'       => 0x1F,
+            'ptgArray'     => 0x20,
+            'ptgFunc'      => 0x21,
+            'ptgFuncVar'   => 0x22,
+            'ptgName'      => 0x23,
+            'ptgRef'       => 0x24,
+            'ptgArea'      => 0x25,
+            'ptgMemArea'   => 0x26,
+            'ptgMemErr'    => 0x27,
+            'ptgMemNoMem'  => 0x28,
+            'ptgMemFunc'   => 0x29,
+            'ptgRefErr'    => 0x2A,
+            'ptgAreaErr'   => 0x2B,
+            'ptgRefN'      => 0x2C,
+            'ptgAreaN'     => 0x2D,
+            'ptgMemAreaN'  => 0x2E,
+            'ptgMemNoMemN' => 0x2F,
+            'ptgNameX'     => 0x39,
+            'ptgRef3d'     => 0x3A,
+            'ptgArea3d'    => 0x3B,
+            'ptgRefErr3d'  => 0x3C,
+            'ptgAreaErr3d' => 0x3D,
+            'ptgArrayV'    => 0x40,
+            'ptgFuncV'     => 0x41,
+            'ptgFuncVarV'  => 0x42,
+            'ptgNameV'     => 0x43,
+            'ptgRefV'      => 0x44,
+            'ptgAreaV'     => 0x45,
+            'ptgMemAreaV'  => 0x46,
+            'ptgMemErrV'   => 0x47,
+            'ptgMemNoMemV' => 0x48,
+            'ptgMemFuncV'  => 0x49,
+            'ptgRefErrV'   => 0x4A,
+            'ptgAreaErrV'  => 0x4B,
+            'ptgRefNV'     => 0x4C,
+            'ptgAreaNV'    => 0x4D,
+            'ptgMemAreaNV' => 0x4E,
+            'ptgMemNoMemN' => 0x4F,
+            'ptgFuncCEV'   => 0x58,
+            'ptgNameXV'    => 0x59,
+            'ptgRef3dV'    => 0x5A,
+            'ptgArea3dV'   => 0x5B,
+            'ptgRefErr3dV' => 0x5C,
+            'ptgAreaErr3d' => 0x5D,
+            'ptgArrayA'    => 0x60,
+            'ptgFuncA'     => 0x61,
+            'ptgFuncVarA'  => 0x62,
+            'ptgNameA'     => 0x63,
+            'ptgRefA'      => 0x64,
+            'ptgAreaA'     => 0x65,
+            'ptgMemAreaA'  => 0x66,
+            'ptgMemErrA'   => 0x67,
+            'ptgMemNoMemA' => 0x68,
+            'ptgMemFuncA'  => 0x69,
+            'ptgRefErrA'   => 0x6A,
+            'ptgAreaErrA'  => 0x6B,
+            'ptgRefNA'     => 0x6C,
+            'ptgAreaNA'    => 0x6D,
+            'ptgMemAreaNA' => 0x6E,
+            'ptgMemNoMemN' => 0x6F,
+            'ptgFuncCEA'   => 0x78,
+            'ptgNameXA'    => 0x79,
+            'ptgRef3dA'    => 0x7A,
+            'ptgArea3dA'   => 0x7B,
+            'ptgRefErr3dA' => 0x7C,
+            'ptgAreaErr3d' => 0x7D
+            );
+
+        // Thanks to Michael Meeks and Gnumeric for the initial arg values.
+        //
+        // The following hash was generated by "function_locale.pl" in the distro.
+        // Refer to function_locale.pl for non-English function names.
+        //
+        // The array elements are as follow:
+        // ptg:   The Excel function ptg code.
+        // args:  The number of arguments that the function takes:
+        //           >=0 is a fixed number of arguments.
+        //           -1  is a variable  number of arguments.
+        // class: The reference, value or array class of the function args.
+        // vol:   The function is volatile.
+        //
+        $this->_functions = array(
+              // function                  ptg  args  class  vol
+              'COUNT'           => array(   0,   -1,    0,    0 ),
+              'IF'              => array(   1,   -1,    1,    0 ),
+              'ISNA'            => array(   2,    1,    1,    0 ),
+              'ISERROR'         => array(   3,    1,    1,    0 ),
+              'SUM'             => array(   4,   -1,    0,    0 ),
+              'AVERAGE'         => array(   5,   -1,    0,    0 ),
+              'MIN'             => array(   6,   -1,    0,    0 ),
+              'MAX'             => array(   7,   -1,    0,    0 ),
+              'ROW'             => array(   8,   -1,    0,    0 ),
+              'COLUMN'          => array(   9,   -1,    0,    0 ),
+              'NA'              => array(  10,    0,    0,    0 ),
+              'NPV'             => array(  11,   -1,    1,    0 ),
+              'STDEV'           => array(  12,   -1,    0,    0 ),
+              'DOLLAR'          => array(  13,   -1,    1,    0 ),
+              'FIXED'           => array(  14,   -1,    1,    0 ),
+              'SIN'             => array(  15,    1,    1,    0 ),
+              'COS'             => array(  16,    1,    1,    0 ),
+              'TAN'             => array(  17,    1,    1,    0 ),
+              'ATAN'            => array(  18,    1,    1,    0 ),
+              'PI'              => array(  19,    0,    1,    0 ),
+              'SQRT'            => array(  20,    1,    1,    0 ),
+              'EXP'             => array(  21,    1,    1,    0 ),
+              'LN'              => array(  22,    1,    1,    0 ),
+              'LOG10'           => array(  23,    1,    1,    0 ),
+              'ABS'             => array(  24,    1,    1,    0 ),
+              'INT'             => array(  25,    1,    1,    0 ),
+              'SIGN'            => array(  26,    1,    1,    0 ),
+              'ROUND'           => array(  27,    2,    1,    0 ),
+              'LOOKUP'          => array(  28,   -1,    0,    0 ),
+              'INDEX'           => array(  29,   -1,    0,    1 ),
+              'REPT'            => array(  30,    2,    1,    0 ),
+              'MID'             => array(  31,    3,    1,    0 ),
+              'LEN'             => array(  32,    1,    1,    0 ),
+              'VALUE'           => array(  33,    1,    1,    0 ),
+              'TRUE'            => array(  34,    0,    1,    0 ),
+              'FALSE'           => array(  35,    0,    1,    0 ),
+              'AND'             => array(  36,   -1,    0,    0 ),
+              'OR'              => array(  37,   -1,    0,    0 ),
+              'NOT'             => array(  38,    1,    1,    0 ),
+              'MOD'             => array(  39,    2,    1,    0 ),
+              'DCOUNT'          => array(  40,    3,    0,    0 ),
+              'DSUM'            => array(  41,    3,    0,    0 ),
+              'DAVERAGE'        => array(  42,    3,    0,    0 ),
+              'DMIN'            => array(  43,    3,    0,    0 ),
+              'DMAX'            => array(  44,    3,    0,    0 ),
+              'DSTDEV'          => array(  45,    3,    0,    0 ),
+              'VAR'             => array(  46,   -1,    0,    0 ),
+              'DVAR'            => array(  47,    3,    0,    0 ),
+              'TEXT'            => array(  48,    2,    1,    0 ),
+              'LINEST'          => array(  49,   -1,    0,    0 ),
+              'TREND'           => array(  50,   -1,    0,    0 ),
+              'LOGEST'          => array(  51,   -1,    0,    0 ),
+              'GROWTH'          => array(  52,   -1,    0,    0 ),
+              'PV'              => array(  56,   -1,    1,    0 ),
+              'FV'              => array(  57,   -1,    1,    0 ),
+              'NPER'            => array(  58,   -1,    1,    0 ),
+              'PMT'             => array(  59,   -1,    1,    0 ),
+              'RATE'            => array(  60,   -1,    1,    0 ),
+              'MIRR'            => array(  61,    3,    0,    0 ),
+              'IRR'             => array(  62,   -1,    0,    0 ),
+              'RAND'            => array(  63,    0,    1,    1 ),
+              'MATCH'           => array(  64,   -1,    0,    0 ),
+              'DATE'            => array(  65,    3,    1,    0 ),
+              'TIME'            => array(  66,    3,    1,    0 ),
+              'DAY'             => array(  67,    1,    1,    0 ),
+              'MONTH'           => array(  68,    1,    1,    0 ),
+              'YEAR'            => array(  69,    1,    1,    0 ),
+              'WEEKDAY'         => array(  70,   -1,    1,    0 ),
+              'HOUR'            => array(  71,    1,    1,    0 ),
+              'MINUTE'          => array(  72,    1,    1,    0 ),
+              'SECOND'          => array(  73,    1,    1,    0 ),
+              'NOW'             => array(  74,    0,    1,    1 ),
+              'AREAS'           => array(  75,    1,    0,    1 ),
+              'ROWS'            => array(  76,    1,    0,    1 ),
+              'COLUMNS'         => array(  77,    1,    0,    1 ),
+              'OFFSET'          => array(  78,   -1,    0,    1 ),
+              'SEARCH'          => array(  82,   -1,    1,    0 ),
+              'TRANSPOSE'       => array(  83,    1,    1,    0 ),
+              'TYPE'            => array(  86,    1,    1,    0 ),
+              'ATAN2'           => array(  97,    2,    1,    0 ),
+              'ASIN'            => array(  98,    1,    1,    0 ),
+              'ACOS'            => array(  99,    1,    1,    0 ),
+              'CHOOSE'          => array( 100,   -1,    1,    0 ),
+              'HLOOKUP'         => array( 101,   -1,    0,    0 ),
+              'VLOOKUP'         => array( 102,   -1,    0,    0 ),
+              'ISREF'           => array( 105,    1,    0,    0 ),
+              'LOG'             => array( 109,   -1,    1,    0 ),
+              'CHAR'            => array( 111,    1,    1,    0 ),
+              'LOWER'           => array( 112,    1,    1,    0 ),
+              'UPPER'           => array( 113,    1,    1,    0 ),
+              'PROPER'          => array( 114,    1,    1,    0 ),
+              'LEFT'            => array( 115,   -1,    1,    0 ),
+              'RIGHT'           => array( 116,   -1,    1,    0 ),
+              'EXACT'           => array( 117,    2,    1,    0 ),
+              'TRIM'            => array( 118,    1,    1,    0 ),
+              'REPLACE'         => array( 119,    4,    1,    0 ),
+              'SUBSTITUTE'      => array( 120,   -1,    1,    0 ),
+              'CODE'            => array( 121,    1,    1,    0 ),
+              'FIND'            => array( 124,   -1,    1,    0 ),
+              'CELL'            => array( 125,   -1,    0,    1 ),
+              'ISERR'           => array( 126,    1,    1,    0 ),
+              'ISTEXT'          => array( 127,    1,    1,    0 ),
+              'ISNUMBER'        => array( 128,    1,    1,    0 ),
+              'ISBLANK'         => array( 129,    1,    1,    0 ),
+              'T'               => array( 130,    1,    0,    0 ),
+              'N'               => array( 131,    1,    0,    0 ),
+              'DATEVALUE'       => array( 140,    1,    1,    0 ),
+              'TIMEVALUE'       => array( 141,    1,    1,    0 ),
+              'SLN'             => array( 142,    3,    1,    0 ),
+              'SYD'             => array( 143,    4,    1,    0 ),
+              'DDB'             => array( 144,   -1,    1,    0 ),
+              'INDIRECT'        => array( 148,   -1,    1,    1 ),
+              'CALL'            => array( 150,   -1,    1,    0 ),
+              'CLEAN'           => array( 162,    1,    1,    0 ),
+              'MDETERM'         => array( 163,    1,    2,    0 ),
+              'MINVERSE'        => array( 164,    1,    2,    0 ),
+              'MMULT'           => array( 165,    2,    2,    0 ),
+              'IPMT'            => array( 167,   -1,    1,    0 ),
+              'PPMT'            => array( 168,   -1,    1,    0 ),
+              'COUNTA'          => array( 169,   -1,    0,    0 ),
+              'PRODUCT'         => array( 183,   -1,    0,    0 ),
+              'FACT'            => array( 184,    1,    1,    0 ),
+              'DPRODUCT'        => array( 189,    3,    0,    0 ),
+              'ISNONTEXT'       => array( 190,    1,    1,    0 ),
+              'STDEVP'          => array( 193,   -1,    0,    0 ),
+              'VARP'            => array( 194,   -1,    0,    0 ),
+              'DSTDEVP'         => array( 195,    3,    0,    0 ),
+              'DVARP'           => array( 196,    3,    0,    0 ),
+              'TRUNC'           => array( 197,   -1,    1,    0 ),
+              'ISLOGICAL'       => array( 198,    1,    1,    0 ),
+              'DCOUNTA'         => array( 199,    3,    0,    0 ),
+              'ROUNDUP'         => array( 212,    2,    1,    0 ),
+              'ROUNDDOWN'       => array( 213,    2,    1,    0 ),
+              'RANK'            => array( 216,   -1,    0,    0 ),
+              'ADDRESS'         => array( 219,   -1,    1,    0 ),
+              'DAYS360'         => array( 220,   -1,    1,    0 ),
+              'TODAY'           => array( 221,    0,    1,    1 ),
+              'VDB'             => array( 222,   -1,    1,    0 ),
+              'MEDIAN'          => array( 227,   -1,    0,    0 ),
+              'SUMPRODUCT'      => array( 228,   -1,    2,    0 ),
+              'SINH'            => array( 229,    1,    1,    0 ),
+              'COSH'            => array( 230,    1,    1,    0 ),
+              'TANH'            => array( 231,    1,    1,    0 ),
+              'ASINH'           => array( 232,    1,    1,    0 ),
+              'ACOSH'           => array( 233,    1,    1,    0 ),
+              'ATANH'           => array( 234,    1,    1,    0 ),
+              'DGET'            => array( 235,    3,    0,    0 ),
+              'INFO'            => array( 244,    1,    1,    1 ),
+              'DB'              => array( 247,   -1,    1,    0 ),
+              'FREQUENCY'       => array( 252,    2,    0,    0 ),
+              'ERROR.TYPE'      => array( 261,    1,    1,    0 ),
+              'REGISTER.ID'     => array( 267,   -1,    1,    0 ),
+              'AVEDEV'          => array( 269,   -1,    0,    0 ),
+              'BETADIST'        => array( 270,   -1,    1,    0 ),
+              'GAMMALN'         => array( 271,    1,    1,    0 ),
+              'BETAINV'         => array( 272,   -1,    1,    0 ),
+              'BINOMDIST'       => array( 273,    4,    1,    0 ),
+              'CHIDIST'         => array( 274,    2,    1,    0 ),
+              'CHIINV'          => array( 275,    2,    1,    0 ),
+              'COMBIN'          => array( 276,    2,    1,    0 ),
+              'CONFIDENCE'      => array( 277,    3,    1,    0 ),
+              'CRITBINOM'       => array( 278,    3,    1,    0 ),
+              'EVEN'            => array( 279,    1,    1,    0 ),
+              'EXPONDIST'       => array( 280,    3,    1,    0 ),
+              'FDIST'           => array( 281,    3,    1,    0 ),
+              'FINV'            => array( 282,    3,    1,    0 ),
+              'FISHER'          => array( 283,    1,    1,    0 ),
+              'FISHERINV'       => array( 284,    1,    1,    0 ),
+              'FLOOR'           => array( 285,    2,    1,    0 ),
+              'GAMMADIST'       => array( 286,    4,    1,    0 ),
+              'GAMMAINV'        => array( 287,    3,    1,    0 ),
+              'CEILING'         => array( 288,    2,    1,    0 ),
+              'HYPGEOMDIST'     => array( 289,    4,    1,    0 ),
+              'LOGNORMDIST'     => array( 290,    3,    1,    0 ),
+              'LOGINV'          => array( 291,    3,    1,    0 ),
+              'NEGBINOMDIST'    => array( 292,    3,    1,    0 ),
+              'NORMDIST'        => array( 293,    4,    1,    0 ),
+              'NORMSDIST'       => array( 294,    1,    1,    0 ),
+              'NORMINV'         => array( 295,    3,    1,    0 ),
+              'NORMSINV'        => array( 296,    1,    1,    0 ),
+              'STANDARDIZE'     => array( 297,    3,    1,    0 ),
+              'ODD'             => array( 298,    1,    1,    0 ),
+              'PERMUT'          => array( 299,    2,    1,    0 ),
+              'POISSON'         => array( 300,    3,    1,    0 ),
+              'TDIST'           => array( 301,    3,    1,    0 ),
+              'WEIBULL'         => array( 302,    4,    1,    0 ),
+              'SUMXMY2'         => array( 303,    2,    2,    0 ),
+              'SUMX2MY2'        => array( 304,    2,    2,    0 ),
+              'SUMX2PY2'        => array( 305,    2,    2,    0 ),
+              'CHITEST'         => array( 306,    2,    2,    0 ),
+              'CORREL'          => array( 307,    2,    2,    0 ),
+              'COVAR'           => array( 308,    2,    2,    0 ),
+              'FORECAST'        => array( 309,    3,    2,    0 ),
+              'FTEST'           => array( 310,    2,    2,    0 ),
+              'INTERCEPT'       => array( 311,    2,    2,    0 ),
+              'PEARSON'         => array( 312,    2,    2,    0 ),
+              'RSQ'             => array( 313,    2,    2,    0 ),
+              'STEYX'           => array( 314,    2,    2,    0 ),
+              'SLOPE'           => array( 315,    2,    2,    0 ),
+              'TTEST'           => array( 316,    4,    2,    0 ),
+              'PROB'            => array( 317,   -1,    2,    0 ),
+              'DEVSQ'           => array( 318,   -1,    0,    0 ),
+              'GEOMEAN'         => array( 319,   -1,    0,    0 ),
+              'HARMEAN'         => array( 320,   -1,    0,    0 ),
+              'SUMSQ'           => array( 321,   -1,    0,    0 ),
+              'KURT'            => array( 322,   -1,    0,    0 ),
+              'SKEW'            => array( 323,   -1,    0,    0 ),
+              'ZTEST'           => array( 324,   -1,    0,    0 ),
+              'LARGE'           => array( 325,    2,    0,    0 ),
+              'SMALL'           => array( 326,    2,    0,    0 ),
+              'QUARTILE'        => array( 327,    2,    0,    0 ),
+              'PERCENTILE'      => array( 328,    2,    0,    0 ),
+              'PERCENTRANK'     => array( 329,   -1,    0,    0 ),
+              'MODE'            => array( 330,   -1,    2,    0 ),
+              'TRIMMEAN'        => array( 331,    2,    0,    0 ),
+              'TINV'            => array( 332,    2,    1,    0 ),
+              'CONCATENATE'     => array( 336,   -1,    1,    0 ),
+              'POWER'           => array( 337,    2,    1,    0 ),
+              'RADIANS'         => array( 342,    1,    1,    0 ),
+              'DEGREES'         => array( 343,    1,    1,    0 ),
+              'SUBTOTAL'        => array( 344,   -1,    0,    0 ),
+              'SUMIF'           => array( 345,   -1,    0,    0 ),
+              'COUNTIF'         => array( 346,    2,    0,    0 ),
+              'COUNTBLANK'      => array( 347,    1,    0,    0 ),
+              'ROMAN'           => array( 354,   -1,    1,    0 )
+              );
+    }
+
+    /**
+    * Convert a token to the proper ptg value.
+    *
+    * @access private
+    * @param mixed $token The token to convert.
+    * @return mixed the converted token on success. Die if the token
+    *               is not recognized
+    */
+    function _convert($token)
+    {
+        if (preg_match("/^\"[^\"]{0,255}\"$/", $token)) {
+            return $this->_convertString($token);
+
+        } elseif (is_numeric($token)) {
+            return $this->_convertNumber($token);
+
+        // match references like A1 or $A$1
+        } elseif (preg_match('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/',$token)) {
+            return $this->_convertRef2d($token);
+
+        // match external references like Sheet1!A1 or Sheet1:Sheet2!A1
+        } elseif (preg_match("/^\w+(\:\w+)?\![A-Ia-i]?[A-Za-z](\d+)$/u",$token)) {
+            return $this->_convertRef3d($token);
+
+        // match external references like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1
+        } elseif (preg_match("/^'[\w -]+(\:[\w -]+)?'\![A-Ia-i]?[A-Za-z](\d+)$/u",$token)) {
+            return $this->_convertRef3d($token);
+
+        // match ranges like A1:B2
+        } elseif (preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/",$token)) {
+            return $this->_convertRange2d($token);
+
+        // match ranges like A1..B2
+        } elseif (preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/",$token)) {
+            return $this->_convertRange2d($token);
+
+        // match external ranges like Sheet1!A1 or Sheet1:Sheet2!A1:B2
+        } elseif (preg_match("/^\w+(\:\w+)?\!([A-Ia-i]?[A-Za-z])?(\d+)\:([A-Ia-i]?[A-Za-z])?(\d+)$/u",$token)) {
+            return $this->_convertRange3d($token);
+
+        // match external ranges like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1:B2
+        } elseif (preg_match("/^'[\w -]+(\:[\w -]+)?'\!([A-Ia-i]?[A-Za-z])?(\d+)\:([A-Ia-i]?[A-Za-z])?(\d+)$/u",$token)) {
+            return $this->_convertRange3d($token);
+
+        // operators (including parentheses)
+        } elseif (isset($this->ptg[$token])) {
+            return pack("C", $this->ptg[$token]);
+
+        // commented so argument number can be processed correctly. See toReversePolish().
+        /*elseif (preg_match("/[A-Z0-9\xc0-\xdc\.]+/",$token))
+        {
+            return($this->_convertFunction($token,$this->_func_args));
+        }*/
+
+        // if it's an argument, ignore the token (the argument remains)
+        } elseif ($token == 'arg') {
+            return '';
+        }
+        // TODO: use real error codes
+        die("Unknown token $token");
+    }
+
+    /**
+    * Convert a number token to ptgInt or ptgNum
+    *
+    * @access private
+    * @param mixed $num an integer or double for conversion to its ptg value
+    */
+    function _convertNumber($num)
+    {
+        // Integer in the range 0..2**16-1
+        if ((preg_match("/^\d+$/", $num)) and ($num <= 65535)) {
+            return pack("Cv", $this->ptg['ptgInt'], $num);
+        } else { // A float
+            if ($this->_byte_order) { // if it's Big Endian
+                $num = strrev($num);
+            }
+            return pack("Cd", $this->ptg['ptgNum'], $num);
+        }
+    }
+
+    /**
+    * Convert a string token to ptgStr
+    *
+    * @access private
+    * @param string $string A string for conversion to its ptg value.
+    * @return mixed the converted token on success. PEAR_Error if the string
+    *               is longer than 255 characters.
+    */
+    function _convertString($string)
+    {
+        // chop away beggining and ending quotes
+        $string = substr($string, 1, strlen($string) - 2);
+        if (strlen($string) > 255) {
+            die("String is too long");
+        }
+
+        if ($this->_BIFF_version == 0x0500) {
+            return pack("CC", $this->ptg['ptgStr'], strlen($string)).$string;
+        } elseif ($this->_BIFF_version == 0x0600) {
+            $encoding = 0;   // TODO: Unicode support
+            return pack("CCC", $this->ptg['ptgStr'], strlen($string), $encoding).$string;
+        }
+    }
+
+    /**
+    * Convert a function to a ptgFunc or ptgFuncVarV depending on the number of
+    * args that it takes.
+    *
+    * @access private
+    * @param string  $token    The name of the function for convertion to ptg value.
+    * @param integer $num_args The number of arguments the function receives.
+    * @return string The packed ptg for the function
+    */
+    function _convertFunction($token, $num_args)
+    {
+        $args     = $this->_functions[$token][1];
+        $volatile = $this->_functions[$token][3];
+
+        // Fixed number of args eg. TIME($i,$j,$k).
+        if ($args >= 0) {
+            return pack("Cv", $this->ptg['ptgFuncV'], $this->_functions[$token][0]);
+        }
+        // Variable number of args eg. SUM($i,$j,$k, ..).
+        if ($args == -1) {
+            return pack("CCv", $this->ptg['ptgFuncVarV'], $num_args, $this->_functions[$token][0]);
+        }
+    }
+
+    /**
+    * Convert an Excel range such as A1:D4 to a ptgRefV.
+    *
+    * @access private
+    * @param string $range An Excel range in the A1:A2 or A1..A2 format.
+    */
+    function _convertRange2d($range)
+    {
+        $class = 2; // as far as I know, this is magick.
+
+        // Split the range into 2 cell refs
+        if (preg_match("/^([A-Ia-i]?[A-Za-z])(\d+)\:([A-Ia-i]?[A-Za-z])(\d+)$/", $range)) {
+            list($cell1, $cell2) = split(':', $range);
+        } elseif (preg_match("/^([A-Ia-i]?[A-Za-z])(\d+)\.\.([A-Ia-i]?[A-Za-z])(\d+)$/", $range)) {
+            list($cell1, $cell2) = split('\.\.', $range);
+
+        } else {
+            // TODO: use real error codes
+            die("Unknown range separator");
+        }
+
+        // Convert the cell references
+        $cell_array1 = $this->_cellToPackedRowcol($cell1);
+        list($row1, $col1) = $cell_array1;
+        $cell_array2 = $this->_cellToPackedRowcol($cell2);
+        list($row2, $col2) = $cell_array2;
+
+        // The ptg value depends on the class of the ptg.
+        if ($class == 0) {
+            $ptgArea = pack("C", $this->ptg['ptgArea']);
+        } elseif ($class == 1) {
+            $ptgArea = pack("C", $this->ptg['ptgAreaV']);
+        } elseif ($class == 2) {
+            $ptgArea = pack("C", $this->ptg['ptgAreaA']);
+        } else {
+            // TODO: use real error codes
+            die("Unknown class $class");
+        }
+        return $ptgArea . $row1 . $row2 . $col1. $col2;
+    }
+
+    /**
+    * Convert an Excel 3d range such as "Sheet1!A1:D4" or "Sheet1:Sheet2!A1:D4" to
+    * a ptgArea3d.
+    *
+    * @access private
+    * @param string $token An Excel range in the Sheet1!A1:A2 format.
+    * @return mixed The packed ptgArea3d token on success, PEAR_Error on failure.
+    */
+    function _convertRange3d($token)
+    {
+        $class = 2; // as far as I know, this is magick.
+
+        // Split the ref at the ! symbol
+        list($ext_ref, $range) = split('!', $token);
+
+        // Convert the external reference part (different for BIFF8)
+        if ($this->_BIFF_version == 0x0500) {
+            $ext_ref = $this->_packExtRef($ext_ref);
+        } elseif ($this->_BIFF_version == 0x0600) {
+             $ext_ref = $this->_getRefIndex($ext_ref);
+        }
+
+        // Split the range into 2 cell refs
+        list($cell1, $cell2) = split(':', $range);
+
+        // Convert the cell references
+        if (preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/", $cell1)) {
+            $cell_array1 = $this->_cellToPackedRowcol($cell1);
+            list($row1, $col1) = $cell_array1;
+            $cell_array2 = $this->_cellToPackedRowcol($cell2);
+            list($row2, $col2) = $cell_array2;
+        } else { // It's a rows range (like 26:27)
+             $cells_array = $this->_rangeToPackedRange($cell1.':'.$cell2);
+             list($row1, $col1, $row2, $col2) = $cells_array;
+        }
+
+        // The ptg value depends on the class of the ptg.
+        if ($class == 0) {
+            $ptgArea = pack("C", $this->ptg['ptgArea3d']);
+        } elseif ($class == 1) {
+            $ptgArea = pack("C", $this->ptg['ptgArea3dV']);
+        } elseif ($class == 2) {
+            $ptgArea = pack("C", $this->ptg['ptgArea3dA']);
+        } else {
+            die("Unknown class $class");
+        }
+
+        return $ptgArea . $ext_ref . $row1 . $row2 . $col1. $col2;
+    }
+
+    /**
+    * Convert an Excel reference such as A1, $B2, C$3 or $D$4 to a ptgRefV.
+    *
+    * @access private
+    * @param string $cell An Excel cell reference
+    * @return string The cell in packed() format with the corresponding ptg
+    */
+    function _convertRef2d($cell)
+    {
+        $class = 2; // as far as I know, this is magick.
+
+        // Convert the cell reference
+        $cell_array = $this->_cellToPackedRowcol($cell);
+        list($row, $col) = $cell_array;
+
+        // The ptg value depends on the class of the ptg.
+        if ($class == 0) {
+            $ptgRef = pack("C", $this->ptg['ptgRef']);
+        } elseif ($class == 1) {
+            $ptgRef = pack("C", $this->ptg['ptgRefV']);
+        } elseif ($class == 2) {
+            $ptgRef = pack("C", $this->ptg['ptgRefA']);
+        } else {
+            // TODO: use real error codes
+            die("Unknown class $class");
+        }
+        return $ptgRef.$row.$col;
+    }
+
+    /**
+    * Convert an Excel 3d reference such as "Sheet1!A1" or "Sheet1:Sheet2!A1" to a
+    * ptgRef3d.
+    *
+    * @access private
+    * @param string $cell An Excel cell reference
+    * @return mixed The packed ptgRef3d token on success, PEAR_Error on failure.
+    */
+    function _convertRef3d($cell)
+    {
+        $class = 2; // as far as I know, this is magick.
+
+        // Split the ref at the ! symbol
+        list($ext_ref, $cell) = split('!', $cell);
+
+        // Convert the external reference part (different for BIFF8)
+        if ($this->_BIFF_version == 0x0500) {
+            $ext_ref = $this->_packExtRef($ext_ref);
+        } elseif ($this->_BIFF_version == 0x0600) {
+            $ext_ref = $this->_getRefIndex($ext_ref);
+        }
+
+        // Convert the cell reference part
+        list($row, $col) = $this->_cellToPackedRowcol($cell);
+
+        // The ptg value depends on the class of the ptg.
+        if ($class == 0) {
+            $ptgRef = pack("C", $this->ptg['ptgRef3d']);
+        } elseif ($class == 1) {
+            $ptgRef = pack("C", $this->ptg['ptgRef3dV']);
+        } elseif ($class == 2) {
+            $ptgRef = pack("C", $this->ptg['ptgRef3dA']);
+        } else {
+            die("Unknown class $class");
+        }
+
+        return $ptgRef . $ext_ref. $row . $col;
+    }
+
+    /**
+    * Convert the sheet name part of an external reference, for example "Sheet1" or
+    * "Sheet1:Sheet2", to a packed structure.
+    *
+    * @access private
+    * @param string $ext_ref The name of the external reference
+    * @return string The reference index in packed() format
+    */
+    function _packExtRef($ext_ref)
+    {
+        $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading  ' if any.
+        $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any.
+
+        // Check if there is a sheet range eg., Sheet1:Sheet2.
+        if (preg_match("/:/", $ext_ref)) {
+            list($sheet_name1, $sheet_name2) = split(':', $ext_ref);
+
+            $sheet1 = $this->_getSheetIndex($sheet_name1);
+            if ($sheet1 == -1) {
+                die("Unknown sheet name $sheet_name1 in formula");
+            }
+            $sheet2 = $this->_getSheetIndex($sheet_name2);
+            if ($sheet2 == -1) {
+                die("Unknown sheet name $sheet_name2 in formula");
+            }
+
+            // Reverse max and min sheet numbers if necessary
+            if ($sheet1 > $sheet2) {
+                list($sheet1, $sheet2) = array($sheet2, $sheet1);
+            }
+        } else { // Single sheet name only.
+            $sheet1 = $this->_getSheetIndex($ext_ref);
+            if ($sheet1 == -1) {
+                die("Unknown sheet name $ext_ref in formula");
+            }
+            $sheet2 = $sheet1;
+        }
+
+        // References are stored relative to 0xFFFF.
+        $offset = -1 - $sheet1;
+
+        return pack('vdvv', $offset, 0x00, $sheet1, $sheet2);
+    }
+
+    /**
+    * Look up the REF index that corresponds to an external sheet name
+    * (or range). If it doesn't exist yet add it to the workbook's references
+    * array. It assumes all sheet names given must exist.
+    *
+    * @access private
+    * @param string $ext_ref The name of the external reference
+    * @return mixed The reference index in packed() format on success,
+    *               PEAR_Error on failure
+    */
+    function _getRefIndex($ext_ref)
+    {
+        $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading  ' if any.
+        $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any.
+
+        // Check if there is a sheet range eg., Sheet1:Sheet2.
+        if (preg_match("/:/", $ext_ref)) {
+            list($sheet_name1, $sheet_name2) = split(':', $ext_ref);
+
+            $sheet1 = $this->_getSheetIndex($sheet_name1);
+            if ($sheet1 == -1) {
+                die("Unknown sheet name $sheet_name1 in formula");
+            }
+            $sheet2 = $this->_getSheetIndex($sheet_name2);
+            if ($sheet2 == -1) {
+                die("Unknown sheet name $sheet_name2 in formula");
+            }
+
+            // Reverse max and min sheet numbers if necessary
+            if ($sheet1 > $sheet2) {
+                list($sheet1, $sheet2) = array($sheet2, $sheet1);
+            }
+        } else { // Single sheet name only.
+            $sheet1 = $this->_getSheetIndex($ext_ref);
+            if ($sheet1 == -1) {
+                die("Unknown sheet name $ext_ref in formula");
+            }
+            $sheet2 = $sheet1;
+        }
+
+        // assume all references belong to this document
+        $supbook_index = 0x00;
+        $ref = pack('vvv', $supbook_index, $sheet1, $sheet2);
+        $total_references = count($this->_references);
+        $index = -1;
+        for ($i = 0; $i < $total_references; $i++) {
+            if ($ref == $this->_references[$i]) {
+                $index = $i;
+                break;
+            }
+        }
+        // if REF was not found add it to references array
+        if ($index == -1) {
+            $this->_references[$total_references] = $ref;
+            $index = $total_references;
+        }
+
+        return pack('v', $index);
+    }
+
+    /**
+    * Look up the index that corresponds to an external sheet name. The hash of
+    * sheet names is updated by the addworksheet() method of the
+    * Spreadsheet_Excel_Writer_Workbook class.
+    *
+    * @access private
+    * @return integer The sheet index, -1 if the sheet was not found
+    */
+    function _getSheetIndex($sheet_name)
+    {
+        if (!isset($this->_ext_sheets[$sheet_name])) {
+            return -1;
+        } else {
+            return $this->_ext_sheets[$sheet_name];
+        }
+    }
+
+    /**
+    * This method is used to update the array of sheet names. It is
+    * called by the addWorksheet() method of the
+    * Spreadsheet_Excel_Writer_Workbook class.
+    *
+    * @access public
+    * @see Spreadsheet_Excel_Writer_Workbook::addWorksheet()
+    * @param string  $name  The name of the worksheet being added
+    * @param integer $index The index of the worksheet being added
+    */
+    function setExtSheet($name, $index)
+    {
+        $this->_ext_sheets[$name] = $index;
+    }
+
+    /**
+    * pack() row and column into the required 3 or 4 byte format.
+    *
+    * @access private
+    * @param string $cell The Excel cell reference to be packed
+    * @return array Array containing the row and column in packed() format
+    */
+    function _cellToPackedRowcol($cell)
+    {
+        $cell = strtoupper($cell);
+        list($row, $col, $row_rel, $col_rel) = $this->_cellToRowcol($cell);
+        if ($col >= 256) {
+            die("Column in: $cell greater than 255");
+        }
+        // FIXME: change for BIFF8
+        if ($row >= 16384) {
+            die("Row in: $cell greater than 16384 ");
+        }
+
+        // Set the high bits to indicate if row or col are relative.
+        if ($this->_BIFF_version == 0x0500) {
+            $row    |= $col_rel << 14;
+            $row    |= $row_rel << 15;
+            $col     = pack('C', $col);
+        } elseif ($this->_BIFF_version == 0x0600) {
+            $col    |= $col_rel << 14;
+            $col    |= $row_rel << 15;
+            $col     = pack('v', $col);
+        }
+        $row     = pack('v', $row);
+
+        return array($row, $col);
+    }
+
+    /**
+    * pack() row range into the required 3 or 4 byte format.
+    * Just using maximum col/rows, which is probably not the correct solution
+    *
+    * @access private
+    * @param string $range The Excel range to be packed
+    * @return array Array containing (row1,col1,row2,col2) in packed() format
+    */
+    function _rangeToPackedRange($range)
+    {
+        preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match);
+        // return absolute rows if there is a $ in the ref
+        $row1_rel = empty($match[1]) ? 1 : 0;
+        $row1     = $match[2];
+        $row2_rel = empty($match[3]) ? 1 : 0;
+        $row2     = $match[4];
+        // Convert 1-index to zero-index
+        $row1--;
+        $row2--;
+        // Trick poor inocent Excel
+        $col1 = 0;
+        $col2 = 16383; // FIXME: maximum possible value for Excel 5 (change this!!!)
+
+        // FIXME: this changes for BIFF8
+        if (($row1 >= 16384) or ($row2 >= 16384)) {
+            die("Row in: $range greater than 16384 ");
+        }
+
+        // Set the high bits to indicate if rows are relative.
+        if ($this->_BIFF_version == 0x0500) {
+            $row1    |= $row1_rel << 14; // FIXME: probably a bug
+            $row2    |= $row2_rel << 15;
+            $col1     = pack('C', $col1);
+            $col2     = pack('C', $col2);
+        } elseif ($this->_BIFF_version == 0x0600) {
+            $col1    |= $row1_rel << 15;
+            $col2    |= $row2_rel << 15;
+            $col1     = pack('v', $col1);
+            $col2     = pack('v', $col2);
+        }
+        $row1     = pack('v', $row1);
+        $row2     = pack('v', $row2);
+
+        return array($row1, $col1, $row2, $col2);
+    }
+
+    /**
+    * Convert an Excel cell reference such as A1 or $B2 or C$3 or $D$4 to a zero
+    * indexed row and column number. Also returns two (0,1) values to indicate
+    * whether the row or column are relative references.
+    *
+    * @access private
+    * @param string $cell The Excel cell reference in A1 format.
+    * @return array
+    */
+    function _cellToRowcol($cell)
+    {
+        preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/',$cell,$match);
+        // return absolute column if there is a $ in the ref
+        $col_rel = empty($match[1]) ? 1 : 0;
+        $col_ref = $match[2];
+        $row_rel = empty($match[3]) ? 1 : 0;
+        $row     = $match[4];
+
+        // Convert base26 column string to a number.
+        $expn   = strlen($col_ref) - 1;
+        $col    = 0;
+        $col_ref_length = strlen($col_ref);
+        for ($i = 0; $i < $col_ref_length; $i++) {
+            $col += (ord($col_ref{$i}) - ord('A') + 1) * pow(26, $expn);
+            $expn--;
+        }
+
+        // Convert 1-index to zero-index
+        $row--;
+        $col--;
+
+        return array($row, $col, $row_rel, $col_rel);
+    }
+
+    /**
+    * Advance to the next valid token.
+    *
+    * @access private
+    */
+    function _advance()
+    {
+        $i = $this->_current_char;
+        $formula_length = strlen($this->_formula);
+        // eat up white spaces
+        if ($i < $formula_length) {
+            while ($this->_formula{$i} == " ") {
+                $i++;
+            }
+
+            if ($i < ($formula_length - 1)) {
+                $this->_lookahead = $this->_formula{$i+1};
+            }
+            $token = '';
+        }
+
+        while ($i < $formula_length) {
+            $token .= $this->_formula{$i};
+            if ($i < ($formula_length - 1)) {
+                $this->_lookahead = $this->_formula{$i+1};
+            } else {
+                $this->_lookahead = '';
+            }
+
+            if ($this->_match($token) != '') {
+                //if ($i < strlen($this->_formula) - 1) {
+                //    $this->_lookahead = $this->_formula{$i+1};
+                //}
+                $this->_current_char = $i + 1;
+                $this->_current_token = $token;
+                return 1;
+            }
+
+            if ($i < ($formula_length - 2)) {
+                $this->_lookahead = $this->_formula{$i+2};
+            } else { // if we run out of characters _lookahead becomes empty
+                $this->_lookahead = '';
+            }
+            $i++;
+        }
+        //die("Lexical error ".$this->_current_char);
+    }
+
+    /**
+    * Checks if it's a valid token.
+    *
+    * @access private
+    * @param mixed $token The token to check.
+    * @return mixed       The checked token or false on failure
+    */
+    function _match($token)
+    {
+        switch($token) {
+            case SPREADSHEET_EXCEL_WRITER_ADD:
+                return $token;
+                break;
+            case SPREADSHEET_EXCEL_WRITER_SUB:
+                return $token;
+                break;
+            case SPREADSHEET_EXCEL_WRITER_MUL:
+                return $token;
+                break;
+            case SPREADSHEET_EXCEL_WRITER_DIV:
+                return $token;
+                break;
+            case SPREADSHEET_EXCEL_WRITER_OPEN:
+                return $token;
+                break;
+            case SPREADSHEET_EXCEL_WRITER_CLOSE:
+                return $token;
+                break;
+            case SPREADSHEET_EXCEL_WRITER_COMA:
+                return $token;
+                break;
+            case SPREADSHEET_EXCEL_WRITER_SEMICOLON:
+                return $token;
+                break;
+            case SPREADSHEET_EXCEL_WRITER_GT:
+                if ($this->_lookahead == '=') { // it's a GE token
+                    break;
+                }
+                return $token;
+                break;
+            case SPREADSHEET_EXCEL_WRITER_LT:
+                // it's a LE or a NE token
+                if (($this->_lookahead == '=') or ($this->_lookahead == '>')) {
+                    break;
+                }
+                return $token;
+                break;
+            case SPREADSHEET_EXCEL_WRITER_GE:
+                return $token;
+                break;
+            case SPREADSHEET_EXCEL_WRITER_LE:
+                return $token;
+                break;
+            case SPREADSHEET_EXCEL_WRITER_EQ:
+                return $token;
+                break;
+            case SPREADSHEET_EXCEL_WRITER_NE:
+                return $token;
+                break;
+            default:
+                // if it's a reference
+                if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?[0-9]+$/',$token) and
+                   !ereg("[0-9]",$this->_lookahead) and 
+                   ($this->_lookahead != ':') and ($this->_lookahead != '.') and
+                   ($this->_lookahead != '!'))
+                {
+                    return $token;
+                }
+                // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1)
+                elseif (preg_match("/^\w+(\:\w+)?\![A-Ia-i]?[A-Za-z][0-9]+$/u",$token) and
+                       !ereg("[0-9]",$this->_lookahead) and
+                       ($this->_lookahead != ':') and ($this->_lookahead != '.'))
+                {
+                    return $token;
+                }
+                // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1)
+                elseif (preg_match("/^'[\w -]+(\:[\w -]+)?'\![A-Ia-i]?[A-Za-z][0-9]+$/u",$token) and
+                       !ereg("[0-9]",$this->_lookahead) and
+                       ($this->_lookahead != ':') and ($this->_lookahead != '.'))
+                {
+                    return $token;
+                }
+                // if it's a range (A1:A2)
+                elseif (preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+:(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+$/",$token) and 
+                       !ereg("[0-9]",$this->_lookahead))
+                {
+                    return $token;
+                }
+                // if it's a range (A1..A2)
+                elseif (preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+$/",$token) and 
+                       !ereg("[0-9]",$this->_lookahead))
+                {
+                    return $token;
+                }
+                // If it's an external range like Sheet1!A1 or Sheet1:Sheet2!A1:B2
+                elseif (preg_match("/^\w+(\:\w+)?\!([A-Ia-i]?[A-Za-z])?[0-9]+:([A-Ia-i]?[A-Za-z])?[0-9]+$/u",$token) and
+                       !ereg("[0-9]",$this->_lookahead))
+                {
+                    return $token;
+                }
+                // If it's an external range like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1:B2
+                elseif (preg_match("/^'[\w -]+(\:[\w -]+)?'\!([A-Ia-i]?[A-Za-z])?[0-9]+:([A-Ia-i]?[A-Za-z])?[0-9]+$/u",$token) and
+                       !ereg("[0-9]",$this->_lookahead))
+                {
+                    return $token;
+                }
+                // If it's a number (check that it's not a sheet name or range)
+                elseif (is_numeric($token) and 
+                        (!is_numeric($token.$this->_lookahead) or ($this->_lookahead == '')) and
+                        ($this->_lookahead != '!') and ($this->_lookahead != ':'))
+                {
+                    return $token;
+                }
+                // If it's a string (of maximum 255 characters)
+                elseif (ereg("^\"[^\"]{0,255}\"$",$token))
+                {
+                    return $token;
+                }
+                // if it's a function call
+                elseif (eregi("^[A-Z0-9\xc0-\xdc\.]+$",$token) and ($this->_lookahead == "("))
+                {
+                    return $token;
+                }
+                return '';
+        }
+    }
+
+    /**
+    * The parsing method. It parses a formula.
+    *
+    * @access public
+    * @param string $formula The formula to parse, without the initial equal
+    *                        sign (=).
+    * @return mixed true on success, PEAR_Error on failure
+    */
+    function parse($formula)
+    {
+        $this->_current_char = 0;
+        $this->_formula      = $formula;
+        $this->_lookahead    = $formula{1};
+        $this->_advance();
+        $this->_parse_tree   = $this->_condition();
+    }
+
+    /**
+    * It parses a condition. It assumes the following rule:
+    * Cond -> Expr [(">" | "<") Expr]
+    *
+    * @access private
+    * @return mixed The parsed ptg'd tree on success, PEAR_Error on failure
+    */
+    function _condition()
+    {
+        $result = $this->_expression();
+        if ($this->_current_token == SPREADSHEET_EXCEL_WRITER_LT) {
+            $this->_advance();
+            $result2 = $this->_expression();
+            $result = $this->_createTree('ptgLT', $result, $result2);
+        } elseif ($this->_current_token == SPREADSHEET_EXCEL_WRITER_GT) {
+            $this->_advance();
+            $result2 = $this->_expression();
+            $result = $this->_createTree('ptgGT', $result, $result2);
+        } elseif ($this->_current_token == SPREADSHEET_EXCEL_WRITER_LE) {
+            $this->_advance();
+            $result2 = $this->_expression();
+            $result = $this->_createTree('ptgLE', $result, $result2);
+        } elseif ($this->_current_token == SPREADSHEET_EXCEL_WRITER_GE) {
+            $this->_advance();
+            $result2 = $this->_expression();
+            $result = $this->_createTree('ptgGE', $result, $result2);
+        } elseif ($this->_current_token == SPREADSHEET_EXCEL_WRITER_EQ) {
+            $this->_advance();
+            $result2 = $this->_expression();
+            $result = $this->_createTree('ptgEQ', $result, $result2);
+        } elseif ($this->_current_token == SPREADSHEET_EXCEL_WRITER_NE) {
+            $this->_advance();
+            $result2 = $this->_expression();
+            $result = $this->_createTree('ptgNE', $result, $result2);
+        }
+        return $result;
+    }
+
+    /**
+    * It parses a expression. It assumes the following rule:
+    * Expr -> Term [("+" | "-") Term]
+    *      -> "string"
+    *      -> "-" Term
+    *
+    * @access private
+    * @return mixed The parsed ptg'd tree on success, PEAR_Error on failure
+    */
+    function _expression()
+    {
+        // If it's a string return a string node
+        if (ereg("^\"[^\"]{0,255}\"$", $this->_current_token)) {
+            $result = $this->_createTree($this->_current_token, '', '');
+            $this->_advance();
+            return $result;
+        } elseif ($this->_current_token == SPREADSHEET_EXCEL_WRITER_SUB) {
+            // catch "-" Term
+            $this->_advance();
+            $result2 = $this->_expression();
+            $result = $this->_createTree('ptgUminus', $result2, '');
+            return $result;
+        }
+        $result = $this->_term();
+        while (($this->_current_token == SPREADSHEET_EXCEL_WRITER_ADD) or
+               ($this->_current_token == SPREADSHEET_EXCEL_WRITER_SUB)) {
+        /**/
+            if ($this->_current_token == SPREADSHEET_EXCEL_WRITER_ADD) {
+                $this->_advance();
+                $result2 = $this->_term();
+                $result = $this->_createTree('ptgAdd', $result, $result2);
+            } else {
+                $this->_advance();
+                $result2 = $this->_term();
+                $result = $this->_createTree('ptgSub', $result, $result2);
+            }
+        }
+        return $result;
+    }
+
+    /**
+    * This function just introduces a ptgParen element in the tree, so that Excel
+    * doesn't get confused when working with a parenthesized formula afterwards.
+    *
+    * @access private
+    * @see _fact()
+    * @return array The parsed ptg'd tree
+    */
+    function _parenthesizedExpression()
+    {
+        $result = $this->_createTree('ptgParen', $this->_expression(), '');
+        return $result;
+    }
+
+    /**
+    * It parses a term. It assumes the following rule:
+    * Term -> Fact [("*" | "/") Fact]
+    *
+    * @access private
+    * @return mixed The parsed ptg'd tree on success, PEAR_Error on failure
+    */
+    function _term()
+    {
+        $result = $this->_fact();
+        while (($this->_current_token == SPREADSHEET_EXCEL_WRITER_MUL) or
+               ($this->_current_token == SPREADSHEET_EXCEL_WRITER_DIV)) {
+        /**/
+            if ($this->_current_token == SPREADSHEET_EXCEL_WRITER_MUL) {
+                $this->_advance();
+                $result2 = $this->_fact();
+                $result = $this->_createTree('ptgMul', $result, $result2);
+            } else {
+                $this->_advance();
+                $result2 = $this->_fact();
+                $result = $this->_createTree('ptgDiv', $result, $result2);
+            }
+        }
+        return $result;
+    }
+
+    /**
+    * It parses a factor. It assumes the following rule:
+    * Fact -> ( Expr )
+    *       | CellRef
+    *       | CellRange
+    *       | Number
+    *       | Function
+    *
+    * @access private
+    * @return mixed The parsed ptg'd tree on success, PEAR_Error on failure
+    */
+    function _fact()
+    {
+        if ($this->_current_token == SPREADSHEET_EXCEL_WRITER_OPEN) {
+            $this->_advance();         // eat the "("
+            $result = $this->_parenthesizedExpression();
+            if ($this->_current_token != SPREADSHEET_EXCEL_WRITER_CLOSE) {
+                die("')' token expected.");
+            }
+            $this->_advance();         // eat the ")"
+            return $result;
+        }
+        // if it's a reference
+        if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?[0-9]+$/',$this->_current_token))
+        {
+            $result = $this->_createTree($this->_current_token, '', '');
+            $this->_advance();
+            return $result;
+        }
+        // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1)
+        elseif (preg_match("/^\w+(\:\w+)?\![A-Ia-i]?[A-Za-z][0-9]+$/u",$this->_current_token))
+        {
+            $result = $this->_createTree($this->_current_token, '', '');
+            $this->_advance();
+            return $result;
+        }
+        // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1)
+        elseif (preg_match("/^'[\w -]+(\:[\w -]+)?'\![A-Ia-i]?[A-Za-z][0-9]+$/u",$this->_current_token))
+        {
+            $result = $this->_createTree($this->_current_token, '', '');
+            $this->_advance();
+            return $result;
+        }
+        // if it's a range
+        elseif (preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+:(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+$/",$this->_current_token) or 
+                preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+$/",$this->_current_token))
+        {
+            $result = $this->_current_token;
+            $this->_advance();
+            return $result;
+        }
+        // If it's an external range (Sheet1!A1 or Sheet1!A1:B2)
+        elseif (preg_match("/^\w+(\:\w+)?\!([A-Ia-i]?[A-Za-z])?[0-9]+:([A-Ia-i]?[A-Za-z])?[0-9]+$/u",$this->_current_token))
+        {
+            $result = $this->_current_token;
+            $this->_advance();
+            return $result;
+        }
+        // If it's an external range ('Sheet1'!A1 or 'Sheet1'!A1:B2)
+        elseif (preg_match("/^'[\w -]+(\:[\w -]+)?'\!([A-Ia-i]?[A-Za-z])?[0-9]+:([A-Ia-i]?[A-Za-z])?[0-9]+$/u",$this->_current_token))
+        {
+            $result = $this->_current_token;
+            $this->_advance();
+            return $result;
+        }
+        elseif (is_numeric($this->_current_token))
+        {
+            $result = $this->_createTree($this->_current_token, '', '');
+            $this->_advance();
+            return $result;
+        }
+        // if it's a function call
+        elseif (eregi("^[A-Z0-9\xc0-\xdc\.]+$",$this->_current_token))
+        {
+            $result = $this->_func();
+            return $result;
+        }
+        die("Syntax error: ".$this->_current_token.
+                                 ", lookahead: ".$this->_lookahead.
+                                 ", current char: ".$this->_current_char);
+    }
+
+    /**
+    * It parses a function call. It assumes the following rule:
+    * Func -> ( Expr [,Expr]* )
+    *
+    * @access private
+    * @return mixed The parsed ptg'd tree on success, PEAR_Error on failure
+    */
+    function _func()
+    {
+        $num_args = 0; // number of arguments received
+        $function = strtoupper($this->_current_token);
+        $result   = ''; // initialize result
+        $this->_advance();
+        $this->_advance();         // eat the "("
+        while ($this->_current_token != ')') {
+        /**/
+            if ($num_args > 0) {
+                if ($this->_current_token == SPREADSHEET_EXCEL_WRITER_COMA or
+                    $this->_current_token == SPREADSHEET_EXCEL_WRITER_SEMICOLON)
+                {
+                    $this->_advance();  // eat the "," or ";"
+                } else {
+                    die("Syntax error: comma expected in ".
+                                      "function $function, arg #{$num_args}");
+                }
+                $result2 = $this->_condition();
+                $result = $this->_createTree('arg', $result, $result2);
+            } else { // first argument
+                $result2 = $this->_condition();
+                $result = $this->_createTree('arg', '', $result2);
+            }
+            $num_args++;
+        }
+        if (!isset($this->_functions[$function])) {
+            die("Function $function() doesn't exist");
+        }
+        $args = $this->_functions[$function][1];
+        // If fixed number of args eg. TIME($i,$j,$k). Check that the number of args is valid.
+        if (($args >= 0) and ($args != $num_args)) {
+            die("Incorrect number of arguments in function $function() ");
+        }
+
+        $result = $this->_createTree($function, $result, $num_args);
+        $this->_advance();         // eat the ")"
+        return $result;
+    }
+
+    /**
+    * Creates a tree. In fact an array which may have one or two arrays (sub-trees)
+    * as elements.
+    *
+    * @access private
+    * @param mixed $value The value of this node.
+    * @param mixed $left  The left array (sub-tree) or a final node.
+    * @param mixed $right The right array (sub-tree) or a final node.
+    * @return array A tree
+    */
+    function _createTree($value, $left, $right)
+    {
+        return array('value' => $value, 'left' => $left, 'right' => $right);
+    }
+
+    /**
+    * Builds a string containing the tree in reverse polish notation (What you
+    * would use in a HP calculator stack).
+    * The following tree:
+    *
+    *    +
+    *   / \
+    *  2   3
+    *
+    * produces: "23+"
+    *
+    * The following tree:
+    *
+    *    +
+    *   / \
+    *  3   *
+    *     / \
+    *    6   A1
+    *
+    * produces: "36A1*+"
+    *
+    * In fact all operands, functions, references, etc... are written as ptg's
+    *
+    * @access public
+    * @param array $tree The optional tree to convert.
+    * @return string The tree in reverse polish notation
+    */
+    function toReversePolish($tree = array())
+    {
+        $polish = ""; // the string we are going to return
+        if (empty($tree)) { // If it's the first call use _parse_tree
+            $tree = $this->_parse_tree;
+        }
+        if (is_array($tree['left'])) {
+            $converted_tree = $this->toReversePolish($tree['left']);
+            $polish .= $converted_tree;
+        } elseif ($tree['left'] != '') { // It's a final node
+            $converted_tree = $this->_convert($tree['left']);
+            $polish .= $converted_tree;
+        }
+        if (is_array($tree['right'])) {
+            $converted_tree = $this->toReversePolish($tree['right']);
+            $polish .= $converted_tree;
+        } elseif ($tree['right'] != '') { // It's a final node
+            $converted_tree = $this->_convert($tree['right']);
+            $polish .= $converted_tree;
+        }
+        // if it's a function convert it here (so we can set it's arguments)
+        if (preg_match("/^[A-Z0-9\xc0-\xdc\.]+$/",$tree['value']) and
+            !preg_match('/^([A-Ia-i]?[A-Za-z])(\d+)$/',$tree['value']) and
+            !preg_match("/^[A-Ia-i]?[A-Za-z](\d+)\.\.[A-Ia-i]?[A-Za-z](\d+)$/",$tree['value']) and
+            !is_numeric($tree['value']) and
+            !isset($this->ptg[$tree['value']]))
+        {
+            // left subtree for a function is always an array.
+            if ($tree['left'] != '') {
+                $left_tree = $this->toReversePolish($tree['left']);
+            } else {
+                $left_tree = '';
+            }
+            // add it's left subtree and return.
+            return $left_tree.$this->_convertFunction($tree['value'], $tree['right']);
+        } else {
+            $converted_tree = $this->_convert($tree['value']);
+        }
+        $polish .= $converted_tree;
+        return $polish;
+    }
+}
+
+/**
+* Class for generating Excel Spreadsheets
+*
+* @author   Xavier Noguer <xnoguer@rezebra.com>
+* @category FileFormats
+* @package  Spreadsheet_Excel_Writer
+*/
+
+class Spreadsheet_Excel_Writer_Worksheet extends Spreadsheet_Excel_Writer_BIFFwriter
+{
+    /**
+    * Name of the Worksheet
+    * @var string
+    */
+    var $name;
+
+    /**
+    * Index for the Worksheet
+    * @var integer
+    */
+    var $index;
+
+    /**
+    * Reference to the (default) Format object for URLs
+    * @var object Format
+    */
+    var $_url_format;
+
+    /**
+    * Reference to the parser used for parsing formulas
+    * @var object Format
+    */
+    var $_parser;
+
+    /**
+    * Filehandle to the temporary file for storing data
+    * @var resource
+    */
+    var $_filehandle;
+
+    /**
+    * Boolean indicating if we are using a temporary file for storing data
+    * @var bool
+    */
+    var $_using_tmpfile;
+
+    /**
+    * Maximum number of rows for an Excel spreadsheet (BIFF5)
+    * @var integer
+    */
+    var $_xls_rowmax;
+
+    /**
+    * Maximum number of columns for an Excel spreadsheet (BIFF5)
+    * @var integer
+    */
+    var $_xls_colmax;
+
+    /**
+    * Maximum number of characters for a string (LABEL record in BIFF5)
+    * @var integer
+    */
+    var $_xls_strmax;
+
+    /**
+    * First row for the DIMENSIONS record
+    * @var integer
+    * @see _storeDimensions()
+    */
+    var $_dim_rowmin;
+
+    /**
+    * Last row for the DIMENSIONS record
+    * @var integer
+    * @see _storeDimensions()
+    */
+    var $_dim_rowmax;
+
+    /**
+    * First column for the DIMENSIONS record
+    * @var integer
+    * @see _storeDimensions()
+    */
+    var $_dim_colmin;
+
+    /**
+    * Last column for the DIMENSIONS record
+    * @var integer
+    * @see _storeDimensions()
+    */
+    var $_dim_colmax;
+
+    /**
+    * Array containing format information for columns
+    * @var array
+    */
+    var $_colinfo;
+
+    /**
+    * Array containing the selected area for the worksheet
+    * @var array
+    */
+    var $_selection;
+
+    /**
+    * Array containing the panes for the worksheet
+    * @var array
+    */
+    var $_panes;
+
+    /**
+    * The active pane for the worksheet
+    * @var integer
+    */
+    var $_active_pane;
+
+    /**
+    * Bit specifying if panes are frozen
+    * @var integer
+    */
+    var $_frozen;
+
+    /**
+    * Bit specifying if the worksheet is selected
+    * @var integer
+    */
+    var $selected;
+
+    /**
+    * The paper size (for printing) (DOCUMENT!!!)
+    * @var integer
+    */
+    var $_paper_size;
+
+    /**
+    * Bit specifying paper orientation (for printing). 0 => landscape, 1 => portrait
+    * @var integer
+    */
+    var $_orientation;
+
+    /**
+    * The page header caption
+    * @var string
+    */
+    var $_header;
+
+    /**
+    * The page footer caption
+    * @var string
+    */
+    var $_footer;
+
+    /**
+    * The horizontal centering value for the page
+    * @var integer
+    */
+    var $_hcenter;
+
+    /**
+    * The vertical centering value for the page
+    * @var integer
+    */
+    var $_vcenter;
+
+    /**
+    * The margin for the header
+    * @var float
+    */
+    var $_margin_head;
+
+    /**
+    * The margin for the footer
+    * @var float
+    */
+    var $_margin_foot;
+
+    /**
+    * The left margin for the worksheet in inches
+    * @var float
+    */
+    var $_margin_left;
+
+    /**
+    * The right margin for the worksheet in inches
+    * @var float
+    */
+    var $_margin_right;
+
+    /**
+    * The top margin for the worksheet in inches
+    * @var float
+    */
+    var $_margin_top;
+
+    /**
+    * The bottom margin for the worksheet in inches
+    * @var float
+    */
+    var $_margin_bottom;
+
+    /**
+    * First row to reapeat on each printed page
+    * @var integer
+    */
+    var $title_rowmin;
+
+    /**
+    * Last row to reapeat on each printed page
+    * @var integer
+    */
+    var $title_rowmax;
+
+    /**
+    * First column to reapeat on each printed page
+    * @var integer
+    */
+    var $title_colmin;
+
+    /**
+    * First row of the area to print
+    * @var integer
+    */
+    var $print_rowmin;
+
+    /**
+    * Last row to of the area to print
+    * @var integer
+    */
+    var $print_rowmax;
+
+    /**
+    * First column of the area to print
+    * @var integer
+    */
+    var $print_colmin;
+
+    /**
+    * Last column of the area to print
+    * @var integer
+    */
+    var $print_colmax;
+
+    /**
+    * Whether to use outline.
+    * @var integer
+    */
+    var $_outline_on;
+
+    /**
+    * Auto outline styles.
+    * @var bool
+    */
+    var $_outline_style;
+
+    /**
+    * Whether to have outline summary below.
+    * @var bool
+    */
+    var $_outline_below;
+
+    /**
+    * Whether to have outline summary at the right.
+    * @var bool
+    */
+    var $_outline_right;
+
+    /**
+    * Outline row level.
+    * @var integer
+    */
+    var $_outline_row_level;
+
+    /**
+    * Whether to fit to page when printing or not.
+    * @var bool
+    */
+    var $_fit_page;
+
+    /**
+    * Number of pages to fit wide
+    * @var integer
+    */
+    var $_fit_width;
+
+    /**
+    * Number of pages to fit high
+    * @var integer
+    */
+    var $_fit_height;
+
+    /**
+    * Reference to the total number of strings in the workbook
+    * @var integer
+    */
+    var $_str_total;
+
+    /**
+    * Reference to the number of unique strings in the workbook
+    * @var integer
+    */
+    var $_str_unique;
+
+    /**
+    * Reference to the array containing all the unique strings in the workbook
+    * @var array
+    */
+    var $_str_table;
+
+    /**
+    * Merged cell ranges
+    * @var array
+    */
+    var $_merged_ranges;
+
+    /**
+    * Charset encoding currently used when calling writeString()
+    * @var string
+    */
+    var $_input_encoding;
+
+    /**
+    * Constructor
+    *
+    * @param string  $name         The name of the new worksheet
+    * @param integer $index        The index of the new worksheet
+    * @param mixed   &$activesheet The current activesheet of the workbook we belong to
+    * @param mixed   &$firstsheet  The first worksheet in the workbook we belong to
+    * @param mixed   &$url_format  The default format for hyperlinks
+    * @param mixed   &$parser      The formula parser created for the Workbook
+    * @access private
+    */
+    function Spreadsheet_Excel_Writer_Worksheet($BIFF_version, $name,
+                                                $index, &$activesheet,
+                                                &$firstsheet, &$str_total,
+                                                &$str_unique, &$str_table,
+                                                &$url_format, &$parser)
+    {
+        // It needs to call its parent's constructor explicitly
+        $this->Spreadsheet_Excel_Writer_BIFFwriter();
+        $this->_BIFF_version   = $BIFF_version;
+        $rowmax                = 65536; // 16384 in Excel 5
+        $colmax                = 256;
+
+        $this->name            = $name;
+        $this->index           = $index;
+        $this->activesheet     = &$activesheet;
+        $this->firstsheet      = &$firstsheet;
+        $this->_str_total      = &$str_total;
+        $this->_str_unique     = &$str_unique;
+        $this->_str_table      = &$str_table;
+        $this->_url_format     = &$url_format;
+        $this->_parser         = &$parser;
+
+        //$this->ext_sheets      = array();
+        $this->_filehandle     = '';
+        $this->_using_tmpfile  = true;
+        //$this->fileclosed      = 0;
+        //$this->offset          = 0;
+        $this->_xls_rowmax     = $rowmax;
+        $this->_xls_colmax     = $colmax;
+        $this->_xls_strmax     = 255;
+        $this->_dim_rowmin     = $rowmax + 1;
+        $this->_dim_rowmax     = 0;
+        $this->_dim_colmin     = $colmax + 1;
+        $this->_dim_colmax     = 0;
+        $this->_colinfo        = array();
+        $this->_selection      = array(0,0,0,0);
+        $this->_panes          = array();
+        $this->_active_pane    = 3;
+        $this->_frozen         = 0;
+        $this->selected        = 0;
+
+        $this->_paper_size      = 0x0;
+        $this->_orientation     = 0x1;
+        $this->_header          = '';
+        $this->_footer          = '';
+        $this->_hcenter         = 0;
+        $this->_vcenter         = 0;
+        $this->_margin_head     = 0.50;
+        $this->_margin_foot     = 0.50;
+        $this->_margin_left     = 0.75;
+        $this->_margin_right    = 0.75;
+        $this->_margin_top      = 1.00;
+        $this->_margin_bottom   = 1.00;
+
+        $this->title_rowmin     = null;
+        $this->title_rowmax     = null;
+        $this->title_colmin     = null;
+        $this->title_colmax     = null;
+        $this->print_rowmin     = null;
+        $this->print_rowmax     = null;
+        $this->print_colmin     = null;
+        $this->print_colmax     = null;
+
+        $this->_print_gridlines  = 1;
+        $this->_screen_gridlines = 1;
+        $this->_print_headers    = 0;
+
+        $this->_fit_page        = 0;
+        $this->_fit_width       = 0;
+        $this->_fit_height      = 0;
+
+        $this->_hbreaks         = array();
+        $this->_vbreaks         = array();
+
+        $this->_protect         = 0;
+        $this->_password        = null;
+
+        $this->col_sizes        = array();
+        $this->_row_sizes        = array();
+
+        $this->_zoom            = 100;
+        $this->_print_scale     = 100;
+
+        $this->_outline_row_level = 0;
+        $this->_outline_style     = 0;
+        $this->_outline_below     = 1;
+        $this->_outline_right     = 1;
+        $this->_outline_on        = 1;
+
+        $this->_merged_ranges     = array();
+        
+               $this->_rtl                               = 0;  // Added by Joe Hunt 2009-03-05 for arabic languages
+        $this->_input_encoding    = '';
+
+        $this->_dv                = array();
+
+        $this->_initialize();
+    }
+
+    /**
+    * Open a tmp file to store the majority of the Worksheet data. If this fails,
+    * for example due to write permissions, store the data in memory. This can be
+    * slow for large files.
+    *
+    * @access private
+    */
+    function _initialize()
+    {
+        // Open tmp file for storing Worksheet data
+        $fh = tmpfile();
+        if ($fh) {
+            // Store filehandle
+            $this->_filehandle = $fh;
+        } else {
+            // If tmpfile() fails store data in memory
+            $this->_using_tmpfile = false;
+        }
+    }
+
+    /**
+    * Add data to the beginning of the workbook (note the reverse order)
+    * and to the end of the workbook.
+    *
+    * @access public
+    * @see Spreadsheet_Excel_Writer_Workbook::storeWorkbook()
+    * @param array $sheetnames The array of sheetnames from the Workbook this
+    *                          worksheet belongs to
+    */
+    function close($sheetnames)
+    {
+        $num_sheets = count($sheetnames);
+
+        /***********************************************
+        * Prepend in reverse order!!
+        */
+
+        // Prepend the sheet dimensions
+        $this->_storeDimensions();
+
+        // Prepend the sheet password
+        $this->_storePassword();
+
+        // Prepend the sheet protection
+        $this->_storeProtect();
+
+        // Prepend the page setup
+        $this->_storeSetup();
+
+        /* FIXME: margins are actually appended */
+        // Prepend the bottom margin
+        $this->_storeMarginBottom();
+
+        // Prepend the top margin
+        $this->_storeMarginTop();
+
+        // Prepend the right margin
+        $this->_storeMarginRight();
+
+        // Prepend the left margin
+        $this->_storeMarginLeft();
+
+        // Prepend the page vertical centering
+        $this->_storeVcenter();
+
+        // Prepend the page horizontal centering
+        $this->_storeHcenter();
+
+        // Prepend the page footer
+        $this->_storeFooter();
+
+        // Prepend the page header
+        $this->_storeHeader();
+
+        // Prepend the vertical page breaks
+        $this->_storeVbreak();
+
+        // Prepend the horizontal page breaks
+        $this->_storeHbreak();
+
+        // Prepend WSBOOL
+        $this->_storeWsbool();
+
+        // Prepend GRIDSET
+        $this->_storeGridset();
+
+         //  Prepend GUTS
+        if ($this->_BIFF_version == 0x0500) {
+            $this->_storeGuts();
+        }
+
+        // Prepend PRINTGRIDLINES
+        $this->_storePrintGridlines();
+
+        // Prepend PRINTHEADERS
+        $this->_storePrintHeaders();
+
+        // Prepend EXTERNSHEET references
+        if ($this->_BIFF_version == 0x0500) {
+            for ($i = $num_sheets; $i > 0; $i--) {
+                $sheetname = $sheetnames[$i-1];
+                $this->_storeExternsheet($sheetname);
+            }
+        }
+
+        // Prepend the EXTERNCOUNT of external references.
+        if ($this->_BIFF_version == 0x0500) {
+            $this->_storeExterncount($num_sheets);
+        }
+
+        // Prepend the COLINFO records if they exist
+        if (!empty($this->_colinfo)) {
+            $colcount = count($this->_colinfo);
+            for ($i = 0; $i < $colcount; $i++) {
+                $this->_storeColinfo($this->_colinfo[$i]);
+            }
+            $this->_storeDefcol();
+        }
+
+        // Prepend the BOF record
+        $this->_storeBof(0x0010);
+
+        /*
+        * End of prepend. Read upwards from here.
+        ***********************************************/
+
+        // Append
+        $this->_storeWindow2();
+        $this->_storeZoom();
+        if (!empty($this->_panes)) {
+            $this->_storePanes($this->_panes);
+        }
+        $this->_storeSelection($this->_selection);
+        $this->_storeMergedCells();
+        /* TODO: add data validity */
+        /*if ($this->_BIFF_version == 0x0600) {
+            $this->_storeDataValidity();
+        }*/
+        $this->_storeEof();
+    }
+
+    /**
+    * Retrieve the worksheet name.
+    * This is usefull when creating worksheets without a name.
+    *
+    * @access public
+    * @return string The worksheet's name
+    */
+    function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+    * Retrieves data from memory in one chunk, or from disk in $buffer
+    * sized chunks.
+    *
+    * @return string The data
+    */
+    function getData()
+    {
+        $buffer = 4096;
+
+        // Return data stored in memory
+        if (isset($this->_data)) {
+            $tmp   = $this->_data;
+            unset($this->_data);
+            $fh    = $this->_filehandle;
+            if ($this->_using_tmpfile) {
+                fseek($fh, 0);
+            }
+            return $tmp;
+        }
+        // Return data stored on disk
+        if ($this->_using_tmpfile) {
+            if ($tmp = fread($this->_filehandle, $buffer)) {
+                return $tmp;
+            }
+        }
+
+        // No data to return
+        return '';
+    }
+
+    /**
+    * Sets a merged cell range
+    *
+    * @access public
+    * @param integer $first_row First row of the area to merge
+    * @param integer $first_col First column of the area to merge
+    * @param integer $last_row  Last row of the area to merge
+    * @param integer $last_col  Last column of the area to merge
+    */
+    function setMerge($first_row, $first_col, $last_row, $last_col)
+    {
+        if (($last_row < $first_row) || ($last_col < $first_col)) {
+            return;
+        }
+        // don't check rowmin, rowmax, etc... because we don't know when this
+        // is going to be called
+        $this->_merged_ranges[] = array($first_row, $first_col, $last_row, $last_col);
+    }
+
+    /**
+    * Set this worksheet as a selected worksheet,
+    * i.e. the worksheet has its tab highlighted.
+    *
+    * @access public
+    */
+    function select()
+    {
+        $this->selected = 1;
+    }
+
+    /**
+    * Set this worksheet as the active worksheet,
+    * i.e. the worksheet that is displayed when the workbook is opened.
+    * Also set it as selected.
+    *
+    * @access public
+    */
+    function activate()
+    {
+        $this->selected = 1;
+        $this->activesheet = $this->index;
+    }
+
+    /**
+    * Set this worksheet as the first visible sheet.
+    * This is necessary when there are a large number of worksheets and the
+    * activated worksheet is not visible on the screen.
+    *
+    * @access public
+    */
+    function setFirstSheet()
+    {
+        $this->firstsheet = $this->index;
+    }
+
+    /**
+    * Set the worksheet protection flag
+    * to prevent accidental modification and to
+    * hide formulas if the locked and hidden format properties have been set.
+    *
+    * @access public
+    * @param string $password The password to use for protecting the sheet.
+    */
+    function protect($password)
+    {
+        $this->_protect   = 1;
+        $this->_password  = $this->_encodePassword($password);
+    }
+
+    /**
+    * Set the width of a single column or a range of columns.
+    *
+    * @access public
+    * @param integer $firstcol first column on the range
+    * @param integer $lastcol  last column on the range
+    * @param integer $width    width to set
+    * @param mixed   $format   The optional XF format to apply to the columns
+    * @param integer $hidden   The optional hidden atribute
+    * @param integer $level    The optional outline level
+    */
+    function setColumn($firstcol, $lastcol, $width, $format = null, $hidden = 0, $level = 0)
+    {
+        $this->_colinfo[] = array($firstcol, $lastcol, $width, &$format, $hidden, $level);
+
+        // Set width to zero if column is hidden
+        $width = ($hidden) ? 0 : $width;
+
+        for ($col = $firstcol; $col <= $lastcol; $col++) {
+            $this->col_sizes[$col] = $width;
+        }
+    }
+
+    /**
+    * Set which cell or cells are selected in a worksheet
+    *
+    * @access public
+    * @param integer $first_row    first row in the selected quadrant
+    * @param integer $first_column first column in the selected quadrant
+    * @param integer $last_row     last row in the selected quadrant
+    * @param integer $last_column  last column in the selected quadrant
+    */
+    function setSelection($first_row,$first_column,$last_row,$last_column)
+    {
+        $this->_selection = array($first_row,$first_column,$last_row,$last_column);
+    }
+
+    /**
+    * Set panes and mark them as frozen.
+    *
+    * @access public
+    * @param array $panes This is the only parameter received and is composed of the following:
+    *                     0 => Vertical split position,
+    *                     1 => Horizontal split position
+    *                     2 => Top row visible
+    *                     3 => Leftmost column visible
+    *                     4 => Active pane
+    */
+    function freezePanes($panes)
+    {
+        $this->_frozen = 1;
+        $this->_panes  = $panes;
+    }
+
+    /**
+    * Set panes and mark them as unfrozen.
+    *
+    * @access public
+    * @param array $panes This is the only parameter received and is composed of the following:
+    *                     0 => Vertical split position,
+    *                     1 => Horizontal split position
+    *                     2 => Top row visible
+    *                     3 => Leftmost column visible
+    *                     4 => Active pane
+    */
+    function thawPanes($panes)
+    {
+        $this->_frozen = 0;
+        $this->_panes  = $panes;
+    }
+
+    /**
+    * Set the page orientation as portrait.
+    *
+    * @access public
+    */
+    function setPortrait()
+    {
+        $this->_orientation = 1;
+    }
+
+    /**
+    * Set the page orientation as landscape.
+    *
+    * @access public
+    */
+    function setLandscape()
+    {
+        $this->_orientation = 0;
+    }
+
+    /**
+    * Set the paper type. Ex. 1 = US Letter, 9 = A4
+    *
+    * @access public
+    * @param integer $size The type of paper size to use
+    */
+    function setPaper($size = 0)
+    {
+        $this->_paper_size = $size;
+    }
+
+
+    /**
+    * Set the page header caption and optional margin.
+    *
+    * @access public
+    * @param string $string The header text
+    * @param float  $margin optional head margin in inches.
+    */
+    function setHeader($string,$margin = 0.50)
+    {
+        if (strlen($string) >= 255) {
+            //carp 'Header string must be less than 255 characters';
+            return;
+        }
+        $this->_header      = $string;
+        $this->_margin_head = $margin;
+    }
+
+    /**
+    * Set the page footer caption and optional margin.
+    *
+    * @access public
+    * @param string $string The footer text
+    * @param float  $margin optional foot margin in inches.
+    */
+    function setFooter($string,$margin = 0.50)
+    {
+        if (strlen($string) >= 255) {
+            //carp 'Footer string must be less than 255 characters';
+            return;
+        }
+        $this->_footer      = $string;
+        $this->_margin_foot = $margin;
+    }
+
+    /**
+    * Center the page horinzontally.
+    *
+    * @access public
+    * @param integer $center the optional value for centering. Defaults to 1 (center).
+    */
+    function centerHorizontally($center = 1)
+    {
+        $this->_hcenter = $center;
+    }
+
+    /**
+    * Center the page vertically.
+    *
+    * @access public
+    * @param integer $center the optional value for centering. Defaults to 1 (center).
+    */
+    function centerVertically($center = 1)
+    {
+        $this->_vcenter = $center;
+    }
+
+    /**
+    * Set all the page margins to the same value in inches.
+    *
+    * @access public
+    * @param float $margin The margin to set in inches
+    */
+    function setMargins($margin)
+    {
+        $this->setMarginLeft($margin);
+        $this->setMarginRight($margin);
+        $this->setMarginTop($margin);
+        $this->setMarginBottom($margin);
+    }
+
+    /**
+    * Set the left and right margins to the same value in inches.
+    *
+    * @access public
+    * @param float $margin The margin to set in inches
+    */
+    function setMargins_LR($margin)
+    {
+        $this->setMarginLeft($margin);
+        $this->setMarginRight($margin);
+    }
+
+    /**
+    * Set the top and bottom margins to the same value in inches.
+    *
+    * @access public
+    * @param float $margin The margin to set in inches
+    */
+    function setMargins_TB($margin)
+    {
+        $this->setMarginTop($margin);
+        $this->setMarginBottom($margin);
+    }
+
+    /**
+    * Set the left margin in inches.
+    *
+    * @access public
+    * @param float $margin The margin to set in inches
+    */
+    function setMarginLeft($margin = 0.75)
+    {
+        $this->_margin_left = $margin;
+    }
+
+    /**
+    * Set the right margin in inches.
+    *
+    * @access public
+    * @param float $margin The margin to set in inches
+    */
+    function setMarginRight($margin = 0.75)
+    {
+        $this->_margin_right = $margin;
+    }
+
+    /**
+    * Set the top margin in inches.
+    *
+    * @access public
+    * @param float $margin The margin to set in inches
+    */
+    function setMarginTop($margin = 1.00)
+    {
+        $this->_margin_top = $margin;
+    }
+
+    /**
+    * Set the bottom margin in inches.
+    *
+    * @access public
+    * @param float $margin The margin to set in inches
+    */
+    function setMarginBottom($margin = 1.00)
+    {
+        $this->_margin_bottom = $margin;
+    }
+
+    /**
+    * Set the rows to repeat at the top of each printed page.
+    *
+    * @access public
+    * @param integer $first_row First row to repeat
+    * @param integer $last_row  Last row to repeat. Optional.
+    */
+    function repeatRows($first_row, $last_row = null)
+    {
+        $this->title_rowmin  = $first_row;
+        if (isset($last_row)) { //Second row is optional
+            $this->title_rowmax  = $last_row;
+        } else {
+            $this->title_rowmax  = $first_row;
+        }
+    }
+
+    /**
+    * Set the columns to repeat at the left hand side of each printed page.
+    *
+    * @access public
+    * @param integer $first_col First column to repeat
+    * @param integer $last_col  Last column to repeat. Optional.
+    */
+    function repeatColumns($first_col, $last_col = null)
+    {
+        $this->title_colmin  = $first_col;
+        if (isset($last_col)) { // Second col is optional
+            $this->title_colmax  = $last_col;
+        } else {
+            $this->title_colmax  = $first_col;
+        }
+    }
+
+    /**
+    * Set the area of each worksheet that will be printed.
+    *
+    * @access public
+    * @param integer $first_row First row of the area to print
+    * @param integer $first_col First column of the area to print
+    * @param integer $last_row  Last row of the area to print
+    * @param integer $last_col  Last column of the area to print
+    */
+    function printArea($first_row, $first_col, $last_row, $last_col)
+    {
+        $this->print_rowmin  = $first_row;
+        $this->print_colmin  = $first_col;
+        $this->print_rowmax  = $last_row;
+        $this->print_colmax  = $last_col;
+    }
+
+
+    /**
+    * Set the option to hide gridlines on the printed page.
+    *
+    * @access public
+    */
+    function hideGridlines()
+    {
+        $this->_print_gridlines = 0;
+    }
+
+    /**
+    * Set the option to hide gridlines on the worksheet (as seen on the screen).
+    *
+    * @access public
+    */
+    function hideScreenGridlines()
+    {
+        $this->_screen_gridlines = 0;
+    }
+
+    /**
+    * Set the option to print the row and column headers on the printed page.
+    *
+    * @access public
+    * @param integer $print Whether to print the headers or not. Defaults to 1 (print).
+    */
+    function printRowColHeaders($print = 1)
+    {
+        $this->_print_headers = $print;
+    }
+
+    /**
+    * Set the vertical and horizontal number of pages that will define the maximum area printed.
+    * It doesn't seem to work with OpenOffice.
+    *
+    * @access public
+    * @param  integer $width  Maximun width of printed area in pages
+    * @param  integer $height Maximun heigth of printed area in pages
+    * @see setPrintScale()
+    */
+    function fitToPages($width, $height)
+    {
+        $this->_fit_page      = 1;
+        $this->_fit_width     = $width;
+        $this->_fit_height    = $height;
+    }
+
+    /**
+    * Store the horizontal page breaks on a worksheet (for printing).
+    * The breaks represent the row after which the break is inserted.
+    *
+    * @access public
+    * @param array $breaks Array containing the horizontal page breaks
+    */
+    function setHPagebreaks($breaks)
+    {
+        foreach ($breaks as $break) {
+            array_push($this->_hbreaks, $break);
+        }
+    }
+
+    /**
+    * Store the vertical page breaks on a worksheet (for printing).
+    * The breaks represent the column after which the break is inserted.
+    *
+    * @access public
+    * @param array $breaks Array containing the vertical page breaks
+    */
+    function setVPagebreaks($breaks)
+    {
+        foreach ($breaks as $break) {
+            array_push($this->_vbreaks, $break);
+        }
+    }
+
+
+    /**
+    * Set the worksheet zoom factor.
+    *
+    * @access public
+    * @param integer $scale The zoom factor
+    */
+    function setZoom($scale = 100)
+    {
+        // Confine the scale to Excel's range
+        if ($scale < 10 || $scale > 400) {
+            $scale = 100;
+        }
+
+        $this->_zoom = floor($scale);
+    }
+
+    /**
+    * Set the scale factor for the printed page.
+    * It turns off the "fit to page" option
+    *
+    * @access public
+    * @param integer $scale The optional scale factor. Defaults to 100
+    */
+    function setPrintScale($scale = 100)
+    {
+        // Confine the scale to Excel's range
+        if ($scale < 10 || $scale > 400) {
+            $scale = 100;
+        }
+
+        // Turn off "fit to page" option
+        $this->_fit_page = 0;
+
+        $this->_print_scale = floor($scale);
+    }
+
+    /**
+    * Map to the appropriate write method acording to the token recieved.
+    *
+    * @access public
+    * @param integer $row    The row of the cell we are writing to
+    * @param integer $col    The column of the cell we are writing to
+    * @param mixed   $token  What we are writing
+    * @param mixed   $format The optional format to apply to the cell
+    */
+    function write($row, $col, $token, $format = null)
+    {
+        // Check for a cell reference in A1 notation and substitute row and column
+        /*if ($_[0] =~ /^\D/) {
+            @_ = $this->_substituteCellref(@_);
+    }*/
+
+        if (preg_match("/^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/", $token)) {
+            // Match number
+            return $this->writeNumber($row, $col, $token, $format);
+        } elseif (preg_match("/^[fh]tt?p:\/\//", $token)) {
+            // Match http or ftp URL
+            return $this->writeUrl($row, $col, $token, '', $format);
+        } elseif (preg_match("/^mailto:/", $token)) {
+            // Match mailto:
+            return $this->writeUrl($row, $col, $token, '', $format);
+        } elseif (preg_match("/^(?:in|ex)ternal:/", $token)) {
+            // Match internal or external sheet link
+            return $this->writeUrl($row, $col, $token, '', $format);
+        } elseif (preg_match("/^=/", $token)) {
+            // Match formula
+            return $this->writeFormula($row, $col, $token, $format);
+        } elseif (preg_match("/^@/", $token)) {
+            // Match formula
+            return $this->writeFormula($row, $col, $token, $format);
+        } elseif ($token == '') {
+            // Match blank
+            return $this->writeBlank($row, $col, $format);
+        } else {
+            // Default: match string
+            return $this->writeString($row, $col, $token, $format);
+        }
+    }
+
+    /**
+    * Write an array of values as a row
+    *
+    * @access public
+    * @param integer $row    The row we are writing to
+    * @param integer $col    The first col (leftmost col) we are writing to
+    * @param array   $val    The array of values to write
+    * @param mixed   $format The optional format to apply to the cell
+    * @return mixed PEAR_Error on failure
+    */
+
+    function writeRow($row, $col, $val, $format = null)
+    {
+        $retval = '';
+        if (is_array($val)) {
+            foreach ($val as $v) {
+                if (is_array($v)) {
+                    $this->writeCol($row, $col, $v, $format);
+                } else {
+                    $this->write($row, $col, $v, $format);
+                }
+                $col++;
+            }
+        } else {
+            die('$val needs to be an array');
+        }
+        return($retval);
+    }
+
+    /**
+    * Write an array of values as a column
+    *
+    * @access public
+    * @param integer $row    The first row (uppermost row) we are writing to
+    * @param integer $col    The col we are writing to
+    * @param array   $val    The array of values to write
+    * @param mixed   $format The optional format to apply to the cell
+    * @return mixed PEAR_Error on failure
+    */
+
+    function writeCol($row, $col, $val, $format = null)
+    {
+        $retval = '';
+        if (is_array($val)) {
+            foreach ($val as $v) {
+                $this->write($row, $col, $v, $format);
+                $row++;
+            }
+        } else {
+            die('$val needs to be an array');
+        }
+        return($retval);
+    }
+
+    /**
+    * Returns an index to the XF record in the workbook
+    *
+    * @access private
+    * @param mixed &$format The optional XF format
+    * @return integer The XF record index
+    */
+    function _XF(&$format)
+    {
+        if ($format) {
+            return($format->getXfIndex());
+        } else {
+            return(0x0F);
+        }
+    }
+
+
+    /******************************************************************************
+    *******************************************************************************
+    *
+    * Internal methods
+    */
+
+
+    /**
+    * Store Worksheet data in memory using the parent's class append() or to a
+    * temporary file, the default.
+    *
+    * @access private
+    * @param string $data The binary data to append
+    */
+    function _append($data)
+    {
+        if ($this->_using_tmpfile) {
+            // Add CONTINUE records if necessary
+            if (strlen($data) > $this->_limit) {
+                $data = $this->_addContinue($data);
+            }
+            fwrite($this->_filehandle, $data);
+            $this->_datasize += strlen($data);
+        } else {
+            parent::_append($data);
+        }
+    }
+
+    /**
+    * Substitute an Excel cell reference in A1 notation for  zero based row and
+    * column values in an argument list.
+    *
+    * Ex: ("A4", "Hello") is converted to (3, 0, "Hello").
+    *
+    * @access private
+    * @param string $cell The cell reference. Or range of cells.
+    * @return array
+    */
+    function _substituteCellref($cell)
+    {
+        $cell = strtoupper($cell);
+
+        // Convert a column range: 'A:A' or 'B:G'
+        if (preg_match("/([A-I]?[A-Z]):([A-I]?[A-Z])/", $cell, $match)) {
+            list($no_use, $col1) =  $this->_cellToRowcol($match[1] .'1'); // Add a dummy row
+            list($no_use, $col2) =  $this->_cellToRowcol($match[2] .'1'); // Add a dummy row
+            return(array($col1, $col2));
+        }
+
+        // Convert a cell range: 'A1:B7'
+        if (preg_match("/\$?([A-I]?[A-Z]\$?\d+):\$?([A-I]?[A-Z]\$?\d+)/", $cell, $match)) {
+            list($row1, $col1) =  $this->_cellToRowcol($match[1]);
+            list($row2, $col2) =  $this->_cellToRowcol($match[2]);
+            return(array($row1, $col1, $row2, $col2));
+        }
+
+        // Convert a cell reference: 'A1' or 'AD2000'
+        if (preg_match("/\$?([A-I]?[A-Z]\$?\d+)/", $cell)) {
+            list($row1, $col1) =  $this->_cellToRowcol($match[1]);
+            return(array($row1, $col1));
+        }
+
+        // TODO use real error codes
+        die("Unknown cell reference $cell");
+    }
+
+    /**
+    * Convert an Excel cell reference in A1 notation to a zero based row and column
+    * reference; converts C1 to (0, 2).
+    *
+    * @access private
+    * @param string $cell The cell reference.
+    * @return array containing (row, column)
+    */
+    function _cellToRowcol($cell)
+    {
+        preg_match("/\$?([A-I]?[A-Z])\$?(\d+)/",$cell,$match);
+        $col     = $match[1];
+        $row     = $match[2];
+
+        // Convert base26 column string to number
+        $chars = split('', $col);
+        $expn  = 0;
+        $col   = 0;
+
+        while ($chars) {
+            $char = array_pop($chars);        // LS char first
+            $col += (ord($char) -ord('A') +1) * pow(26,$expn);
+            $expn++;
+        }
+
+        // Convert 1-index to zero-index
+        $row--;
+        $col--;
+
+        return(array($row, $col));
+    }
+
+    /**
+    * Based on the algorithm provided by Daniel Rentz of OpenOffice.
+    *
+    * @access private
+    * @param string $plaintext The password to be encoded in plaintext.
+    * @return string The encoded password
+    */
+    function _encodePassword($plaintext)
+    {
+        $password = 0x0000;
+        $i        = 1;       // char position
+
+        // split the plain text password in its component characters
+        $chars = preg_split('//', $plaintext, -1, PREG_SPLIT_NO_EMPTY);
+        foreach ($chars as $char) {
+            $value        = ord($char) << $i;   // shifted ASCII value
+            $rotated_bits = $value >> 15;       // rotated bits beyond bit 15
+            $value       &= 0x7fff;             // first 15 bits
+            $password    ^= ($value | $rotated_bits);
+            $i++;
+        }
+
+        $password ^= strlen($plaintext);
+        $password ^= 0xCE4B;
+
+        return($password);
+    }
+
+    /**
+    * This method sets the properties for outlining and grouping. The defaults
+    * correspond to Excel's defaults.
+    *
+    * @param bool $visible
+    * @param bool $symbols_below
+    * @param bool $symbols_right
+    * @param bool $auto_style
+    */
+    function setOutline($visible = true, $symbols_below = true, $symbols_right = true, $auto_style = false)
+    {
+        $this->_outline_on    = $visible;
+        $this->_outline_below = $symbols_below;
+        $this->_outline_right = $symbols_right;
+        $this->_outline_style = $auto_style;
+
+        // Ensure this is a boolean vale for Window2
+        if ($this->_outline_on) {
+            $this->_outline_on = 1;
+        }
+     }
+
+    /******************************************************************************
+    *******************************************************************************
+    *
+    * BIFF RECORDS
+    */
+
+
+    /**
+    * Write a double to the specified row and column (zero indexed).
+    * An integer can be written as a double. Excel will display an
+    * integer. $format is optional.
+    *
+    * Returns  0 : normal termination
+    *         -2 : row or column out of range
+    *
+    * @access public
+    * @param integer $row    Zero indexed row
+    * @param integer $col    Zero indexed column
+    * @param float   $num    The number to write
+    * @param mixed   $format The optional XF format
+    * @return integer
+    */
+    function writeNumber($row, $col, $num, $format = null)
+    {
+        $record    = 0x0203;                 // Record identifier
+        $length    = 0x000E;                 // Number of bytes to follow
+
+        $xf        = $this->_XF($format);    // The cell format
+
+        // Check that row and col are valid and store max and min values
+        if ($row >= $this->_xls_rowmax) {
+            return(-2);
+        }
+        if ($col >= $this->_xls_colmax) {
+            return(-2);
+        }
+        if ($row <  $this->_dim_rowmin)  {
+            $this->_dim_rowmin = $row;
+        }
+        if ($row >  $this->_dim_rowmax)  {
+            $this->_dim_rowmax = $row;
+        }
+        if ($col <  $this->_dim_colmin)  {
+            $this->_dim_colmin = $col;
+        }
+        if ($col >  $this->_dim_colmax)  {
+            $this->_dim_colmax = $col;
+        }
+
+        $header    = pack("vv",  $record, $length);
+        $data      = pack("vvv", $row, $col, $xf);
+        $xl_double = pack("d",   $num);
+        if ($this->_byte_order) { // if it's Big Endian
+            $xl_double = strrev($xl_double);
+        }
+
+        $this->_append($header.$data.$xl_double);
+        return(0);
+    }
+
+    /**
+    * Write a string to the specified row and column (zero indexed).
+    * NOTE: there is an Excel 5 defined limit of 255 characters.
+    * $format is optional.
+    * Returns  0 : normal termination
+    *         -2 : row or column out of range
+    *         -3 : long string truncated to 255 chars
+    *
+    * @access public
+    * @param integer $row    Zero indexed row
+    * @param integer $col    Zero indexed column
+    * @param string  $str    The string to write
+    * @param mixed   $format The XF format for the cell
+    * @return integer
+    */
+    function writeString($row, $col, $str, $format = null)
+    {
+        if ($this->_BIFF_version == 0x0600) {
+            return $this->writeStringBIFF8($row, $col, $str, $format);
+        }
+        $strlen    = strlen($str);
+        $record    = 0x0204;                   // Record identifier
+        $length    = 0x0008 + $strlen;         // Bytes to follow
+        $xf        = $this->_XF($format);      // The cell format
+
+        $str_error = 0;
+
+        // Check that row and col are valid and store max and min values
+        if ($row >= $this->_xls_rowmax) {
+            return(-2);
+        }
+        if ($col >= $this->_xls_colmax) {
+            return(-2);
+        }
+        if ($row <  $this->_dim_rowmin) {
+            $this->_dim_rowmin = $row;
+        }
+        if ($row >  $this->_dim_rowmax) {
+            $this->_dim_rowmax = $row;
+        }
+        if ($col <  $this->_dim_colmin) {
+            $this->_dim_colmin = $col;
+        }
+        if ($col >  $this->_dim_colmax) {
+            $this->_dim_colmax = $col;
+        }
+
+        if ($strlen > $this->_xls_strmax) { // LABEL must be < 255 chars
+            $str       = substr($str, 0, $this->_xls_strmax);
+            $length    = 0x0008 + $this->_xls_strmax;
+            $strlen    = $this->_xls_strmax;
+            $str_error = -3;
+        }
+
+        $header    = pack("vv",   $record, $length);
+        $data      = pack("vvvv", $row, $col, $xf, $strlen);
+        $this->_append($header . $data . $str);
+        return($str_error);
+    }
+
+    /**
+    * Sets Input Encoding for writing strings
+    *
+    * @access public
+    * @param string $encoding The encoding. Ex: 'UTF-16LE', 'utf-8', 'ISO-859-7'
+    */
+    function setInputEncoding($encoding)
+    {
+       global $encoding_string;
+         if ($encoding != 'UTF-16LE' && !function_exists('iconv')) {
+             die("Using an input encoding other than UTF-16LE requires PHP support for iconv");
+         }
+         $this->_input_encoding = $encoding_string = $encoding;
+    }
+
+    /** added 2009-03-05 by Joe Hunt, FA for arabic languages */
+    function setRTL()
+    {
+       $this->_rtl = 1;
+    }  
+
+    /**
+    * Write a string to the specified row and column (zero indexed).
+    * This is the BIFF8 version (no 255 chars limit).
+    * $format is optional.
+    * Returns  0 : normal termination
+    *         -2 : row or column out of range
+    *         -3 : long string truncated to 255 chars
+    *
+    * @access public
+    * @param integer $row    Zero indexed row
+    * @param integer $col    Zero indexed column
+    * @param string  $str    The string to write
+    * @param mixed   $format The XF format for the cell
+    * @return integer
+    */
+    function writeStringBIFF8($row, $col, $str, $format = null)
+    {
+        if ($this->_input_encoding == 'UTF-16LE')
+        {
+            $strlen = function_exists('mb_strlen') ? mb_strlen($str, 'UTF-16LE') : (strlen($str) / 2);
+            $encoding  = 0x1;
+        }
+        elseif ($this->_input_encoding != '')
+        {
+            $str = iconv($this->_input_encoding, 'UTF-16LE', $str);
+            $strlen = function_exists('mb_strlen') ? mb_strlen($str, 'UTF-16LE') : (strlen($str) / 2);
+            $encoding  = 0x1;
+        }
+        else
+        {
+            $strlen    = strlen($str);
+            $encoding  = 0x0;
+        }
+        $record    = 0x00FD;                   // Record identifier
+        $length    = 0x000A;                   // Bytes to follow
+        $xf        = $this->_XF($format);      // The cell format
+
+        $str_error = 0;
+
+        // Check that row and col are valid and store max and min values
+        if ($this->_checkRowCol($row, $col) == false) {
+            return -2;
+        }
+
+        $str = pack('vC', $strlen, $encoding).$str;
+
+        /* check if string is already present */
+        if (!isset($this->_str_table[$str])) {
+            $this->_str_table[$str] = $this->_str_unique++;
+        }
+        $this->_str_total++;
+
+        $header    = pack('vv',   $record, $length);
+        $data      = pack('vvvV', $row, $col, $xf, $this->_str_table[$str]);
+        $this->_append($header.$data);
+        return $str_error;
+    }
+
+    /**
+    * Check row and col before writing to a cell, and update the sheet's
+    * dimensions accordingly
+    *
+    * @access private
+    * @param integer $row    Zero indexed row
+    * @param integer $col    Zero indexed column
+    * @return boolean true for success, false if row and/or col are grester
+    *                 then maximums allowed.
+    */
+    function _checkRowCol($row, $col)
+    {
+        if ($row >= $this->_xls_rowmax) {
+            return false;
+        }
+        if ($col >= $this->_xls_colmax) {
+            return false;
+        }
+        if ($row <  $this->_dim_rowmin) {
+            $this->_dim_rowmin = $row;
+        }
+        if ($row >  $this->_dim_rowmax) {
+            $this->_dim_rowmax = $row;
+        }
+        if ($col <  $this->_dim_colmin) {
+            $this->_dim_colmin = $col;
+        }
+        if ($col >  $this->_dim_colmax) {
+            $this->_dim_colmax = $col;
+        }
+        return true;
+    }
+
+    /**
+    * Writes a note associated with the cell given by the row and column.
+    * NOTE records don't have a length limit.
+    *
+    * @access public
+    * @param integer $row    Zero indexed row
+    * @param integer $col    Zero indexed column
+    * @param string  $note   The note to write
+    */
+    function writeNote($row, $col, $note)
+    {
+        $note_length    = strlen($note);
+        $record         = 0x001C;                // Record identifier
+        $max_length     = 2048;                  // Maximun length for a NOTE record
+        //$length      = 0x0006 + $note_length;    // Bytes to follow
+
+        // Check that row and col are valid and store max and min values
+        if ($row >= $this->_xls_rowmax) {
+            return(-2);
+        }
+        if ($col >= $this->_xls_colmax) {
+            return(-2);
+        }
+        if ($row <  $this->_dim_rowmin) {
+            $this->_dim_rowmin = $row;
+        }
+        if ($row >  $this->_dim_rowmax) {
+            $this->_dim_rowmax = $row;
+        }
+        if ($col <  $this->_dim_colmin) {
+            $this->_dim_colmin = $col;
+        }
+        if ($col >  $this->_dim_colmax) {
+            $this->_dim_colmax = $col;
+        }
+
+        // Length for this record is no more than 2048 + 6
+        $length    = 0x0006 + min($note_length, 2048);
+        $header    = pack("vv",   $record, $length);
+        $data      = pack("vvv", $row, $col, $note_length);
+        $this->_append($header . $data . substr($note, 0, 2048));
+
+        for ($i = $max_length; $i < $note_length; $i += $max_length) {
+            $chunk  = substr($note, $i, $max_length);
+            $length = 0x0006 + strlen($chunk);
+            $header = pack("vv",   $record, $length);
+            $data   = pack("vvv", -1, 0, strlen($chunk));
+            $this->_append($header.$data.$chunk);
+        }
+        return(0);
+    }
+
+    /**
+    * Write a blank cell to the specified row and column (zero indexed).
+    * A blank cell is used to specify formatting without adding a string
+    * or a number.
+    *
+    * A blank cell without a format serves no purpose. Therefore, we don't write
+    * a BLANK record unless a format is specified.
+    *
+    * Returns  0 : normal termination (including no format)
+    *         -1 : insufficient number of arguments
+    *         -2 : row or column out of range
+    *
+    * @access public
+    * @param integer $row    Zero indexed row
+    * @param integer $col    Zero indexed column
+    * @param mixed   $format The XF format
+    */
+    function writeBlank($row, $col, $format)
+    {
+        // Don't write a blank cell unless it has a format
+        if (!$format) {
+            return(0);
+        }
+
+        $record    = 0x0201;                 // Record identifier
+        $length    = 0x0006;                 // Number of bytes to follow
+        $xf        = $this->_XF($format);    // The cell format
+
+        // Check that row and col are valid and store max and min values
+        if ($row >= $this->_xls_rowmax) {
+            return(-2);
+        }
+        if ($col >= $this->_xls_colmax) {
+            return(-2);
+        }
+        if ($row <  $this->_dim_rowmin) {
+            $this->_dim_rowmin = $row;
+        }
+        if ($row >  $this->_dim_rowmax) {
+            $this->_dim_rowmax = $row;
+        }
+        if ($col <  $this->_dim_colmin) {
+            $this->_dim_colmin = $col;
+        }
+        if ($col >  $this->_dim_colmax) {
+            $this->_dim_colmax = $col;
+        }
+
+        $header    = pack("vv",  $record, $length);
+        $data      = pack("vvv", $row, $col, $xf);
+        $this->_append($header . $data);
+        return 0;
+    }
+
+    /**
+    * Write a formula to the specified row and column (zero indexed).
+    * The textual representation of the formula is passed to the parser in
+    * Parser.php which returns a packed binary string.
+    *
+    * Returns  0 : normal termination
+    *         -1 : formula errors (bad formula)
+    *         -2 : row or column out of range
+    *
+    * @access public
+    * @param integer $row     Zero indexed row
+    * @param integer $col     Zero indexed column
+    * @param string  $formula The formula text string
+    * @param mixed   $format  The optional XF format
+    * @return integer
+    */
+    function writeFormula($row, $col, $formula, $format = null)
+    {
+        $record    = 0x0006;     // Record identifier
+
+        // Excel normally stores the last calculated value of the formula in $num.
+        // Clearly we are not in a position to calculate this a priori. Instead
+        // we set $num to zero and set the option flags in $grbit to ensure
+        // automatic calculation of the formula when the file is opened.
+        //
+        $xf        = $this->_XF($format); // The cell format
+        $num       = 0x00;                // Current value of formula
+        $grbit     = 0x03;                // Option flags
+        $unknown   = 0x0000;              // Must be zero
+
+
+        // Check that row and col are valid and store max and min values
+        if ($this->_checkRowCol($row, $col) == false) {
+            return -2;
+        }
+
+        // Strip the '=' or '@' sign at the beginning of the formula string
+        if (preg_match("/^=/", $formula)) {
+            $formula = preg_replace("/(^=)/", "", $formula);
+        } elseif (preg_match("/^@/", $formula)) {
+            $formula = preg_replace("/(^@)/", "", $formula);
+        } else {
+            // Error handling
+            $this->writeString($row, $col, 'Unrecognised character for formula');
+            return -1;
+        }
+
+        // Parse the formula using the parser in Parser.php
+        $this->_parser->parse($formula);
+        $formula = $this->_parser->toReversePolish();
+        $formlen    = strlen($formula);    // Length of the binary string
+        $length     = 0x16 + $formlen;     // Length of the record data
+
+        $header    = pack("vv",      $record, $length);
+        $data      = pack("vvvdvVv", $row, $col, $xf, $num,
+                                     $grbit, $unknown, $formlen);
+
+        $this->_append($header . $data . $formula);
+        return 0;
+    }
+
+    /**
+    * Write a hyperlink.
+    * This is comprised of two elements: the visible label and
+    * the invisible link. The visible label is the same as the link unless an
+    * alternative string is specified. The label is written using the
+    * writeString() method. Therefore the 255 characters string limit applies.
+    * $string and $format are optional.
+    *
+    * The hyperlink can be to a http, ftp, mail, internal sheet (not yet), or external
+    * directory url.
+    *
+    * Returns  0 : normal termination
+    *         -2 : row or column out of range
+    *         -3 : long string truncated to 255 chars
+    *
+    * @access public
+    * @param integer $row    Row
+    * @param integer $col    Column
+    * @param string  $url    URL string
+    * @param string  $string Alternative label
+    * @param mixed   $format The cell format
+    * @return integer
+    */
+    function writeUrl($row, $col, $url, $string = '', $format = null)
+    {
+        // Add start row and col to arg list
+        return($this->_writeUrlRange($row, $col, $row, $col, $url, $string, $format));
+    }
+
+    /**
+    * This is the more general form of writeUrl(). It allows a hyperlink to be
+    * written to a range of cells. This function also decides the type of hyperlink
+    * to be written. These are either, Web (http, ftp, mailto), Internal
+    * (Sheet1!A1) or external ('c:\temp\foo.xls#Sheet1!A1').
+    *
+    * @access private
+    * @see writeUrl()
+    * @param integer $row1   Start row
+    * @param integer $col1   Start column
+    * @param integer $row2   End row
+    * @param integer $col2   End column
+    * @param string  $url    URL string
+    * @param string  $string Alternative label
+    * @param mixed   $format The cell format
+    * @return integer
+    */
+
+    function _writeUrlRange($row1, $col1, $row2, $col2, $url, $string = '', $format = null)
+    {
+
+        // Check for internal/external sheet links or default to web link
+        if (preg_match('[^internal:]', $url)) {
+            return($this->_writeUrlInternal($row1, $col1, $row2, $col2, $url, $string, $format));
+        }
+        if (preg_match('[^external:]', $url)) {
+            return($this->_writeUrlExternal($row1, $col1, $row2, $col2, $url, $string, $format));
+        }
+        return($this->_writeUrlWeb($row1, $col1, $row2, $col2, $url, $string, $format));
+    }
+
+
+    /**
+    * Used to write http, ftp and mailto hyperlinks.
+    * The link type ($options) is 0x03 is the same as absolute dir ref without
+    * sheet. However it is differentiated by the $unknown2 data stream.
+    *
+    * @access private
+    * @see writeUrl()
+    * @param integer $row1   Start row
+    * @param integer $col1   Start column
+    * @param integer $row2   End row
+    * @param integer $col2   End column
+    * @param string  $url    URL string
+    * @param string  $str    Alternative label
+    * @param mixed   $format The cell format
+    * @return integer
+    */
+    function _writeUrlWeb($row1, $col1, $row2, $col2, $url, $str, $format = null)
+    {
+        $record      = 0x01B8;                       // Record identifier
+        $length      = 0x00000;                      // Bytes to follow
+
+        if (!$format) {
+            $format = $this->_url_format;
+        }
+
+        // Write the visible label using the writeString() method.
+        if ($str == '') {
+            $str = $url;
+        }
+        $str_error = $this->writeString($row1, $col1, $str, $format);
+        if (($str_error == -2) || ($str_error == -3)) {
+            return $str_error;
+        }
+
+        // Pack the undocumented parts of the hyperlink stream
+        $unknown1    = pack("H*", "D0C9EA79F9BACE118C8200AA004BA90B02000000");
+        $unknown2    = pack("H*", "E0C9EA79F9BACE118C8200AA004BA90B");
+
+        // Pack the option flags
+        $options     = pack("V", 0x03);
+
+        // Convert URL to a null terminated wchar string
+        $url         = join("\0", preg_split("''", $url, -1, PREG_SPLIT_NO_EMPTY));
+        $url         = $url . "\0\0\0";
+
+        // Pack the length of the URL
+        $url_len     = pack("V", strlen($url));
+
+        // Calculate the data length
+        $length      = 0x34 + strlen($url);
+
+        // Pack the header data
+        $header      = pack("vv",   $record, $length);
+        $data        = pack("vvvv", $row1, $row2, $col1, $col2);
+
+        // Write the packed data
+        $this->_append($header . $data .
+                       $unknown1 . $options .
+                       $unknown2 . $url_len . $url);
+        return($str_error);
+    }
+
+    /**
+    * Used to write internal reference hyperlinks such as "Sheet1!A1".
+    *
+    * @access private
+    * @see writeUrl()
+    * @param integer $row1   Start row
+    * @param integer $col1   Start column
+    * @param integer $row2   End row
+    * @param integer $col2   End column
+    * @param string  $url    URL string
+    * @param string  $str    Alternative label
+    * @param mixed   $format The cell format
+    * @return integer
+    */
+    function _writeUrlInternal($row1, $col1, $row2, $col2, $url, $str, $format = null)
+    {
+        $record      = 0x01B8;                       // Record identifier
+        $length      = 0x00000;                      // Bytes to follow
+
+        if (!$format) {
+            $format = $this->_url_format;
+        }
+
+        // Strip URL type
+        $url = preg_replace('/^internal:/', '', $url);
+
+        // Write the visible label
+        if ($str == '') {
+            $str = $url;
+        }
+        $str_error = $this->writeString($row1, $col1, $str, $format);
+        if (($str_error == -2) || ($str_error == -3)) {
+            return $str_error;
+        }
+
+        // Pack the undocumented parts of the hyperlink stream
+        $unknown1    = pack("H*", "D0C9EA79F9BACE118C8200AA004BA90B02000000");
+
+        // Pack the option flags
+        $options     = pack("V", 0x08);
+
+        // Convert the URL type and to a null terminated wchar string
+        $url         = join("\0", preg_split("''", $url, -1, PREG_SPLIT_NO_EMPTY));
+        $url         = $url . "\0\0\0";
+
+        // Pack the length of the URL as chars (not wchars)
+        $url_len     = pack("V", floor(strlen($url)/2));
+
+        // Calculate the data length
+        $length      = 0x24 + strlen($url);
+
+        // Pack the header data
+        $header      = pack("vv",   $record, $length);
+        $data        = pack("vvvv", $row1, $row2, $col1, $col2);
+
+        // Write the packed data
+        $this->_append($header . $data .
+                       $unknown1 . $options .
+                       $url_len . $url);
+        return($str_error);
+    }
+
+    /**
+    * Write links to external directory names such as 'c:\foo.xls',
+    * c:\foo.xls#Sheet1!A1', '../../foo.xls'. and '../../foo.xls#Sheet1!A1'.
+    *
+    * Note: Excel writes some relative links with the $dir_long string. We ignore
+    * these cases for the sake of simpler code.
+    *
+    * @access private
+    * @see writeUrl()
+    * @param integer $row1   Start row
+    * @param integer $col1   Start column
+    * @param integer $row2   End row
+    * @param integer $col2   End column
+    * @param string  $url    URL string
+    * @param string  $str    Alternative label
+    * @param mixed   $format The cell format
+    * @return integer
+    */
+    function _writeUrlExternal($row1, $col1, $row2, $col2, $url, $str, $format = null)
+    {
+        // Network drives are different. We will handle them separately
+        // MS/Novell network drives and shares start with \\
+        if (preg_match('[^external:\\\\]', $url)) {
+            return; //($this->_writeUrlExternal_net($row1, $col1, $row2, $col2, $url, $str, $format));
+        }
+    
+        $record      = 0x01B8;                       // Record identifier
+        $length      = 0x00000;                      // Bytes to follow
+    
+        if (!$format) {
+            $format = $this->_url_format;
+        }
+    
+        // Strip URL type and change Unix dir separator to Dos style (if needed)
+        //
+        $url = preg_replace('/^external:/', '', $url);
+        $url = preg_replace('/\//', "\\", $url);
+    
+        // Write the visible label
+        if ($str == '') {
+            $str = preg_replace('/\#/', ' - ', $url);
+        }
+        $str_error = $this->writeString($row1, $col1, $str, $format);
+        if (($str_error == -2) or ($str_error == -3)) {
+            return $str_error;
+        }
+    
+        // Determine if the link is relative or absolute:
+        //   relative if link contains no dir separator, "somefile.xls"
+        //   relative if link starts with up-dir, "..\..\somefile.xls"
+        //   otherwise, absolute
+        
+        $absolute    = 0x02; // Bit mask
+        if (!preg_match("/\\\/", $url)) {
+            $absolute    = 0x00;
+        }
+        if (preg_match("/^\.\.\\\/", $url)) {
+            $absolute    = 0x00;
+        }
+        $link_type               = 0x01 | $absolute;
+    
+        // Determine if the link contains a sheet reference and change some of the
+        // parameters accordingly.
+        // Split the dir name and sheet name (if it exists)
+        /*if (preg_match("/\#/", $url)) {
+            list($dir_long, $sheet) = split("\#", $url);
+        } else {
+            $dir_long = $url;
+        }
+    
+        if (isset($sheet)) {
+            $link_type |= 0x08;
+            $sheet_len  = pack("V", strlen($sheet) + 0x01);
+            $sheet      = join("\0", split('', $sheet));
+            $sheet     .= "\0\0\0";
+        } else {
+            $sheet_len   = '';
+            $sheet       = '';
+        }*/
+        $dir_long = $url;
+        if (preg_match("/\#/", $url)) {
+            $link_type |= 0x08;
+        }
+
+
+    
+        // Pack the link type
+        $link_type   = pack("V", $link_type);
+    
+        // Calculate the up-level dir count e.g.. (..\..\..\ == 3)
+        $up_count    = preg_match_all("/\.\.\\\/", $dir_long, $useless);
+        $up_count    = pack("v", $up_count);
+    
+        // Store the short dos dir name (null terminated)
+        $dir_short   = preg_replace("/\.\.\\\/", '', $dir_long) . "\0";
+    
+        // Store the long dir name as a wchar string (non-null terminated)
+        //$dir_long       = join("\0", split('', $dir_long));
+        $dir_long       = $dir_long . "\0";
+    
+        // Pack the lengths of the dir strings
+        $dir_short_len = pack("V", strlen($dir_short)      );
+        $dir_long_len  = pack("V", strlen($dir_long)       );
+        $stream_len    = pack("V", 0);//strlen($dir_long) + 0x06);
+    
+        // Pack the undocumented parts of the hyperlink stream
+        $unknown1 = pack("H*",'D0C9EA79F9BACE118C8200AA004BA90B02000000'       );
+        $unknown2 = pack("H*",'0303000000000000C000000000000046'               );
+        $unknown3 = pack("H*",'FFFFADDE000000000000000000000000000000000000000');
+        $unknown4 = pack("v",  0x03                                            );
+    
+        // Pack the main data stream
+        $data        = pack("vvvv", $row1, $row2, $col1, $col2) .
+                          $unknown1     .
+                          $link_type    .
+                          $unknown2     .
+                          $up_count     .
+                          $dir_short_len.
+                          $dir_short    .
+                          $unknown3     .
+                          $stream_len   .
+                          $dir_long_len .
+                          $unknown4     .
+                          $dir_long     .
+                          $sheet_len    .
+                          $sheet        ;
+    
+        // Pack the header data
+        $length   = strlen($data);
+        $header   = pack("vv", $record, $length);
+    
+        // Write the packed data
+        $this->_append($header. $data);
+        return($str_error);
+    }
+
+
+    /**
+    * This method is used to set the height and format for a row.
+    *
+    * @access public
+    * @param integer $row    The row to set
+    * @param integer $height Height we are giving to the row.
+    *                        Use null to set XF without setting height
+    * @param mixed   $format XF format we are giving to the row
+    * @param bool    $hidden The optional hidden attribute
+    * @param integer $level  The optional outline level for row, in range [0,7]
+    */
+    function setRow($row, $height, $format = null, $hidden = false, $level = 0)
+    {
+        $record      = 0x0208;               // Record identifier
+        $length      = 0x0010;               // Number of bytes to follow
+
+        $colMic      = 0x0000;               // First defined column
+        $colMac      = 0x0000;               // Last defined column
+        $irwMac      = 0x0000;               // Used by Excel to optimise loading
+        $reserved    = 0x0000;               // Reserved
+        $grbit       = 0x0000;               // Option flags
+        $ixfe        = $this->_XF($format);  // XF index
+
+        // set _row_sizes so _sizeRow() can use it
+        $this->_row_sizes[$row] = $height;
+
+        // Use setRow($row, null, $XF) to set XF format without setting height
+        if ($height != null) {
+            $miyRw = $height * 20;  // row height
+        } else {
+            $miyRw = 0xff;          // default row height is 256
+        }
+
+        $level = max(0, min($level, 7));  // level should be between 0 and 7
+        $this->_outline_row_level = max($level, $this->_outline_row_level);
+
+
+        // Set the options flags. fUnsynced is used to show that the font and row
+        // heights are not compatible. This is usually the case for WriteExcel.
+        // The collapsed flag 0x10 doesn't seem to be used to indicate that a row
+        // is collapsed. Instead it is used to indicate that the previous row is
+        // collapsed. The zero height flag, 0x20, is used to collapse a row.
+
+        $grbit |= $level;
+        if ($hidden) {
+            $grbit |= 0x0020;
+        }
+        $grbit |= 0x0040; // fUnsynced
+        if ($format) {
+            $grbit |= 0x0080;
+        }
+        $grbit |= 0x0100;
+
+        $header   = pack("vv",       $record, $length);
+        $data     = pack("vvvvvvvv", $row, $colMic, $colMac, $miyRw,
+                                     $irwMac,$reserved, $grbit, $ixfe);
+        $this->_append($header.$data);
+    }
+
+    /**
+    * Writes Excel DIMENSIONS to define the area in which there is data.
+    *
+    * @access private
+    */
+    function _storeDimensions()
+    {
+        $record    = 0x0200;                 // Record identifier
+        $row_min   = $this->_dim_rowmin;     // First row
+        $row_max   = $this->_dim_rowmax + 1; // Last row plus 1
+        $col_min   = $this->_dim_colmin;     // First column
+        $col_max   = $this->_dim_colmax + 1; // Last column plus 1
+        $reserved  = 0x0000;                 // Reserved by Excel
+
+        if ($this->_BIFF_version == 0x0500) {
+            $length    = 0x000A;               // Number of bytes to follow
+            $data      = pack("vvvvv", $row_min, $row_max,
+                                       $col_min, $col_max, $reserved);
+        } elseif ($this->_BIFF_version == 0x0600) {
+            $length    = 0x000E;
+            $data      = pack("VVvvv", $row_min, $row_max,
+                                       $col_min, $col_max, $reserved);
+        }
+        $header = pack("vv", $record, $length);
+        $this->_prepend($header.$data);
+    }
+
+    /**
+    * Write BIFF record Window2.
+    *
+    * @access private
+    */
+    function _storeWindow2()
+    {
+        $record         = 0x023E;     // Record identifier
+        if ($this->_BIFF_version == 0x0500) {
+            $length         = 0x000A;     // Number of bytes to follow
+        } elseif ($this->_BIFF_version == 0x0600) {
+            $length         = 0x0012;
+        }
+
+        $grbit          = 0x00B6;     // Option flags
+        $rwTop          = 0x0000;     // Top row visible in window
+        $colLeft        = 0x0000;     // Leftmost column visible in window
+
+
+        // The options flags that comprise $grbit
+        $fDspFmla       = 0;                     // 0 - bit
+        $fDspGrid       = $this->_screen_gridlines; // 1
+        $fDspRwCol      = 1;                     // 2
+        $fFrozen        = $this->_frozen;        // 3
+        $fDspZeros      = 1;                     // 4
+        $fDefaultHdr    = 1;                     // 5
+        $fArabic        = $this->_rtl;           // 6
+        $fDspGuts       = $this->_outline_on;    // 7
+        $fFrozenNoSplit = 0;                     // 0 - bit
+        $fSelected      = $this->selected;       // 1
+        $fPaged         = 1;                     // 2
+
+        $grbit             = $fDspFmla;
+        $grbit            |= $fDspGrid       << 1;
+        $grbit            |= $fDspRwCol      << 2;
+        $grbit            |= $fFrozen        << 3;
+        $grbit            |= $fDspZeros      << 4;
+        $grbit            |= $fDefaultHdr    << 5;
+        $grbit            |= $fArabic        << 6;
+        $grbit            |= $fDspGuts       << 7;
+        $grbit            |= $fFrozenNoSplit << 8;
+        $grbit            |= $fSelected      << 9;
+        $grbit            |= $fPaged         << 10;
+
+        $header  = pack("vv",   $record, $length);
+        $data    = pack("vvv", $grbit, $rwTop, $colLeft);
+        // FIXME !!!
+        if ($this->_BIFF_version == 0x0500) {
+            $rgbHdr         = 0x00000000; // Row/column heading and gridline color
+            $data .= pack("V", $rgbHdr);
+        } elseif ($this->_BIFF_version == 0x0600) {
+            $rgbHdr       = 0x0040; // Row/column heading and gridline color index
+            $zoom_factor_page_break = 0x0000;
+            $zoom_factor_normal     = 0x0000;
+            $data .= pack("vvvvV", $rgbHdr, 0x0000, $zoom_factor_page_break, $zoom_factor_normal, 0x00000000);
+        }
+        $this->_append($header.$data);
+    }
+
+    /**
+    * Write BIFF record DEFCOLWIDTH if COLINFO records are in use.
+    *
+    * @access private
+    */
+    function _storeDefcol()
+    {
+        $record   = 0x0055;      // Record identifier
+        $length   = 0x0002;      // Number of bytes to follow
+        $colwidth = 0x0008;      // Default column width
+
+        $header   = pack("vv", $record, $length);
+        $data     = pack("v",  $colwidth);
+        $this->_prepend($header . $data);
+    }
+
+    /**
+    * Write BIFF record COLINFO to define column widths
+    *
+    * Note: The SDK says the record length is 0x0B but Excel writes a 0x0C
+    * length record.
+    *
+    * @access private
+    * @param array $col_array This is the only parameter received and is composed of the following:
+    *                0 => First formatted column,
+    *                1 => Last formatted column,
+    *                2 => Col width (8.43 is Excel default),
+    *                3 => The optional XF format of the column,
+    *                4 => Option flags.
+    *                5 => Optional outline level
+    */
+    function _storeColinfo($col_array)
+    {
+        if (isset($col_array[0])) {
+            $colFirst = $col_array[0];
+        }
+        if (isset($col_array[1])) {
+            $colLast = $col_array[1];
+        }
+        if (isset($col_array[2])) {
+            $coldx = $col_array[2];
+        } else {
+            $coldx = 8.43;
+        }
+        if (isset($col_array[3])) {
+            $format = $col_array[3];
+        } else {
+            $format = 0;
+        }
+        if (isset($col_array[4])) {
+            $grbit = $col_array[4];
+        } else {
+            $grbit = 0;
+        }
+        if (isset($col_array[5])) {
+            $level = $col_array[5];
+        } else {
+            $level = 0;
+        }
+        $record   = 0x007D;          // Record identifier
+        $length   = 0x000B;          // Number of bytes to follow
+
+        $coldx   += 0.72;            // Fudge. Excel subtracts 0.72 !?
+        $coldx   *= 256;             // Convert to units of 1/256 of a char
+
+        $ixfe     = $this->_XF($format);
+        $reserved = 0x00;            // Reserved
+
+        $level = max(0, min($level, 7));
+        $grbit |= $level << 8;
+
+        $header   = pack("vv",     $record, $length);
+        $data     = pack("vvvvvC", $colFirst, $colLast, $coldx,
+                                   $ixfe, $grbit, $reserved);
+        $this->_prepend($header.$data);
+    }
+
+    /**
+    * Write BIFF record SELECTION.
+    *
+    * @access private
+    * @param array $array array containing ($rwFirst,$colFirst,$rwLast,$colLast)
+    * @see setSelection()
+    */
+    function _storeSelection($array)
+    {
+        list($rwFirst,$colFirst,$rwLast,$colLast) = $array;
+        $record   = 0x001D;                  // Record identifier
+        $length   = 0x000F;                  // Number of bytes to follow
+
+        $pnn      = $this->_active_pane;     // Pane position
+        $rwAct    = $rwFirst;                // Active row
+        $colAct   = $colFirst;               // Active column
+        $irefAct  = 0;                       // Active cell ref
+        $cref     = 1;                       // Number of refs
+
+        if (!isset($rwLast)) {
+            $rwLast   = $rwFirst;       // Last  row in reference
+        }
+        if (!isset($colLast)) {
+            $colLast  = $colFirst;      // Last  col in reference
+        }
+
+        // Swap last row/col for first row/col as necessary
+        if ($rwFirst > $rwLast) {
+            list($rwFirst, $rwLast) = array($rwLast, $rwFirst);
+        }
+
+        if ($colFirst > $colLast) {
+            list($colFirst, $colLast) = array($colLast, $colFirst);
+        }
+
+        $header   = pack("vv",         $record, $length);
+        $data     = pack("CvvvvvvCC",  $pnn, $rwAct, $colAct,
+                                       $irefAct, $cref,
+                                       $rwFirst, $rwLast,
+                                       $colFirst, $colLast);
+        $this->_append($header . $data);
+    }
+
+    /**
+    * Store the MERGEDCELLS record for all ranges of merged cells
+    *
+    * @access private
+    */
+    function _storeMergedCells()
+    {
+        // if there are no merged cell ranges set, return
+        if (count($this->_merged_ranges) == 0) {
+            return;
+        }
+        $record   = 0x00E5;
+        $length   = 2 + count($this->_merged_ranges) * 8;
+
+        $header   = pack('vv', $record, $length);
+        $data     = pack('v',  count($this->_merged_ranges));
+        foreach ($this->_merged_ranges as $range) {
+            $data .= pack('vvvv', $range[0], $range[2], $range[1], $range[3]);
+        }
+        $this->_append($header . $data);
+    }
+
+    /**
+    * Write BIFF record EXTERNCOUNT to indicate the number of external sheet
+    * references in a worksheet.
+    *
+    * Excel only stores references to external sheets that are used in formulas.
+    * For simplicity we store references to all the sheets in the workbook
+    * regardless of whether they are used or not. This reduces the overall
+    * complexity and eliminates the need for a two way dialogue between the formula
+    * parser the worksheet objects.
+    *
+    * @access private
+    * @param integer $count The number of external sheet references in this worksheet
+    */
+    function _storeExterncount($count)
+    {
+        $record = 0x0016;          // Record identifier
+        $length = 0x0002;          // Number of bytes to follow
+
+        $header = pack("vv", $record, $length);
+        $data   = pack("v",  $count);
+        $this->_prepend($header . $data);
+    }
+
+    /**
+    * Writes the Excel BIFF EXTERNSHEET record. These references are used by
+    * formulas. A formula references a sheet name via an index. Since we store a
+    * reference to all of the external worksheets the EXTERNSHEET index is the same
+    * as the worksheet index.
+    *
+    * @access private
+    * @param string $sheetname The name of a external worksheet
+    */
+    function _storeExternsheet($sheetname)
+    {
+        $record    = 0x0017;         // Record identifier
+
+        // References to the current sheet are encoded differently to references to
+        // external sheets.
+        //
+        if ($this->name == $sheetname) {
+            $sheetname = '';
+            $length    = 0x02;  // The following 2 bytes
+            $cch       = 1;     // The following byte
+            $rgch      = 0x02;  // Self reference
+        } else {
+            $length    = 0x02 + strlen($sheetname);
+            $cch       = strlen($sheetname);
+            $rgch      = 0x03;  // Reference to a sheet in the current workbook
+        }
+
+        $header = pack("vv",  $record, $length);
+        $data   = pack("CC", $cch, $rgch);
+        $this->_prepend($header . $data . $sheetname);
+    }
+
+    /**
+    * Writes the Excel BIFF PANE record.
+    * The panes can either be frozen or thawed (unfrozen).
+    * Frozen panes are specified in terms of an integer number of rows and columns.
+    * Thawed panes are specified in terms of Excel's units for rows and columns.
+    *
+    * @access private
+    * @param array $panes This is the only parameter received and is composed of the following:
+    *                     0 => Vertical split position,
+    *                     1 => Horizontal split position
+    *                     2 => Top row visible
+    *                     3 => Leftmost column visible
+    *                     4 => Active pane
+    */
+    function _storePanes($panes)
+    {
+        $y       = $panes[0];
+        $x       = $panes[1];
+        $rwTop   = $panes[2];
+        $colLeft = $panes[3];
+        if (count($panes) > 4) { // if Active pane was received
+            $pnnAct = $panes[4];
+        } else {
+            $pnnAct = null;
+        }
+        $record  = 0x0041;       // Record identifier
+        $length  = 0x000A;       // Number of bytes to follow
+
+        // Code specific to frozen or thawed panes.
+        if ($this->_frozen) {
+            // Set default values for $rwTop and $colLeft
+            if (!isset($rwTop)) {
+                $rwTop   = $y;
+            }
+            if (!isset($colLeft)) {
+                $colLeft = $x;
+            }
+        } else {
+            // Set default values for $rwTop and $colLeft
+            if (!isset($rwTop)) {
+                $rwTop   = 0;
+            }
+            if (!isset($colLeft)) {
+                $colLeft = 0;
+            }
+
+            // Convert Excel's row and column units to the internal units.
+            // The default row height is 12.75
+            // The default column width is 8.43
+            // The following slope and intersection values were interpolated.
+            //
+            $y = 20*$y      + 255;
+            $x = 113.879*$x + 390;
+        }
+
+
+        // Determine which pane should be active. There is also the undocumented
+        // option to override this should it be necessary: may be removed later.
+        //
+        if (!isset($pnnAct)) {
+            if ($x != 0 && $y != 0) {
+                $pnnAct = 0; // Bottom right
+            }
+            if ($x != 0 && $y == 0) {
+                $pnnAct = 1; // Top right
+            }
+            if ($x == 0 && $y != 0) {
+                $pnnAct = 2; // Bottom left
+            }
+            if ($x == 0 && $y == 0) {
+                $pnnAct = 3; // Top left
+            }
+        }
+
+        $this->_active_pane = $pnnAct; // Used in _storeSelection
+
+        $header     = pack("vv",    $record, $length);
+        $data       = pack("vvvvv", $x, $y, $rwTop, $colLeft, $pnnAct);
+        $this->_append($header . $data);
+    }
+
+    /**
+    * Store the page setup SETUP BIFF record.
+    *
+    * @access private
+    */
+    function _storeSetup()
+    {
+        $record       = 0x00A1;                  // Record identifier
+        $length       = 0x0022;                  // Number of bytes to follow
+
+        $iPaperSize   = $this->_paper_size;    // Paper size
+        $iScale       = $this->_print_scale;   // Print scaling factor
+        $iPageStart   = 0x01;                 // Starting page number
+        $iFitWidth    = $this->_fit_width;    // Fit to number of pages wide
+        $iFitHeight   = $this->_fit_height;   // Fit to number of pages high
+        $grbit        = 0x00;                 // Option flags
+        $iRes         = 0x0258;               // Print resolution
+        $iVRes        = 0x0258;               // Vertical print resolution
+        $numHdr       = $this->_margin_head;  // Header Margin
+        $numFtr       = $this->_margin_foot;   // Footer Margin
+        $iCopies      = 0x01;                 // Number of copies
+
+        $fLeftToRight = 0x0;                     // Print over then down
+        $fLandscape   = $this->_orientation;     // Page orientation
+        $fNoPls       = 0x0;                     // Setup not read from printer
+        $fNoColor     = 0x0;                     // Print black and white
+        $fDraft       = 0x0;                     // Print draft quality
+        $fNotes       = 0x0;                     // Print notes
+        $fNoOrient    = 0x0;                     // Orientation not set
+        $fUsePage     = 0x0;                     // Use custom starting page
+
+        $grbit           = $fLeftToRight;
+        $grbit          |= $fLandscape    << 1;
+        $grbit          |= $fNoPls        << 2;
+        $grbit          |= $fNoColor      << 3;
+        $grbit          |= $fDraft        << 4;
+        $grbit          |= $fNotes        << 5;
+        $grbit          |= $fNoOrient     << 6;
+        $grbit          |= $fUsePage      << 7;
+
+        $numHdr = pack("d", $numHdr);
+        $numFtr = pack("d", $numFtr);
+        if ($this->_byte_order) { // if it's Big Endian
+            $numHdr = strrev($numHdr);
+            $numFtr = strrev($numFtr);
+        }
+
+        $header = pack("vv", $record, $length);
+        $data1  = pack("vvvvvvvv", $iPaperSize,
+                                   $iScale,
+                                   $iPageStart,
+                                   $iFitWidth,
+                                   $iFitHeight,
+                                   $grbit,
+                                   $iRes,
+                                   $iVRes);
+        $data2  = $numHdr.$numFtr;
+        $data3  = pack("v", $iCopies);
+        $this->_prepend($header . $data1 . $data2 . $data3);
+    }
+
+    /**
+    * Store the header caption BIFF record.
+    *
+    * @access private
+    */
+    function _storeHeader()
+    {
+        $record  = 0x0014;               // Record identifier
+
+        $str      = $this->_header;       // header string
+        $cch      = strlen($str);         // Length of header string
+        if ($this->_BIFF_version == 0x0600) {
+            $encoding = 0x0;                  // TODO: Unicode support
+            $length   = 3 + $cch;             // Bytes to follow
+        } else {
+            $length  = 1 + $cch;             // Bytes to follow
+        }
+
+        $header   = pack("vv", $record, $length);
+        if ($this->_BIFF_version == 0x0600) {
+            $data     = pack("vC",  $cch, $encoding);
+        } else {
+            $data      = pack("C",  $cch);
+        }
+
+        $this->_prepend($header.$data.$str);
+    }
+
+    /**
+    * Store the footer caption BIFF record.
+    *
+    * @access private
+    */
+    function _storeFooter()
+    {
+        $record  = 0x0015;               // Record identifier
+
+        $str      = $this->_footer;       // Footer string
+        $cch      = strlen($str);         // Length of footer string
+        if ($this->_BIFF_version == 0x0600) {
+            $encoding = 0x0;                  // TODO: Unicode support
+            $length   = 3 + $cch;             // Bytes to follow
+        } else {
+            $length  = 1 + $cch;
+        }
+
+        $header    = pack("vv", $record, $length);
+        if ($this->_BIFF_version == 0x0600) {
+            $data      = pack("vC",  $cch, $encoding);
+        } else {
+            $data      = pack("C",  $cch);
+        }
+
+        $this->_prepend($header . $data . $str);
+    }
+
+    /**
+    * Store the horizontal centering HCENTER BIFF record.
+    *
+    * @access private
+    */
+    function _storeHcenter()
+    {
+        $record   = 0x0083;              // Record identifier
+        $length   = 0x0002;              // Bytes to follow
+
+        $fHCenter = $this->_hcenter;     // Horizontal centering
+
+        $header    = pack("vv", $record, $length);
+        $data      = pack("v",  $fHCenter);
+
+        $this->_prepend($header.$data);
+    }
+
+    /**
+    * Store the vertical centering VCENTER BIFF record.
+    *
+    * @access private
+    */
+    function _storeVcenter()
+    {
+        $record   = 0x0084;              // Record identifier
+        $length   = 0x0002;              // Bytes to follow
+
+        $fVCenter = $this->_vcenter;     // Horizontal centering
+
+        $header    = pack("vv", $record, $length);
+        $data      = pack("v",  $fVCenter);
+        $this->_prepend($header . $data);
+    }
+
+    /**
+    * Store the LEFTMARGIN BIFF record.
+    *
+    * @access private
+    */
+    function _storeMarginLeft()
+    {
+        $record  = 0x0026;                   // Record identifier
+        $length  = 0x0008;                   // Bytes to follow
+
+        $margin  = $this->_margin_left;       // Margin in inches
+
+        $header    = pack("vv",  $record, $length);
+        $data      = pack("d",   $margin);
+        if ($this->_byte_order) { // if it's Big Endian
+            $data = strrev($data);
+        }
+
+        $this->_prepend($header . $data);
+    }
+
+    /**
+    * Store the RIGHTMARGIN BIFF record.
+    *
+    * @access private
+    */
+    function _storeMarginRight()
+    {
+        $record  = 0x0027;                   // Record identifier
+        $length  = 0x0008;                   // Bytes to follow
+
+        $margin  = $this->_margin_right;      // Margin in inches
+
+        $header    = pack("vv",  $record, $length);
+        $data      = pack("d",   $margin);
+        if ($this->_byte_order) { // if it's Big Endian
+            $data = strrev($data);
+        }
+
+        $this->_prepend($header . $data);
+    }
+
+    /**
+    * Store the TOPMARGIN BIFF record.
+    *
+    * @access private
+    */
+    function _storeMarginTop()
+    {
+        $record  = 0x0028;                   // Record identifier
+        $length  = 0x0008;                   // Bytes to follow
+
+        $margin  = $this->_margin_top;        // Margin in inches
+
+        $header    = pack("vv",  $record, $length);
+        $data      = pack("d",   $margin);
+        if ($this->_byte_order) { // if it's Big Endian
+            $data = strrev($data);
+        }
+
+        $this->_prepend($header . $data);
+    }
+
+    /**
+    * Store the BOTTOMMARGIN BIFF record.
+    *
+    * @access private
+    */
+    function _storeMarginBottom()
+    {
+        $record  = 0x0029;                   // Record identifier
+        $length  = 0x0008;                   // Bytes to follow
+
+        $margin  = $this->_margin_bottom;     // Margin in inches
+
+        $header    = pack("vv",  $record, $length);
+        $data      = pack("d",   $margin);
+        if ($this->_byte_order) { // if it's Big Endian
+            $data = strrev($data);
+        }
+
+        $this->_prepend($header . $data);
+    }
+
+    /**
+    * Merges the area given by its arguments.
+    * This is an Excel97/2000 method. It is required to perform more complicated
+    * merging than the normal setAlign('merge').
+    *
+    * @access public
+    * @param integer $first_row First row of the area to merge
+    * @param integer $first_col First column of the area to merge
+    * @param integer $last_row  Last row of the area to merge
+    * @param integer $last_col  Last column of the area to merge
+    */
+    function mergeCells($first_row, $first_col, $last_row, $last_col)
+    {
+        $record  = 0x00E5;                   // Record identifier
+        $length  = 0x000A;                   // Bytes to follow
+        $cref     = 1;                       // Number of refs
+
+        // Swap last row/col for first row/col as necessary
+        if ($first_row > $last_row) {
+            list($first_row, $last_row) = array($last_row, $first_row);
+        }
+
+        if ($first_col > $last_col) {
+            list($first_col, $last_col) = array($last_col, $first_col);
+        }
+
+        $header   = pack("vv",    $record, $length);
+        $data     = pack("vvvvv", $cref, $first_row, $last_row,
+                                  $first_col, $last_col);
+
+        $this->_append($header.$data);
+    }
+
+    /**
+    * Write the PRINTHEADERS BIFF record.
+    *
+    * @access private
+    */
+    function _storePrintHeaders()
+    {
+        $record      = 0x002a;                   // Record identifier
+        $length      = 0x0002;                   // Bytes to follow
+
+        $fPrintRwCol = $this->_print_headers;     // Boolean flag
+
+        $header      = pack("vv", $record, $length);
+        $data        = pack("v", $fPrintRwCol);
+        $this->_prepend($header . $data);
+    }
+
+    /**
+    * Write the PRINTGRIDLINES BIFF record. Must be used in conjunction with the
+    * GRIDSET record.
+    *
+    * @access private
+    */
+    function _storePrintGridlines()
+    {
+        $record      = 0x002b;                    // Record identifier
+        $length      = 0x0002;                    // Bytes to follow
+
+        $fPrintGrid  = $this->_print_gridlines;    // Boolean flag
+
+        $header      = pack("vv", $record, $length);
+        $data        = pack("v", $fPrintGrid);
+        $this->_prepend($header . $data);
+    }
+
+    /**
+    * Write the GRIDSET BIFF record. Must be used in conjunction with the
+    * PRINTGRIDLINES record.
+    *
+    * @access private
+    */
+    function _storeGridset()
+    {
+        $record      = 0x0082;                        // Record identifier
+        $length      = 0x0002;                        // Bytes to follow
+
+        $fGridSet    = !($this->_print_gridlines);     // Boolean flag
+
+        $header      = pack("vv",  $record, $length);
+        $data        = pack("v",   $fGridSet);
+        $this->_prepend($header . $data);
+    }
+
+    /**
+    * Write the GUTS BIFF record. This is used to configure the gutter margins
+    * where Excel outline symbols are displayed. The visibility of the gutters is
+    * controlled by a flag in WSBOOL.
+    *
+    * @see _storeWsbool()
+    * @access private
+    */
+    function _storeGuts()
+    {
+        $record      = 0x0080;   // Record identifier
+        $length      = 0x0008;   // Bytes to follow
+
+        $dxRwGut     = 0x0000;   // Size of row gutter
+        $dxColGut    = 0x0000;   // Size of col gutter
+
+        $row_level   = $this->_outline_row_level;
+        $col_level   = 0;
+
+        // Calculate the maximum column outline level. The equivalent calculation
+        // for the row outline level is carried out in setRow().
+        $colcount = count($this->_colinfo);
+        for ($i = 0; $i < $colcount; $i++) {
+           // Skip cols without outline level info.
+           if (count($col_level) >= 6) {
+              $col_level = max($this->_colinfo[$i][5], $col_level);
+           }
+        }
+
+        // Set the limits for the outline levels (0 <= x <= 7).
+        $col_level = max(0, min($col_level, 7));
+
+        // The displayed level is one greater than the max outline levels
+        if ($row_level) {
+            $row_level++;
+        }
+        if ($col_level) {
+            $col_level++;
+        }
+
+        $header      = pack("vv",   $record, $length);
+        $data        = pack("vvvv", $dxRwGut, $dxColGut, $row_level, $col_level);
+
+        $this->_prepend($header.$data);
+    }
+
+
+    /**
+    * Write the WSBOOL BIFF record, mainly for fit-to-page. Used in conjunction
+    * with the SETUP record.
+    *
+    * @access private
+    */
+    function _storeWsbool()
+    {
+        $record      = 0x0081;   // Record identifier
+        $length      = 0x0002;   // Bytes to follow
+        $grbit       = 0x0000;
+
+        // The only option that is of interest is the flag for fit to page. So we
+        // set all the options in one go.
+        //
+        /*if ($this->_fit_page) {
+            $grbit = 0x05c1;
+        } else {
+            $grbit = 0x04c1;
+        }*/
+        // Set the option flags
+        $grbit |= 0x0001;                           // Auto page breaks visible
+        if ($this->_outline_style) {
+            $grbit |= 0x0020; // Auto outline styles
+        }
+        if ($this->_outline_below) {
+            $grbit |= 0x0040; // Outline summary below
+        }
+        if ($this->_outline_right) {
+            $grbit |= 0x0080; // Outline summary right
+        }
+        if ($this->_fit_page) {
+            $grbit |= 0x0100; // Page setup fit to page
+        }
+        if ($this->_outline_on) {
+            $grbit |= 0x0400; // Outline symbols displayed
+        }
+
+        $header      = pack("vv", $record, $length);
+        $data        = pack("v",  $grbit);
+        $this->_prepend($header . $data);
+    }
+
+    /**
+    * Write the HORIZONTALPAGEBREAKS BIFF record.
+    *
+    * @access private
+    */
+    function _storeHbreak()
+    {
+        // Return if the user hasn't specified pagebreaks
+        if (empty($this->_hbreaks)) {
+            return;
+        }
+
+        // Sort and filter array of page breaks
+        $breaks = $this->_hbreaks;
+        sort($breaks, SORT_NUMERIC);
+        if ($breaks[0] == 0) { // don't use first break if it's 0
+            array_shift($breaks);
+        }
+
+        $record  = 0x001b;               // Record identifier
+        $cbrk    = count($breaks);       // Number of page breaks
+        if ($this->_BIFF_version == 0x0600) {
+            $length  = 2 + 6*$cbrk;      // Bytes to follow
+        } else {
+            $length  = 2 + 2*$cbrk;      // Bytes to follow
+        }
+
+        $header  = pack("vv", $record, $length);
+        $data    = pack("v",  $cbrk);
+
+        // Append each page break
+        foreach ($breaks as $break) {
+            if ($this->_BIFF_version == 0x0600) {
+                $data .= pack("vvv", $break, 0x0000, 0x00ff);
+            } else {
+                $data .= pack("v", $break);
+            }
+        }
+
+        $this->_prepend($header.$data);
+    }
+
+
+    /**
+    * Write the VERTICALPAGEBREAKS BIFF record.
+    *
+    * @access private
+    */
+    function _storeVbreak()
+    {
+        // Return if the user hasn't specified pagebreaks
+        if (empty($this->_vbreaks)) {
+            return;
+        }
+
+        // 1000 vertical pagebreaks appears to be an internal Excel 5 limit.
+        // It is slightly higher in Excel 97/200, approx. 1026
+        $breaks = array_slice($this->_vbreaks,0,1000);
+
+        // Sort and filter array of page breaks
+        sort($breaks, SORT_NUMERIC);
+        if ($breaks[0] == 0) { // don't use first break if it's 0
+            array_shift($breaks);
+        }
+
+        $record  = 0x001a;               // Record identifier
+        $cbrk    = count($breaks);       // Number of page breaks
+        if ($this->_BIFF_version == 0x0600) {
+            $length  = 2 + 6*$cbrk;      // Bytes to follow
+        } else {
+            $length  = 2 + 2*$cbrk;      // Bytes to follow
+        }
+
+        $header  = pack("vv",  $record, $length);
+        $data    = pack("v",   $cbrk);
+
+        // Append each page break
+        foreach ($breaks as $break) {
+            if ($this->_BIFF_version == 0x0600) {
+                $data .= pack("vvv", $break, 0x0000, 0xffff);
+            } else {
+                $data .= pack("v", $break);
+            }
+        }
+
+        $this->_prepend($header . $data);
+    }
+
+    /**
+    * Set the Biff PROTECT record to indicate that the worksheet is protected.
+    *
+    * @access private
+    */
+    function _storeProtect()
+    {
+        // Exit unless sheet protection has been specified
+        if ($this->_protect == 0) {
+            return;
+        }
+
+        $record      = 0x0012;             // Record identifier
+        $length      = 0x0002;             // Bytes to follow
+
+        $fLock       = $this->_protect;    // Worksheet is protected
+
+        $header      = pack("vv", $record, $length);
+        $data        = pack("v",  $fLock);
+
+        $this->_prepend($header.$data);
+    }
+
+    /**
+    * Write the worksheet PASSWORD record.
+    *
+    * @access private
+    */
+    function _storePassword()
+    {
+        // Exit unless sheet protection and password have been specified
+        if (($this->_protect == 0) || (!isset($this->_password))) {
+            return;
+        }
+
+        $record      = 0x0013;               // Record identifier
+        $length      = 0x0002;               // Bytes to follow
+
+        $wPassword   = $this->_password;     // Encoded password
+
+        $header      = pack("vv", $record, $length);
+        $data        = pack("v",  $wPassword);
+
+        $this->_prepend($header . $data);
+    }
+
+
+    /**
+    * Insert a 24bit bitmap image in a worksheet.
+    *
+    * @access public
+    * @param integer $row     The row we are going to insert the bitmap into
+    * @param integer $col     The column we are going to insert the bitmap into
+    * @param string  $bitmap  The bitmap filename
+    * @param integer $x       The horizontal position (offset) of the image inside the cell.
+    * @param integer $y       The vertical position (offset) of the image inside the cell.
+    * @param integer $scale_x The horizontal scale
+    * @param integer $scale_y The vertical scale
+    */
+    function insertBitmap($row, $col, $bitmap, $x = 0, $y = 0, $scale_x = 1, $scale_y = 1)
+    {
+        $bitmap_array = $this->_processBitmap($bitmap);
+        if ($this->isError($bitmap_array)) {
+            $this->writeString($row, $col, $bitmap_array->getMessage());
+            return;
+        }
+        list($width, $height, $size, $data) = $bitmap_array; //$this->_processBitmap($bitmap);
+
+        // Scale the frame of the image.
+        $width  *= $scale_x;
+        $height *= $scale_y;
+
+        // Calculate the vertices of the image and write the OBJ record
+        $this->_positionImage($col, $row, $x, $y, $width, $height);
+
+        // Write the IMDATA record to store the bitmap data
+        $record      = 0x007f;
+        $length      = 8 + $size;
+        $cf          = 0x09;
+        $env         = 0x01;
+        $lcb         = $size;
+
+        $header      = pack("vvvvV", $record, $length, $cf, $env, $lcb);
+        $this->_append($header.$data);
+    }
+
+    /**
+    * Calculate the vertices that define the position of the image as required by
+    * the OBJ record.
+    *
+    *         +------------+------------+
+    *         |     A      |      B     |
+    *   +-----+------------+------------+
+    *   |     |(x1,y1)     |            |
+    *   |  1  |(A1)._______|______      |
+    *   |     |    |              |     |
+    *   |     |    |              |     |
+    *   +-----+----|    BITMAP    |-----+
+    *   |     |    |              |     |
+    *   |  2  |    |______________.     |
+    *   |     |            |        (B2)|
+    *   |     |            |     (x2,y2)|
+    *   +---- +------------+------------+
+    *
+    * Example of a bitmap that covers some of the area from cell A1 to cell B2.
+    *
+    * Based on the width and height of the bitmap we need to calculate 8 vars:
+    *     $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2.
+    * The width and height of the cells are also variable and have to be taken into
+    * account.
+    * The values of $col_start and $row_start are passed in from the calling
+    * function. The values of $col_end and $row_end are calculated by subtracting
+    * the width and height of the bitmap from the width and height of the
+    * underlying cells.
+    * The vertices are expressed as a percentage of the underlying cell width as
+    * follows (rhs values are in pixels):
+    *
+    *       x1 = X / W *1024
+    *       y1 = Y / H *256
+    *       x2 = (X-1) / W *1024
+    *       y2 = (Y-1) / H *256
+    *
+    *       Where:  X is distance from the left side of the underlying cell
+    *               Y is distance from the top of the underlying cell
+    *               W is the width of the cell
+    *               H is the height of the cell
+    *
+    * @access private
+    * @note  the SDK incorrectly states that the height should be expressed as a
+    *        percentage of 1024.
+    * @param integer $col_start Col containing upper left corner of object
+    * @param integer $row_start Row containing top left corner of object
+    * @param integer $x1        Distance to left side of object
+    * @param integer $y1        Distance to top of object
+    * @param integer $width     Width of image frame
+    * @param integer $height    Height of image frame
+    */
+    function _positionImage($col_start, $row_start, $x1, $y1, $width, $height)
+    {
+        // Initialise end cell to the same as the start cell
+        $col_end    = $col_start;  // Col containing lower right corner of object
+        $row_end    = $row_start;  // Row containing bottom right corner of object
+
+        // Zero the specified offset if greater than the cell dimensions
+        if ($x1 >= $this->_sizeCol($col_start)) {
+            $x1 = 0;
+        }
+        if ($y1 >= $this->_sizeRow($row_start)) {
+            $y1 = 0;
+        }
+
+        $width      = $width  + $x1 -1;
+        $height     = $height + $y1 -1;
+
+        // Subtract the underlying cell widths to find the end cell of the image
+        while ($width >= $this->_sizeCol($col_end)) {
+            $width -= $this->_sizeCol($col_end);
+            $col_end++;
+        }
+
+        // Subtract the underlying cell heights to find the end cell of the image
+        while ($height >= $this->_sizeRow($row_end)) {
+            $height -= $this->_sizeRow($row_end);
+            $row_end++;
+        }
+
+        // Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell
+        // with zero eight or width.
+        //
+        if ($this->_sizeCol($col_start) == 0) {
+            return;
+        }
+        if ($this->_sizeCol($col_end)   == 0) {
+            return;
+        }
+        if ($this->_sizeRow($row_start) == 0) {
+            return;
+        }
+        if ($this->_sizeRow($row_end)   == 0) {
+            return;
+        }
+
+        // Convert the pixel values to the percentage value expected by Excel
+        $x1 = $x1     / $this->_sizeCol($col_start)   * 1024;
+        $y1 = $y1     / $this->_sizeRow($row_start)   *  256;
+        $x2 = $width  / $this->_sizeCol($col_end)     * 1024; // Distance to right side of object
+        $y2 = $height / $this->_sizeRow($row_end)     *  256; // Distance to bottom of object
+
+        $this->_storeObjPicture($col_start, $x1,
+                                 $row_start, $y1,
+                                 $col_end, $x2,
+                                 $row_end, $y2);
+    }
+
+    /**
+    * Convert the width of a cell from user's units to pixels. By interpolation
+    * the relationship is: y = 7x +5. If the width hasn't been set by the user we
+    * use the default value. If the col is hidden we use a value of zero.
+    *
+    * @access private
+    * @param integer $col The column
+    * @return integer The width in pixels
+    */
+    function _sizeCol($col)
+    {
+        // Look up the cell value to see if it has been changed
+        if (isset($this->col_sizes[$col])) {
+            if ($this->col_sizes[$col] == 0) {
+                return(0);
+            } else {
+                return(floor(7 * $this->col_sizes[$col] + 5));
+            }
+        } else {
+            return(64);
+        }
+    }
+
+    /**
+    * Convert the height of a cell from user's units to pixels. By interpolation
+    * the relationship is: y = 4/3x. If the height hasn't been set by the user we
+    * use the default value. If the row is hidden we use a value of zero. (Not
+    * possible to hide row yet).
+    *
+    * @access private
+    * @param integer $row The row
+    * @return integer The width in pixels
+    */
+    function _sizeRow($row)
+    {
+        // Look up the cell value to see if it has been changed
+        if (isset($this->_row_sizes[$row])) {
+            if ($this->_row_sizes[$row] == 0) {
+                return(0);
+            } else {
+                return(floor(4/3 * $this->_row_sizes[$row]));
+            }
+        } else {
+            return(17);
+        }
+    }
+
+    /**
+    * Store the OBJ record that precedes an IMDATA record. This could be generalise
+    * to support other Excel objects.
+    *
+    * @access private
+    * @param integer $colL Column containing upper left corner of object
+    * @param integer $dxL  Distance from left side of cell
+    * @param integer $rwT  Row containing top left corner of object
+    * @param integer $dyT  Distance from top of cell
+    * @param integer $colR Column containing lower right corner of object
+    * @param integer $dxR  Distance from right of cell
+    * @param integer $rwB  Row containing bottom right corner of object
+    * @param integer $dyB  Distance from bottom of cell
+    */
+    function _storeObjPicture($colL,$dxL,$rwT,$dyT,$colR,$dxR,$rwB,$dyB)
+    {
+        $record      = 0x005d;   // Record identifier
+        $length      = 0x003c;   // Bytes to follow
+
+        $cObj        = 0x0001;   // Count of objects in file (set to 1)
+        $OT          = 0x0008;   // Object type. 8 = Picture
+        $id          = 0x0001;   // Object ID
+        $grbit       = 0x0614;   // Option flags
+
+        $cbMacro     = 0x0000;   // Length of FMLA structure
+        $Reserved1   = 0x0000;   // Reserved
+        $Reserved2   = 0x0000;   // Reserved
+
+        $icvBack     = 0x09;     // Background colour
+        $icvFore     = 0x09;     // Foreground colour
+        $fls         = 0x00;     // Fill pattern
+        $fAuto       = 0x00;     // Automatic fill
+        $icv         = 0x08;     // Line colour
+        $lns         = 0xff;     // Line style
+        $lnw         = 0x01;     // Line weight
+        $fAutoB      = 0x00;     // Automatic border
+        $frs         = 0x0000;   // Frame style
+        $cf          = 0x0009;   // Image format, 9 = bitmap
+        $Reserved3   = 0x0000;   // Reserved
+        $cbPictFmla  = 0x0000;   // Length of FMLA structure
+        $Reserved4   = 0x0000;   // Reserved
+        $grbit2      = 0x0001;   // Option flags
+        $Reserved5   = 0x0000;   // Reserved
+
+
+        $header      = pack("vv", $record, $length);
+        $data        = pack("V", $cObj);
+        $data       .= pack("v", $OT);
+        $data       .= pack("v", $id);
+        $data       .= pack("v", $grbit);
+        $data       .= pack("v", $colL);
+        $data       .= pack("v", $dxL);
+        $data       .= pack("v", $rwT);
+        $data       .= pack("v", $dyT);
+        $data       .= pack("v", $colR);
+        $data       .= pack("v", $dxR);
+        $data       .= pack("v", $rwB);
+        $data       .= pack("v", $dyB);
+        $data       .= pack("v", $cbMacro);
+        $data       .= pack("V", $Reserved1);
+        $data       .= pack("v", $Reserved2);
+        $data       .= pack("C", $icvBack);
+        $data       .= pack("C", $icvFore);
+        $data       .= pack("C", $fls);
+        $data       .= pack("C", $fAuto);
+        $data       .= pack("C", $icv);
+        $data       .= pack("C", $lns);
+        $data       .= pack("C", $lnw);
+        $data       .= pack("C", $fAutoB);
+        $data       .= pack("v", $frs);
+        $data       .= pack("V", $cf);
+        $data       .= pack("v", $Reserved3);
+        $data       .= pack("v", $cbPictFmla);
+        $data       .= pack("v", $Reserved4);
+        $data       .= pack("v", $grbit2);
+        $data       .= pack("V", $Reserved5);
+
+        $this->_append($header . $data);
+    }
+
+    /**
+    * Convert a 24 bit bitmap into the modified internal format used by Windows.
+    * This is described in BITMAPCOREHEADER and BITMAPCOREINFO structures in the
+    * MSDN library.
+    *
+    * @access private
+    * @param string $bitmap The bitmap to process
+    * @return array Array with data and properties of the bitmap
+    */
+    function _processBitmap($bitmap)
+    {
+        // Open file.
+        $bmp_fd = @fopen($bitmap,"rb");
+        if (!$bmp_fd) {
+            die("Couldn't import $bitmap");
+        }
+
+        // Slurp the file into a string.
+        $data = fread($bmp_fd, filesize($bitmap));
+
+        // Check that the file is big enough to be a bitmap.
+        if (strlen($data) <= 0x36) {
+            die("$bitmap doesn't contain enough data.\n");
+        }
+
+        // The first 2 bytes are used to identify the bitmap.
+        $identity = unpack("A2ident", $data);
+        if ($identity['ident'] != "BM") {
+            die("$bitmap doesn't appear to be a valid bitmap image.\n");
+        }
+
+        // Remove bitmap data: ID.
+        $data = substr($data, 2);
+
+        // Read and remove the bitmap size. This is more reliable than reading
+        // the data size at offset 0x22.
+        //
+        $size_array   = unpack("Vsa", substr($data, 0, 4));
+        $size   = $size_array['sa'];
+        $data   = substr($data, 4);
+        $size  -= 0x36; // Subtract size of bitmap header.
+        $size  += 0x0C; // Add size of BIFF header.
+
+        // Remove bitmap data: reserved, offset, header length.
+        $data = substr($data, 12);
+
+        // Read and remove the bitmap width and height. Verify the sizes.
+        $width_and_height = unpack("V2", substr($data, 0, 8));
+        $width  = $width_and_height[1];
+        $height = $width_and_height[2];
+        $data   = substr($data, 8);
+        if ($width > 0xFFFF) {
+            die("$bitmap: largest image width supported is 65k.\n");
+        }
+        if ($height > 0xFFFF) {
+            die("$bitmap: largest image height supported is 65k.\n");
+        }
+
+        // Read and remove the bitmap planes and bpp data. Verify them.
+        $planes_and_bitcount = unpack("v2", substr($data, 0, 4));
+        $data = substr($data, 4);
+        if ($planes_and_bitcount[2] != 24) { // Bitcount
+            die("$bitmap isn't a 24bit true color bitmap.\n");
+        }
+        if ($planes_and_bitcount[1] != 1) {
+            die("$bitmap: only 1 plane supported in bitmap image.\n");
+        }
+
+        // Read and remove the bitmap compression. Verify compression.
+        $compression = unpack("Vcomp", substr($data, 0, 4));
+        $data = substr($data, 4);
+
+        //$compression = 0;
+        if ($compression['comp'] != 0) {
+            die("$bitmap: compression not supported in bitmap image.\n");
+        }
+
+        // Remove bitmap data: data size, hres, vres, colours, imp. colours.
+        $data = substr($data, 20);
+
+        // Add the BITMAPCOREHEADER data
+        $header  = pack("Vvvvv", 0x000c, $width, $height, 0x01, 0x18);
+        $data    = $header . $data;
+
+        return (array($width, $height, $size, $data));
+    }
+
+    /**
+    * Store the window zoom factor. This should be a reduced fraction but for
+    * simplicity we will store all fractions with a numerator of 100.
+    *
+    * @access private
+    */
+    function _storeZoom()
+    {
+        // If scale is 100 we don't need to write a record
+        if ($this->_zoom == 100) {
+            return;
+        }
+
+        $record      = 0x00A0;               // Record identifier
+        $length      = 0x0004;               // Bytes to follow
+
+        $header      = pack("vv", $record, $length);
+        $data        = pack("vv", $this->_zoom, 100);
+        $this->_append($header . $data);
+    }
+
+    /**
+    * FIXME: add comments
+    */
+    function setValidation($row1, $col1, $row2, $col2, &$validator)
+    {
+        $this->_dv[] = $validator->_getData() .
+                       pack("vvvvv", 1, $row1, $row2, $col1, $col2);
+    }
+
+    /**
+    * Store the DVAL and DV records.
+    *
+    * @access private
+    */
+    function _storeDataValidity()
+    {
+        $record      = 0x01b2;      // Record identifier
+        $length      = 0x0012;      // Bytes to follow
+
+        $grbit       = 0x0002;      // Prompt box at cell, no cached validity data at DV records
+        $horPos      = 0x00000000;  // Horizontal position of prompt box, if fixed position
+        $verPos      = 0x00000000;  // Vertical position of prompt box, if fixed position
+        $objId       = 0xffffffff;  // Object identifier of drop down arrow object, or -1 if not visible
+
+        $header      = pack('vv', $record, $length);
+        $data        = pack('vVVVV', $grbit, $horPos, $verPos, $objId,
+                                     count($this->_dv));
+        $this->_append($header.$data);
+
+        $record = 0x01be;              // Record identifier
+        foreach ($this->_dv as $dv) {
+            $length = strlen($dv);      // Bytes to follow
+            $header = pack("vv", $record, $length);
+            $this->_append($header . $dv);
+        }
+    }
+}
+
+/**
+* Class for generating Excel Spreadsheets
+*
+* @author   Xavier Noguer <xnoguer@rezebra.com>
+* @category FileFormats
+* @package  Spreadsheet_Excel_Writer
+*/
+
+class Spreadsheet_Excel_Writer_Workbook extends Spreadsheet_Excel_Writer_BIFFwriter
+{
+    /**
+    * Filename for the Workbook
+    * @var string
+    */
+    var $_filename;
+
+    /**
+    * Formula parser
+    * @var object Parser
+    */
+    var $_parser;
+
+    /**
+    * Flag for 1904 date system (0 => base date is 1900, 1 => base date is 1904)
+    * @var integer
+    */
+    var $_1904;
+
+    /**
+    * The active worksheet of the workbook (0 indexed)
+    * @var integer
+    */
+    var $_activesheet;
+
+    /**
+    * 1st displayed worksheet in the workbook (0 indexed)
+    * @var integer
+    */
+    var $_firstsheet;
+
+    /**
+    * Number of workbook tabs selected
+    * @var integer
+    */
+    var $_selected;
+
+    /**
+    * Index for creating adding new formats to the workbook
+    * @var integer
+    */
+    var $_xf_index;
+
+    /**
+    * Flag for preventing close from being called twice.
+    * @var integer
+    * @see close()
+    */
+    var $_fileclosed;
+
+    /**
+    * The BIFF file size for the workbook.
+    * @var integer
+    * @see _calcSheetOffsets()
+    */
+    var $_biffsize;
+
+    /**
+    * The default sheetname for all sheets created.
+    * @var string
+    */
+    var $_sheetname;
+
+    /**
+    * The default XF format.
+    * @var object Format
+    */
+    var $_tmp_format;
+
+    /**
+    * Array containing references to all of this workbook's worksheets
+    * @var array
+    */
+    var $_worksheets;
+
+    /**
+    * Array of sheetnames for creating the EXTERNSHEET records
+    * @var array
+    */
+    var $_sheetnames;
+
+    /**
+    * Array containing references to all of this workbook's formats
+    * @var array
+    */
+    var $_formats;
+
+    /**
+    * Array containing the colour palette
+    * @var array
+    */
+    var $_palette;
+
+    /**
+    * The default format for URLs.
+    * @var object Format
+    */
+    var $_url_format;
+
+    /**
+    * The codepage indicates the text encoding used for strings
+    * @var integer
+    */
+    var $_codepage;
+
+    /**
+    * The country code used for localization
+    * @var integer
+    */
+    var $_country_code;
+
+    /**
+    * The temporary dir for storing the OLE file
+    * @var string
+    */
+    var $_tmp_dir;
+
+    /**
+    * number of bytes for sizeinfo of strings
+    * @var integer
+    */
+    var $_string_sizeinfo_size;
+
+    /**
+    * Class constructor
+    *
+    * @param string filename for storing the workbook. "-" for writing to stdout.
+    * @access public
+    */
+    function Spreadsheet_Excel_Writer_Workbook($filename)
+    {
+        // It needs to call its parent's constructor explicitly
+        $this->Spreadsheet_Excel_Writer_BIFFwriter();
+
+        $this->_filename         = $filename;
+        $this->_parser           =& new Spreadsheet_Excel_Writer_Parser($this->_byte_order, $this->_BIFF_version);
+        $this->_1904             = 0;
+        $this->_activesheet      = 0;
+        $this->_firstsheet       = 0;
+        $this->_selected         = 0;
+        $this->_xf_index         = 16; // 15 style XF's and 1 cell XF.
+        $this->_fileclosed       = 0;
+        $this->_biffsize         = 0;
+        $this->_sheetname        = 'Sheet';
+        $this->_tmp_format       =& new Spreadsheet_Excel_Writer_Format($this->_BIFF_version);
+        $this->_worksheets       = array();
+        $this->_sheetnames       = array();
+        $this->_formats          = array();
+        $this->_palette          = array();
+        $this->_codepage         = 0x04E4; // FIXME: should change for BIFF8
+        $this->_country_code     = -1;
+        $this->_string_sizeinfo  = 3;
+
+        // Add the default format for hyperlinks
+        $this->_url_format =& $this->addFormat(array('color' => 'blue', 'underline' => 1));
+        $this->_str_total       = 0;
+        $this->_str_unique      = 0;
+        $this->_str_table       = array();
+        $this->_setPaletteXl97();
+        $this->_tmp_dir         = '';
+    }
+
+    /**
+    * Calls finalization methods.
+    * This method should always be the last one to be called on every workbook
+    *
+    * @access public
+    * @return mixed true on success. PEAR_Error on failure
+    */
+    function close()
+    {
+        if ($this->_fileclosed) { // Prevent close() from being called twice.
+            return true;
+        }
+        $this->_storeWorkbook();
+        $this->_fileclosed = 1;
+        return true;
+    }
+
+    /**
+    * An accessor for the _worksheets[] array
+    * Returns an array of the worksheet objects in a workbook
+    * It actually calls to worksheets()
+    *
+    * @access public
+    * @see worksheets()
+    * @return array
+    */
+    function sheets()
+    {
+        return $this->worksheets();
+    }
+
+    /**
+    * An accessor for the _worksheets[] array.
+    * Returns an array of the worksheet objects in a workbook
+    *
+    * @access public
+    * @return array
+    */
+    function worksheets()
+    {
+        return $this->_worksheets;
+    }
+
+    /**
+    * Sets the BIFF version.
+    * This method exists just to access experimental functionality
+    * from BIFF8. It will be deprecated !
+    * Only possible value is 8 (Excel 97/2000).
+    * For any other value it fails silently.
+    *
+    * @access public
+    * @param integer $version The BIFF version
+    */
+    function setVersion($version)
+    {
+        if ($version == 8) { // only accept version 8
+            $version = 0x0600;
+            $this->_BIFF_version = $version;
+            // change BIFFwriter limit for CONTINUE records
+            $this->_limit = 8228;
+            $this->_tmp_format->_BIFF_version = $version;
+            $this->_url_format->_BIFF_version = $version;
+            $this->_parser->_BIFF_version = $version;
+
+            $total_worksheets = count($this->_worksheets);
+            // change version for all worksheets too
+            for ($i = 0; $i < $total_worksheets; $i++) {
+                $this->_worksheets[$i]->_BIFF_version = $version;
+            }
+
+            $total_formats = count($this->_formats);
+            // change version for all formats too
+            for ($i = 0; $i < $total_formats; $i++) {
+                $this->_formats[$i]->_BIFF_version = $version;
+            }
+        }
+    }
+
+    /**
+    * Set the country identifier for the workbook
+    *
+    * @access public
+    * @param integer $code Is the international calling country code for the
+    *                      chosen country.
+    */
+    function setCountry($code)
+    {
+        $this->_country_code = $code;
+    }
+
+    /**
+    * Add a new worksheet to the Excel workbook.
+    * If no name is given the name of the worksheet will be Sheeti$i, with
+    * $i in [1..].
+    *
+    * @access public
+    * @param string $name the optional name of the worksheet
+    * @return mixed reference to a worksheet object on success, PEAR_Error
+    *               on failure
+    */
+    function &addWorksheet($name = '')
+    {
+        $index     = count($this->_worksheets);
+        $sheetname = $this->_sheetname;
+
+        if ($name == '') {
+            $name = $sheetname.($index+1);
+        }
+
+        // Check that sheetname is <= 31 chars (Excel limit before BIFF8).
+        if ($this->_BIFF_version != 0x0600)
+        {
+            if (strlen($name) > 31) {
+                die("Sheetname $name must be <= 31 chars");
+            }
+        }
+
+        // Check that the worksheet name doesn't already exist: a fatal Excel error.
+        $total_worksheets = count($this->_worksheets);
+        for ($i = 0; $i < $total_worksheets; $i++) {
+            if ($this->_worksheets[$i]->getName() == $name) {
+                die("Worksheet '$name' already exists");
+            }
+        }
+
+        $worksheet = new Spreadsheet_Excel_Writer_Worksheet($this->_BIFF_version,
+                                   $name, $index,
+                                   $this->_activesheet, $this->_firstsheet,
+                                   $this->_str_total, $this->_str_unique,
+                                   $this->_str_table, $this->_url_format,
+                                   $this->_parser);
+
+        $this->_worksheets[$index] = &$worksheet;    // Store ref for iterator
+        $this->_sheetnames[$index] = $name;          // Store EXTERNSHEET names
+        $this->_parser->setExtSheet($name, $index);  // Register worksheet name with parser
+        return $worksheet;
+    }
+
+    /**
+    * Add a new format to the Excel workbook.
+    * Also, pass any properties to the Format constructor.
+    *
+    * @access public
+    * @param array $properties array with properties for initializing the format.
+    * @return &Spreadsheet_Excel_Writer_Format reference to an Excel Format
+    */
+    function &addFormat($properties = array())
+    {
+        $format = new Spreadsheet_Excel_Writer_Format($this->_BIFF_version, $this->_xf_index, $properties);
+        $this->_xf_index += 1;
+        $this->_formats[] = &$format;
+        return $format;
+    }
+
+    /**
+     * Create new validator.
+     *
+     * @access public
+     * @return &Spreadsheet_Excel_Writer_Validator reference to a Validator
+     */
+    function &addValidator()
+    {
+        include_once 'Spreadsheet/Excel/Writer/Validator.php';
+        /* FIXME: check for successful inclusion*/
+        $valid = new Spreadsheet_Excel_Writer_Validator($this->_parser);
+        return $valid;
+    }
+
+    /**
+    * Change the RGB components of the elements in the colour palette.
+    *
+    * @access public
+    * @param integer $index colour index
+    * @param integer $red   red RGB value [0-255]
+    * @param integer $green green RGB value [0-255]
+    * @param integer $blue  blue RGB value [0-255]
+    * @return integer The palette index for the custom color
+    */
+    function setCustomColor($index, $red, $green, $blue)
+    {
+        // Match a HTML #xxyyzz style parameter
+        /*if (defined $_[1] and $_[1] =~ /^#(\w\w)(\w\w)(\w\w)/ ) {
+            @_ = ($_[0], hex $1, hex $2, hex $3);
+        }*/
+
+        // Check that the colour index is the right range
+        if ($index < 8 or $index > 64) {
+            // TODO: assign real error codes
+            die("Color index $index outside range: 8 <= index <= 64");
+        }
+
+        // Check that the colour components are in the right range
+        if (($red   < 0 or $red   > 255) ||
+            ($green < 0 or $green > 255) ||
+            ($blue  < 0 or $blue  > 255))
+        {
+            die("Color component outside range: 0 <= color <= 255");
+        }
+
+        $index -= 8; // Adjust colour index (wingless dragonfly)
+
+        // Set the RGB value
+        $this->_palette[$index] = array($red, $green, $blue, 0);
+        return($index + 8);
+    }
+
+    /**
+    * Sets the colour palette to the Excel 97+ default.
+    *
+    * @access private
+    */
+    function _setPaletteXl97()
+    {
+        $this->_palette = array(
+                           array(0x00, 0x00, 0x00, 0x00),   // 8
+                           array(0xff, 0xff, 0xff, 0x00),   // 9
+                           array(0xff, 0x00, 0x00, 0x00),   // 10
+                           array(0x00, 0xff, 0x00, 0x00),   // 11
+                           array(0x00, 0x00, 0xff, 0x00),   // 12
+                           array(0xff, 0xff, 0x00, 0x00),   // 13
+                           array(0xff, 0x00, 0xff, 0x00),   // 14
+                           array(0x00, 0xff, 0xff, 0x00),   // 15
+                           array(0x80, 0x00, 0x00, 0x00),   // 16
+                           array(0x00, 0x80, 0x00, 0x00),   // 17
+                           array(0x00, 0x00, 0x80, 0x00),   // 18
+                           array(0x80, 0x80, 0x00, 0x00),   // 19
+                           array(0x80, 0x00, 0x80, 0x00),   // 20
+                           array(0x00, 0x80, 0x80, 0x00),   // 21
+                           array(0xc0, 0xc0, 0xc0, 0x00),   // 22
+                           array(0x80, 0x80, 0x80, 0x00),   // 23
+                           array(0x99, 0x99, 0xff, 0x00),   // 24
+                           array(0x99, 0x33, 0x66, 0x00),   // 25
+                           array(0xff, 0xff, 0xcc, 0x00),   // 26
+                           array(0xcc, 0xff, 0xff, 0x00),   // 27
+                           array(0x66, 0x00, 0x66, 0x00),   // 28
+                           array(0xff, 0x80, 0x80, 0x00),   // 29
+                           array(0x00, 0x66, 0xcc, 0x00),   // 30
+                           array(0xcc, 0xcc, 0xff, 0x00),   // 31
+                           array(0x00, 0x00, 0x80, 0x00),   // 32
+                           array(0xff, 0x00, 0xff, 0x00),   // 33
+                           array(0xff, 0xff, 0x00, 0x00),   // 34
+                           array(0x00, 0xff, 0xff, 0x00),   // 35
+                           array(0x80, 0x00, 0x80, 0x00),   // 36
+                           array(0x80, 0x00, 0x00, 0x00),   // 37
+                           array(0x00, 0x80, 0x80, 0x00),   // 38
+                           array(0x00, 0x00, 0xff, 0x00),   // 39
+                           array(0x00, 0xcc, 0xff, 0x00),   // 40
+                           array(0xcc, 0xff, 0xff, 0x00),   // 41
+                           array(0xcc, 0xff, 0xcc, 0x00),   // 42
+                           array(0xff, 0xff, 0x99, 0x00),   // 43
+                           array(0x99, 0xcc, 0xff, 0x00),   // 44
+                           array(0xff, 0x99, 0xcc, 0x00),   // 45
+                           array(0xcc, 0x99, 0xff, 0x00),   // 46
+                           array(0xff, 0xcc, 0x99, 0x00),   // 47
+                           array(0x33, 0x66, 0xff, 0x00),   // 48
+                           array(0x33, 0xcc, 0xcc, 0x00),   // 49
+                           array(0x99, 0xcc, 0x00, 0x00),   // 50
+                           array(0xff, 0xcc, 0x00, 0x00),   // 51
+                           array(0xff, 0x99, 0x00, 0x00),   // 52
+                           array(0xff, 0x66, 0x00, 0x00),   // 53
+                           array(0x66, 0x66, 0x99, 0x00),   // 54
+                           array(0x96, 0x96, 0x96, 0x00),   // 55
+                           array(0x00, 0x33, 0x66, 0x00),   // 56
+                           array(0x33, 0x99, 0x66, 0x00),   // 57
+                           array(0x00, 0x33, 0x00, 0x00),   // 58
+                           array(0x33, 0x33, 0x00, 0x00),   // 59
+                           array(0x99, 0x33, 0x00, 0x00),   // 60
+                           array(0x99, 0x33, 0x66, 0x00),   // 61
+                           array(0x33, 0x33, 0x99, 0x00),   // 62
+                           array(0x33, 0x33, 0x33, 0x00),   // 63
+                         );
+    }
+
+    /**
+    * Assemble worksheets into a workbook and send the BIFF data to an OLE
+    * storage.
+    *
+    * @access private
+    * @return mixed true on success. PEAR_Error on failure
+    */
+    function _storeWorkbook()
+    {
+        // Ensure that at least one worksheet has been selected.
+        if ($this->_activesheet == 0) {
+            $this->_worksheets[0]->selected = 1;
+        }
+
+        // Calculate the number of selected worksheet tabs and call the finalization
+        // methods for each worksheet
+        $total_worksheets = count($this->_worksheets);
+        for ($i = 0; $i < $total_worksheets; $i++) {
+            if ($this->_worksheets[$i]->selected) {
+                $this->_selected++;
+            }
+            $this->_worksheets[$i]->close($this->_sheetnames);
+        }
+
+        // Add Workbook globals
+        $this->_storeBof(0x0005);
+        $this->_storeCodepage();
+        if ($this->_BIFF_version == 0x0600) {
+            $this->_storeWindow1();
+        }
+        if ($this->_BIFF_version == 0x0500) {
+            $this->_storeExterns();    // For print area and repeat rows
+        }
+        $this->_storeNames();      // For print area and repeat rows
+        if ($this->_BIFF_version == 0x0500) {
+            $this->_storeWindow1();
+        }
+        $this->_storeDatemode();
+        $this->_storeAllFonts();
+        $this->_storeAllNumFormats();
+        $this->_storeAllXfs();
+        $this->_storeAllStyles();
+        $this->_storePalette();
+        $this->_calcSheetOffsets();
+
+        // Add BOUNDSHEET records
+        for ($i = 0; $i < $total_worksheets; $i++) {
+            $this->_storeBoundsheet($this->_worksheets[$i]->name,$this->_worksheets[$i]->offset);
+        }
+
+        if ($this->_country_code != -1) {
+            $this->_storeCountry();
+        }
+
+        if ($this->_BIFF_version == 0x0600) {
+            //$this->_storeSupbookInternal();
+            /* TODO: store external SUPBOOK records and XCT and CRN records
+            in case of external references for BIFF8 */
+            //$this->_storeExternsheetBiff8();
+            $this->_storeSharedStringsTable();
+        }
+
+        // End Workbook globals
+        $this->_storeEof();
+
+        // Store the workbook in an OLE container
+        $res = $this->_storeOLEFile();
+        return true;
+    }
+
+    /**
+    * Sets the temp dir used for storing the OLE file
+    *
+    * @access public
+    * @param string $dir The dir to be used as temp dir
+    * @return true if given dir is valid, false otherwise
+    */
+    function setTempDir($dir)
+    {
+        if (is_dir($dir)) {
+            $this->_tmp_dir = $dir;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+    * Store the workbook in an OLE container
+    *
+    * @access private
+    * @return mixed true on success. PEAR_Error on failure
+    */
+    function _storeOLEFile()
+    {
+        $OLE  = new OLEwriter($this->_filename);
+        // Write Worksheet data if data <~ 7MB
+        if ($OLE->setSize($this->_biffsize))
+        {
+            $OLE->writeHeader();
+            $OLE->write($this->_data);
+            foreach($this->_worksheets as $sheet) 
+            {
+                while ($tmp = $sheet->getData()) {
+                    $OLE->write($tmp);
+                }
+            }
+        }
+        $OLE->close();
+        return true;
+    }
+
+    /**
+    * Calculate offsets for Worksheet BOF records.
+    *
+    * @access private
+    */
+    function _calcSheetOffsets()
+    {
+        if ($this->_BIFF_version == 0x0600) {
+            $boundsheet_length = 12;  // fixed length for a BOUNDSHEET record
+        } else {
+            $boundsheet_length = 11;
+        }
+        $EOF               = 4;
+        $offset            = $this->_datasize;
+
+        if ($this->_BIFF_version == 0x0600) {
+            // add the length of the SST
+            /* TODO: check this works for a lot of strings (> 8224 bytes) */
+            $offset += $this->_calculateSharedStringsSizes();
+            if ($this->_country_code != -1) {
+                $offset += 8; // adding COUNTRY record
+            }
+            // add the lenght of SUPBOOK, EXTERNSHEET and NAME records
+            //$offset += 8; // FIXME: calculate real value when storing the records
+        }
+        $total_worksheets = count($this->_worksheets);
+        // add the length of the BOUNDSHEET records
+        for ($i = 0; $i < $total_worksheets; $i++) {
+            $offset += $boundsheet_length + strlen($this->_worksheets[$i]->name);
+        }
+        $offset += $EOF;
+
+        for ($i = 0; $i < $total_worksheets; $i++) {
+            $this->_worksheets[$i]->offset = $offset;
+            $offset += $this->_worksheets[$i]->_datasize;
+        }
+        $this->_biffsize = $offset;
+    }
+
+    /**
+    * Store the Excel FONT records.
+    *
+    * @access private
+    */
+    function _storeAllFonts()
+    {
+        // tmp_format is added by the constructor. We use this to write the default XF's
+        $format = $this->_tmp_format;
+        $font   = $format->getFont();
+
+        // Note: Fonts are 0-indexed. According to the SDK there is no index 4,
+        // so the following fonts are 0, 1, 2, 3, 5
+        //
+        for ($i = 1; $i <= 5; $i++){
+            $this->_append($font);
+        }
+
+        // Iterate through the XF objects and write a FONT record if it isn't the
+        // same as the default FONT and if it hasn't already been used.
+        //
+        $fonts = array();
+        $index = 6;                  // The first user defined FONT
+
+        $key = $format->getFontKey(); // The default font from _tmp_format
+        $fonts[$key] = 0;             // Index of the default font
+
+        $total_formats = count($this->_formats);
+        for ($i = 0; $i < $total_formats; $i++) {
+            $key = $this->_formats[$i]->getFontKey();
+            if (isset($fonts[$key])) {
+                // FONT has already been used
+                $this->_formats[$i]->font_index = $fonts[$key];
+            } else {
+                // Add a new FONT record
+                $fonts[$key]        = $index;
+                $this->_formats[$i]->font_index = $index;
+                $index++;
+                $font = $this->_formats[$i]->getFont();
+                $this->_append($font);
+            }
+        }
+    }
+
+    /**
+    * Store user defined numerical formats i.e. FORMAT records
+    *
+    * @access private
+    */
+    function _storeAllNumFormats()
+    {
+        // Leaning num_format syndrome
+        $hash_num_formats = array();
+        $num_formats      = array();
+        $index = 164;
+
+        // Iterate through the XF objects and write a FORMAT record if it isn't a
+        // built-in format type and if the FORMAT string hasn't already been used.
+        $total_formats = count($this->_formats);
+        for ($i = 0; $i < $total_formats; $i++) {
+            $num_format = $this->_formats[$i]->_num_format;
+
+            // Check if $num_format is an index to a built-in format.
+            // Also check for a string of zeros, which is a valid format string
+            // but would evaluate to zero.
+            //
+            if (!preg_match("/^0+\d/", $num_format)) {
+                if (preg_match("/^\d+$/", $num_format)) { // built-in format
+                    continue;
+                }
+            }
+
+            if (isset($hash_num_formats[$num_format])) {
+                // FORMAT has already been used
+                $this->_formats[$i]->_num_format = $hash_num_formats[$num_format];
+            } else{
+                // Add a new FORMAT
+                $hash_num_formats[$num_format]  = $index;
+                $this->_formats[$i]->_num_format = $index;
+                array_push($num_formats,$num_format);
+                $index++;
+            }
+        }
+
+        // Write the new FORMAT records starting from 0xA4
+        $index = 164;
+        foreach ($num_formats as $num_format) {
+            $this->_storeNumFormat($num_format,$index);
+            $index++;
+        }
+    }
+
+    /**
+    * Write all XF records.
+    *
+    * @access private
+    */
+    function _storeAllXfs()
+    {
+        // _tmp_format is added by the constructor. We use this to write the default XF's
+        // The default font index is 0
+        //
+        $format = $this->_tmp_format;
+        for ($i = 0; $i <= 14; $i++) {
+            $xf = $format->getXf('style'); // Style XF
+            $this->_append($xf);
+        }
+
+        $xf = $format->getXf('cell');      // Cell XF
+        $this->_append($xf);
+
+        // User defined XFs
+        $total_formats = count($this->_formats);
+        for ($i = 0; $i < $total_formats; $i++) {
+            $xf = $this->_formats[$i]->getXf('cell');
+            $this->_append($xf);
+        }
+    }
+
+    /**
+    * Write all STYLE records.
+    *
+    * @access private
+    */
+    function _storeAllStyles()
+    {
+        $this->_storeStyle();
+    }
+
+    /**
+    * Write the EXTERNCOUNT and EXTERNSHEET records. These are used as indexes for
+    * the NAME records.
+    *
+    * @access private
+    */
+    function _storeExterns()
+    {
+        // Create EXTERNCOUNT with number of worksheets
+        $this->_storeExterncount(count($this->_worksheets));
+
+        // Create EXTERNSHEET for each worksheet
+        foreach ($this->_sheetnames as $sheetname) {
+            $this->_storeExternsheet($sheetname);
+        }
+    }
+
+    /**
+    * Write the NAME record to define the print area and the repeat rows and cols.
+    *
+    * @access private
+    */
+    function _storeNames()
+    {
+        // Create the print area NAME records
+        $total_worksheets = count($this->_worksheets);
+        for ($i = 0; $i < $total_worksheets; $i++) {
+            // Write a Name record if the print area has been defined
+            if (isset($this->_worksheets[$i]->print_rowmin)) {
+                $this->_storeNameShort(
+                    $this->_worksheets[$i]->index,
+                    0x06, // NAME type
+                    $this->_worksheets[$i]->print_rowmin,
+                    $this->_worksheets[$i]->print_rowmax,
+                    $this->_worksheets[$i]->print_colmin,
+                    $this->_worksheets[$i]->print_colmax
+                    );
+            }
+        }
+
+        // Create the print title NAME records
+        $total_worksheets = count($this->_worksheets);
+        for ($i = 0; $i < $total_worksheets; $i++) {
+            $rowmin = $this->_worksheets[$i]->title_rowmin;
+            $rowmax = $this->_worksheets[$i]->title_rowmax;
+            $colmin = $this->_worksheets[$i]->title_colmin;
+            $colmax = $this->_worksheets[$i]->title_colmax;
+
+            // Determine if row + col, row, col or nothing has been defined
+            // and write the appropriate record
+            //
+            if (isset($rowmin) && isset($colmin)) {
+                // Row and column titles have been defined.
+                // Row title has been defined.
+                $this->_storeNameLong(
+                    $this->_worksheets[$i]->index,
+                    0x07, // NAME type
+                    $rowmin,
+                    $rowmax,
+                    $colmin,
+                    $colmax
+                    );
+            } elseif (isset($rowmin)) {
+                // Row title has been defined.
+                $this->_storeNameShort(
+                    $this->_worksheets[$i]->index,
+                    0x07, // NAME type
+                    $rowmin,
+                    $rowmax,
+                    0x00,
+                    0xff
+                    );
+            } elseif (isset($colmin)) {
+                // Column title has been defined.
+                $this->_storeNameShort(
+                    $this->_worksheets[$i]->index,
+                    0x07, // NAME type
+                    0x0000,
+                    0x3fff,
+                    $colmin,
+                    $colmax
+                    );
+            } else {
+                // Print title hasn't been defined.
+            }
+        }
+    }
+
+
+
+
+    /******************************************************************************
+    *
+    * BIFF RECORDS
+    *
+    */
+
+    /**
+    * Stores the CODEPAGE biff record.
+    *
+    * @access private
+    */
+    function _storeCodepage()
+    {
+        $record          = 0x0042;             // Record identifier
+        $length          = 0x0002;             // Number of bytes to follow
+        $cv              = $this->_codepage;   // The code page
+
+        $header          = pack('vv', $record, $length);
+        $data            = pack('v',  $cv);
+
+        $this->_append($header . $data);
+    }
+
+    /**
+    * Write Excel BIFF WINDOW1 record.
+    *
+    * @access private
+    */
+    function _storeWindow1()
+    {
+        $record    = 0x003D;                 // Record identifier
+        $length    = 0x0012;                 // Number of bytes to follow
+
+        $xWn       = 0x0000;                 // Horizontal position of window
+        $yWn       = 0x0000;                 // Vertical position of window
+        $dxWn      = 0x25BC;                 // Width of window
+        $dyWn      = 0x1572;                 // Height of window
+
+        $grbit     = 0x0038;                 // Option flags
+        $ctabsel   = $this->_selected;       // Number of workbook tabs selected
+        $wTabRatio = 0x0258;                 // Tab to scrollbar ratio
+
+        $itabFirst = $this->_firstsheet;     // 1st displayed worksheet
+        $itabCur   = $this->_activesheet;    // Active worksheet
+
+        $header    = pack("vv",        $record, $length);
+        $data      = pack("vvvvvvvvv", $xWn, $yWn, $dxWn, $dyWn,
+                                       $grbit,
+                                       $itabCur, $itabFirst,
+                                       $ctabsel, $wTabRatio);
+        $this->_append($header . $data);
+    }
+
+    /**
+    * Writes Excel BIFF BOUNDSHEET record.
+    * FIXME: inconsistent with BIFF documentation
+    *
+    * @param string  $sheetname Worksheet name
+    * @param integer $offset    Location of worksheet BOF
+    * @access private
+    */
+    function _storeBoundsheet($sheetname,$offset)
+    {
+        $record    = 0x0085;                    // Record identifier
+/*        
+        if ($this->_BIFF_version == 0x0600)    // Tried to fix the correct handling here, with the
+        {                                                                              // corrected specification from M$ - Joe Hunt 2009-03-08
+               global $encoding_string;
+               if ($encoding_string == 'UTF-16LE')
+               {
+                   $strlen = function_exists('mb_strlen') ? mb_strlen($sheetname, 'UTF-16LE') : (strlen($sheetname) / 2);
+                   $encoding  = 0x1;
+               }
+               else if ($encoding_string != '')
+               {
+                   $sheetname = iconv($encoding_string, 'UTF-16LE', $sheetname);
+                   $strlen = function_exists('mb_strlen') ? mb_strlen($sheetname, 'UTF-16LE') : (strlen($sheetname) / 2);
+                   $encoding  = 0x1;
+               }
+               if ($strlen % 2 != 0)
+                       $strlen++;
+               $encoding  = 0x1;
+               
+            //$strlen = strlen($sheetname);
+            $length    = 0x08 + $strlen;               // Number of bytes to follow
+        } else {
+               $strlen = strlen($sheetname);
+            $length = 0x07 + $strlen;                  // Number of bytes to follow
+        }
+
+        $grbit     = 0x0000;                    // Visibility and sheet type
+        $cch       = $strlen;                          // Length of sheet name
+
+        $header    = pack("vv",  $record, $length);
+        if ($this->_BIFF_version == 0x0600) {
+            $data      = pack("VvCC", $offset, $grbit, $cch, $encoding);
+        } else {
+            $data      = pack("VvC", $offset, $grbit, $cch);
+        }
+*/        
+        if ($this->_BIFF_version == 0x0600) 
+        {
+            $strlen = strlen($sheetname);
+            $length    = 0x08 + $strlen;               // Number of bytes to follow
+        } else {
+               $strlen = strlen($sheetname);
+            $length = 0x07 + $strlen;                  // Number of bytes to follow
+        }
+
+        $grbit     = 0x0000;                    // Visibility and sheet type
+        $cch       = $strlen;                          // Length of sheet name
+
+        $header    = pack("vv",  $record, $length);
+        if ($this->_BIFF_version == 0x0600) {
+            $data      = pack("Vvv", $offset, $grbit, $cch);
+        } else {
+            $data      = pack("VvC", $offset, $grbit, $cch);
+        }
+        $this->_append($header.$data.$sheetname);
+    }
+
+    /**
+    * Write Internal SUPBOOK record
+    *
+    * @access private
+    */
+    function _storeSupbookInternal()
+    {
+        $record    = 0x01AE;   // Record identifier
+        $length    = 0x0004;   // Bytes to follow
+
+        $header    = pack("vv", $record, $length);
+        $data      = pack("vv", count($this->_worksheets), 0x0104);
+        $this->_append($header . $data);
+    }
+
+    /**
+    * Writes the Excel BIFF EXTERNSHEET record. These references are used by
+    * formulas.
+    *
+    * @param string $sheetname Worksheet name
+    * @access private
+    */
+    function _storeExternsheetBiff8()
+    {
+        $total_references = count($this->_parser->_references);
+        $record   = 0x0017;                     // Record identifier
+        $length   = 2 + 6 * $total_references;  // Number of bytes to follow
+
+        $supbook_index = 0;           // FIXME: only using internal SUPBOOK record
+        $header           = pack("vv",  $record, $length);
+        $data             = pack('v', $total_references);
+        for ($i = 0; $i < $total_references; $i++) {
+            $data .= $this->_parser->_references[$i];
+        }
+        $this->_append($header . $data);
+    }
+
+    /**
+    * Write Excel BIFF STYLE records.
+    *
+    * @access private
+    */
+    function _storeStyle()
+    {
+        $record    = 0x0293;   // Record identifier
+        $length    = 0x0004;   // Bytes to follow
+
+        $ixfe      = 0x8000;   // Index to style XF
+        $BuiltIn   = 0x00;     // Built-in style
+        $iLevel    = 0xff;     // Outline style level
+
+        $header    = pack("vv",  $record, $length);
+        $data      = pack("vCC", $ixfe, $BuiltIn, $iLevel);
+        $this->_append($header . $data);
+    }
+
+
+    /**
+    * Writes Excel FORMAT record for non "built-in" numerical formats.
+    *
+    * @param string  $format Custom format string
+    * @param integer $ifmt   Format index code
+    * @access private
+    */
+    function _storeNumFormat($format, $ifmt)
+    {
+        $record    = 0x041E;                      // Record identifier
+
+        if ($this->_BIFF_version == 0x0600) {
+            $length    = 5 + strlen($format);      // Number of bytes to follow
+            $encoding = 0x0;
+        } elseif ($this->_BIFF_version == 0x0500) {
+            $length    = 3 + strlen($format);      // Number of bytes to follow
+        }
+
+        $cch       = strlen($format);             // Length of format string
+
+        $header    = pack("vv", $record, $length);
+        if ($this->_BIFF_version == 0x0600) {
+            $data      = pack("vvC", $ifmt, $cch, $encoding);
+        } elseif ($this->_BIFF_version == 0x0500) {
+            $data      = pack("vC", $ifmt, $cch);
+        }
+        $this->_append($header . $data . $format);
+    }
+
+    /**
+    * Write DATEMODE record to indicate the date system in use (1904 or 1900).
+    *
+    * @access private
+    */
+    function _storeDatemode()
+    {
+        $record    = 0x0022;         // Record identifier
+        $length    = 0x0002;         // Bytes to follow
+
+        $f1904     = $this->_1904;   // Flag for 1904 date system
+
+        $header    = pack("vv", $record, $length);
+        $data      = pack("v", $f1904);
+        $this->_append($header . $data);
+    }
+
+
+    /**
+    * Write BIFF record EXTERNCOUNT to indicate the number of external sheet
+    * references in the workbook.
+    *
+    * Excel only stores references to external sheets that are used in NAME.
+    * The workbook NAME record is required to define the print area and the repeat
+    * rows and columns.
+    *
+    * A similar method is used in Worksheet.php for a slightly different purpose.
+    *
+    * @param integer $cxals Number of external references
+    * @access private
+    */
+    function _storeExterncount($cxals)
+    {
+        $record   = 0x0016;          // Record identifier
+        $length   = 0x0002;          // Number of bytes to follow
+
+        $header   = pack("vv", $record, $length);
+        $data     = pack("v",  $cxals);
+        $this->_append($header . $data);
+    }
+
+
+    /**
+    * Writes the Excel BIFF EXTERNSHEET record. These references are used by
+    * formulas. NAME record is required to define the print area and the repeat
+    * rows and columns.
+    *
+    * A similar method is used in Worksheet.php for a slightly different purpose.
+    *
+    * @param string $sheetname Worksheet name
+    * @access private
+    */
+    function _storeExternsheet($sheetname)
+    {
+        $record      = 0x0017;                     // Record identifier
+        $length      = 0x02 + strlen($sheetname);  // Number of bytes to follow
+
+        $cch         = strlen($sheetname);         // Length of sheet name
+        $rgch        = 0x03;                       // Filename encoding
+
+        $header      = pack("vv",  $record, $length);
+        $data        = pack("CC", $cch, $rgch);
+        $this->_append($header . $data . $sheetname);
+    }
+
+
+    /**
+    * Store the NAME record in the short format that is used for storing the print
+    * area, repeat rows only and repeat columns only.
+    *
+    * @param integer $index  Sheet index
+    * @param integer $type   Built-in name type
+    * @param integer $rowmin Start row
+    * @param integer $rowmax End row
+    * @param integer $colmin Start colum
+    * @param integer $colmax End column
+    * @access private
+    */
+    function _storeNameShort($index, $type, $rowmin, $rowmax, $colmin, $colmax)
+    {
+        $record          = 0x0018;       // Record identifier
+        $length          = 0x0024;       // Number of bytes to follow
+
+        $grbit           = 0x0020;       // Option flags
+        $chKey           = 0x00;         // Keyboard shortcut
+        $cch             = 0x01;         // Length of text name
+        $cce             = 0x0015;       // Length of text definition
+        $ixals           = $index + 1;   // Sheet index
+        $itab            = $ixals;       // Equal to ixals
+        $cchCustMenu     = 0x00;         // Length of cust menu text
+        $cchDescription  = 0x00;         // Length of description text
+        $cchHelptopic    = 0x00;         // Length of help topic text
+        $cchStatustext   = 0x00;         // Length of status bar text
+        $rgch            = $type;        // Built-in name type
+
+        $unknown03       = 0x3b;
+        $unknown04       = 0xffff-$index;
+        $unknown05       = 0x0000;
+        $unknown06       = 0x0000;
+        $unknown07       = 0x1087;
+        $unknown08       = 0x8005;
+
+        $header             = pack("vv", $record, $length);
+        $data               = pack("v", $grbit);
+        $data              .= pack("C", $chKey);
+        $data              .= pack("C", $cch);
+        $data              .= pack("v", $cce);
+        $data              .= pack("v", $ixals);
+        $data              .= pack("v", $itab);
+        $data              .= pack("C", $cchCustMenu);
+        $data              .= pack("C", $cchDescription);
+        $data              .= pack("C", $cchHelptopic);
+        $data              .= pack("C", $cchStatustext);
+        $data              .= pack("C", $rgch);
+        $data              .= pack("C", $unknown03);
+        $data              .= pack("v", $unknown04);
+        $data              .= pack("v", $unknown05);
+        $data              .= pack("v", $unknown06);
+        $data              .= pack("v", $unknown07);
+        $data              .= pack("v", $unknown08);
+        $data              .= pack("v", $index);
+        $data              .= pack("v", $index);
+        $data              .= pack("v", $rowmin);
+        $data              .= pack("v", $rowmax);
+        $data              .= pack("C", $colmin);
+        $data              .= pack("C", $colmax);
+        $this->_append($header . $data);
+    }
+
+
+    /**
+    * Store the NAME record in the long format that is used for storing the repeat
+    * rows and columns when both are specified. This shares a lot of code with
+    * _storeNameShort() but we use a separate method to keep the code clean.
+    * Code abstraction for reuse can be carried too far, and I should know. ;-)
+    *
+    * @param integer $index Sheet index
+    * @param integer $type  Built-in name type
+    * @param integer $rowmin Start row
+    * @param integer $rowmax End row
+    * @param integer $colmin Start colum
+    * @param integer $colmax End column
+    * @access private
+    */
+    function _storeNameLong($index, $type, $rowmin, $rowmax, $colmin, $colmax)
+    {
+        $record          = 0x0018;       // Record identifier
+        $length          = 0x003d;       // Number of bytes to follow
+        $grbit           = 0x0020;       // Option flags
+        $chKey           = 0x00;         // Keyboard shortcut
+        $cch             = 0x01;         // Length of text name
+        $cce             = 0x002e;       // Length of text definition
+        $ixals           = $index + 1;   // Sheet index
+        $itab            = $ixals;       // Equal to ixals
+        $cchCustMenu     = 0x00;         // Length of cust menu text
+        $cchDescription  = 0x00;         // Length of description text
+        $cchHelptopic    = 0x00;         // Length of help topic text
+        $cchStatustext   = 0x00;         // Length of status bar text
+        $rgch            = $type;        // Built-in name type
+
+        $unknown01       = 0x29;
+        $unknown02       = 0x002b;
+        $unknown03       = 0x3b;
+        $unknown04       = 0xffff-$index;
+        $unknown05       = 0x0000;
+        $unknown06       = 0x0000;
+        $unknown07       = 0x1087;
+        $unknown08       = 0x8008;
+
+        $header             = pack("vv",  $record, $length);
+        $data               = pack("v", $grbit);
+        $data              .= pack("C", $chKey);
+        $data              .= pack("C", $cch);
+        $data              .= pack("v", $cce);
+        $data              .= pack("v", $ixals);
+        $data              .= pack("v", $itab);
+        $data              .= pack("C", $cchCustMenu);
+        $data              .= pack("C", $cchDescription);
+        $data              .= pack("C", $cchHelptopic);
+        $data              .= pack("C", $cchStatustext);
+        $data              .= pack("C", $rgch);
+        $data              .= pack("C", $unknown01);
+        $data              .= pack("v", $unknown02);
+        // Column definition
+        $data              .= pack("C", $unknown03);
+        $data              .= pack("v", $unknown04);
+        $data              .= pack("v", $unknown05);
+        $data              .= pack("v", $unknown06);
+        $data              .= pack("v", $unknown07);
+        $data              .= pack("v", $unknown08);
+        $data              .= pack("v", $index);
+        $data              .= pack("v", $index);
+        $data              .= pack("v", 0x0000);
+        $data              .= pack("v", 0x3fff);
+        $data              .= pack("C", $colmin);
+        $data              .= pack("C", $colmax);
+        // Row definition
+        $data              .= pack("C", $unknown03);
+        $data              .= pack("v", $unknown04);
+        $data              .= pack("v", $unknown05);
+        $data              .= pack("v", $unknown06);
+        $data              .= pack("v", $unknown07);
+        $data              .= pack("v", $unknown08);
+        $data              .= pack("v", $index);
+        $data              .= pack("v", $index);
+        $data              .= pack("v", $rowmin);
+        $data              .= pack("v", $rowmax);
+        $data              .= pack("C", 0x00);
+        $data              .= pack("C", 0xff);
+        // End of data
+        $data              .= pack("C", 0x10);
+        $this->_append($header . $data);
+    }
+
+    /**
+    * Stores the COUNTRY record for localization
+    *
+    * @access private
+    */
+    function _storeCountry()
+    {
+        $record          = 0x008C;    // Record identifier
+        $length          = 4;         // Number of bytes to follow
+
+        $header = pack('vv',  $record, $length);
+        /* using the same country code always for simplicity */
+        $data = pack('vv', $this->_country_code, $this->_country_code);
+        $this->_append($header . $data);
+    }
+
+    /**
+    * Stores the PALETTE biff record.
+    *
+    * @access private
+    */
+    function _storePalette()
+    {
+        $aref            = $this->_palette;
+
+        $record          = 0x0092;                 // Record identifier
+        $length          = 2 + 4 * count($aref);   // Number of bytes to follow
+        $ccv             =         count($aref);   // Number of RGB values to follow
+        $data = '';                                // The RGB data
+
+        // Pack the RGB data
+        foreach ($aref as $color) {
+            foreach ($color as $byte) {
+                $data .= pack("C",$byte);
+            }
+        }
+
+        $header = pack("vvv",  $record, $length, $ccv);
+        $this->_append($header . $data);
+    }
+
+    /**
+    * Calculate
+    * Handling of the SST continue blocks is complicated by the need to include an
+    * additional continuation byte depending on whether the string is split between
+    * blocks or whether it starts at the beginning of the block. (There are also
+    * additional complications that will arise later when/if Rich Strings are
+    * supported).
+    *
+    * @access private
+    */
+    function _calculateSharedStringsSizes()
+    {
+        /* Iterate through the strings to calculate the CONTINUE block sizes.
+           For simplicity we use the same size for the SST and CONTINUE records:
+           8228 : Maximum Excel97 block size
+             -4 : Length of block header
+             -8 : Length of additional SST header information
+         = 8216
+        */
+        $continue_limit     = 8216;
+        $block_length       = 0;
+        $written            = 0;
+        $this->_block_sizes = array();
+        $continue           = 0;
+
+        foreach (array_keys($this->_str_table) as $string) {
+            $string_length = strlen($string);
+
+            // Block length is the total length of the strings that will be
+            // written out in a single SST or CONTINUE block.
+            $block_length += $string_length;
+
+            // We can write the string if it doesn't cross a CONTINUE boundary
+            if ($block_length < $continue_limit) {
+                $written      += $string_length;
+                continue;
+            }
+
+            // Deal with the cases where the next string to be written will exceed
+            // the CONTINUE boundary. If the string is very long it may need to be
+            // written in more than one CONTINUE record.
+            while ($block_length >= $continue_limit) {
+
+                // We need to avoid the case where a string is continued in the first
+                // n bytes that contain the string header information.
+                $header_length   = 3; // Min string + header size -1
+                $space_remaining = $continue_limit - $written - $continue;
+
+
+                /* TODO: Unicode data should only be split on char (2 byte)
+                boundaries. Therefore, in some cases we need to reduce the
+                amount of available
+                */
+
+                if ($space_remaining > $header_length) {
+                    // Write as much as possible of the string in the current block
+                    $written      += $space_remaining;
+
+                    // Reduce the current block length by the amount written
+                    $block_length -= $continue_limit - $continue;
+
+                    // Store the max size for this block
+                    $this->_block_sizes[] = $continue_limit;
+
+                    // If the current string was split then the next CONTINUE block
+                    // should have the string continue flag (grbit) set unless the
+                    // split string fits exactly into the remaining space.
+                    if ($block_length > 0) {
+                        $continue = 1;
+                    } else {
+                        $continue = 0;
+                    }
+                } else {
+                    // Store the max size for this block
+                    $this->_block_sizes[] = $written + $continue;
+
+                    // Not enough space to start the string in the current block
+                    $block_length -= $continue_limit - $space_remaining - $continue;
+                    $continue = 0;
+
+                }
+
+                // If the string (or substr) is small enough we can write it in the
+                // new CONTINUE block. Else, go through the loop again to write it in
+                // one or more CONTINUE blocks
+                if ($block_length < $continue_limit) {
+                    $written = $block_length;
+                } else {
+                    $written = 0;
+                }
+            }
+        }
+
+        // Store the max size for the last block unless it is empty
+        if ($written + $continue) {
+            $this->_block_sizes[] = $written + $continue;
+        }
+
+
+        /* Calculate the total length of the SST and associated CONTINUEs (if any).
+         The SST record will have a length even if it contains no strings.
+         This length is required to set the offsets in the BOUNDSHEET records since
+         they must be written before the SST records
+        */
+        $total_offset = array_sum($this->_block_sizes);
+        // SST information
+        $total_offset += 8;
+        if (!empty($this->_block_sizes)) {
+            $total_offset += (count($this->_block_sizes)) * 4; // add CONTINUE headers
+        }
+        return $total_offset;
+    }
+
+    /**
+    * Write all of the workbooks strings into an indexed array.
+    * See the comments in _calculate_shared_string_sizes() for more information.
+    *
+    * The Excel documentation says that the SST record should be followed by an
+    * EXTSST record. The EXTSST record is a hash table that is used to optimise
+    * access to SST. However, despite the documentation it doesn't seem to be
+    * required so we will ignore it.
+    *
+    * @access private
+    */
+    function _storeSharedStringsTable()
+    {
+        $record  = 0x00fc;  // Record identifier
+        // sizes are upside down
+        $this->_block_sizes = array_reverse($this->_block_sizes);
+        $length = array_pop($this->_block_sizes) + 8; // First block size plus SST information
+
+        // Write the SST block header information
+        $header      = pack("vv", $record, $length);
+        $data        = pack("VV", $this->_str_total, $this->_str_unique);
+        $this->_append($header . $data);
+
+
+        // Iterate through the strings to calculate the CONTINUE block sizes
+        $continue_limit = 8216;
+        $block_length   = 0;
+        $written        = 0;
+        $continue       = 0;
+
+
+        /* TODO: not good for performance */
+        foreach (array_keys($this->_str_table) as $string) {
+
+            $string_length = strlen($string);
+            $encoding      = 0; // assume there are no Unicode strings
+            $split_string  = 0;
+
+            // Block length is the total length of the strings that will be
+            // written out in a single SST or CONTINUE block.
+            //
+            $block_length += $string_length;
+
+
+            // We can write the string if it doesn't cross a CONTINUE boundary
+            if ($block_length < $continue_limit) {
+                $this->_append($string);
+                $written += $string_length;
+                continue;
+            }
+
+            // Deal with the cases where the next string to be written will exceed
+            // the CONTINUE boundary. If the string is very long it may need to be
+            // written in more than one CONTINUE record.
+            //
+            while ($block_length >= $continue_limit) {
+
+                // We need to avoid the case where a string is continued in the first
+                // n bytes that contain the string header information.
+                //
+                $header_length   = 3; // Min string + header size -1
+                $space_remaining = $continue_limit - $written - $continue;
+
+
+                // Unicode data should only be split on char (2 byte) boundaries.
+                // Therefore, in some cases we need to reduce the amount of available
+
+                if ($space_remaining > $header_length) {
+                    // Write as much as possible of the string in the current block
+                    $tmp = substr($string, 0, $space_remaining);
+                    $this->_append($tmp);
+
+                    // The remainder will be written in the next block(s)
+                    $string = substr($string, $space_remaining);
+
+                    // Reduce the current block length by the amount written
+                    $block_length -= $continue_limit - $continue;
+
+                    // If the current string was split then the next CONTINUE block
+                    // should have the string continue flag (grbit) set unless the
+                    // split string fits exactly into the remaining space.
+                    //
+                    if ($block_length > 0) {
+                        $continue = 1;
+                    } else {
+                        $continue = 0;
+                    }
+                } else {
+                    // Not enough space to start the string in the current block
+                    $block_length -= $continue_limit - $space_remaining - $continue;
+                    $continue = 0;
+                }
+
+                // Write the CONTINUE block header
+                if (!empty($this->_block_sizes)) {
+                    $record  = 0x003C;
+                    $length  = array_pop($this->_block_sizes);
+                    $header  = pack('vv', $record, $length);
+                    if ($continue) {
+                        $header .= pack('C', $encoding);
+                    }
+                    $this->_append($header);
+                }
+
+                // If the string (or substr) is small enough we can write it in the
+                // new CONTINUE block. Else, go through the loop again to write it in
+                // one or more CONTINUE blocks
+                //
+                if ($block_length < $continue_limit) {
+                    $this->_append($string);
+                    $written = $block_length;
+                } else {
+                    $written = 0;
+                }
+            }
+        }
+    }
+}
+?>
diff --git a/reporting/includes/excel_report.inc b/reporting/includes/excel_report.inc
new file mode 100644 (file)
index 0000000..0f25ea0
--- /dev/null
@@ -0,0 +1,424 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+/* $Revision$ */
+$page_security = 8;
+include_once($path_to_root . "/reporting/includes/Workbook.php");
+include_once($path_to_root . "/admin/db/company_db.inc");
+include_once($path_to_root . "/config.php");
+// xls version
+class FrontReport extends Spreadsheet_Excel_Writer_Workbook
+{
+       var $size;
+       var $company;
+       var $user;
+       var $host;
+       var $fiscal_year;
+       var $title;
+       var $filename;
+       var $unique_name;
+       var $path;
+       var $code;
+       var $bottomMargin = 0;
+       var $lineHeight;
+       var $leftMargin = 0;
+
+       var $cols;
+       var $params;
+       var $headers;
+       var $aligns;
+       var $headers2;
+       var $aligns2;
+       var $cols2;
+       var $fontSize;
+       var $oldFontSize;
+       var $currency;
+       var $row = 9999999;
+       var $y;
+       var $numcols;
+       
+       var $formatTitle;
+       var $formatDateTime;
+       var $formatDate;
+       var $formatHeaderLeft;
+       var $formatHeaderRight;
+       var $formatFooter;
+       var $formatAmount = array();
+       
+       var $sheet;
+
+       function FrontReport($title, $filename, $size = 'A4', $fontsize = 9)
+       {
+               global $comp_path, $dateseps;
+               
+               $this->size = $size;
+               $this->title = $title;
+               $this->lineHeight = 12;
+               $this->fontSize = $fontsize;
+               $this->oldFontSize = 0;
+               $this->y = 1;
+               $this->currency = '';
+               $rtl = ($_SESSION['language']->dir == 'rtl');
+               $this->code = strtolower($_SESSION['language']->encoding);
+               $this->filename = $filename.".xls";
+               $this->unique_name = uniqid('').".xls";
+               $this->path = $comp_path.'/'.user_company(). '/pdf_files';
+               $this->Spreadsheet_Excel_Writer_Workbook($this->path."/".$this->unique_name);
+               //$this->setCountry(48);
+               if ($this->code != "iso-8859-1")
+                       $this->setVersion(8); // set biff version to 8 (0x0006 internal)
+               $this->sheet =& $this->addWorksheet($this->title);
+               if ($this->code != "iso-8859-1")
+                       $this->sheet->setInputEncoding($this->code); // set sheet encoding
+               if ($rtl)
+                       $this->sheet->setRTL();
+               $this->formatTitle =& $this->addFormat();       
+               $this->formatTitle->setSize(16);
+               $this->formatTitle->setBold();
+               $this->formatTitle->setAlign($rtl ? 'right' : 'left');
+               $this->formatTitle->setTop(2);
+               $this->formatTitle->setTopColor('gray');
+
+               $how = user_date_format();
+               $sep = $dateseps[user_date_sep()];
+               if ($sep == '.')
+                       $sep = "\\.";
+               if ($how == 0)
+               {
+                       $dateformat_long = "mm{$sep}dd{$sep}yyyy\ \ hh:mm\ am/pm";
+                       $dateformat = "mm{$sep}dd{$sep}yyyy";
+               }       
+               elseif ($how == 1)      
+               {
+                       $dateformat_long = "dd{$sep}mm{$sep}yyyy\ \ hh:mm";
+                       $dateformat = "dd{$sep}mm{$sep}yyyy";
+               }       
+               else    
+               {
+                       $dateformat_long = "yyyy{$sep}mm{$sep}dd\ \ hh:mm";
+                       $dateformat = "yyyy{$sep}mm{$sep}dd";
+               }       
+               $this->formatDateTime =& $this->addFormat();
+               $this->formatDateTime->setNumFormat($dateformat_long);
+               $this->formatDateTime->setAlign($rtl ? 'right' : 'left');
+               $this->formatDate =& $this->addFormat();
+               $this->formatDate->setNumFormat($dateformat);
+               $this->formatDate->setAlign($rtl ? 'right' : 'left');
+               $this->formatRight =& $this->addFormat();
+               $this->formatRight->setAlign($rtl ? 'left' : 'right');
+               $this->formatLeft =& $this->addFormat();
+               $this->formatLeft->setAlign($rtl ? 'right' : 'left');
+               
+               $this->formatHeaderLeft =& $this->addFormat();
+               $this->formatHeaderLeft->setItalic();
+               $this->formatHeaderLeft->setTop(2);
+               $this->formatHeaderLeft->setTopColor('gray');
+               $this->formatHeaderLeft->setBottom(2);
+               $this->formatHeaderLeft->setBottomColor('gray');
+               $this->formatHeaderLeft->setAlign('vcenter');
+               $this->formatDate->setAlign($rtl ? 'right' : 'left');
+               $this->formatHeaderRight =& $this->addFormat();
+               $this->formatHeaderRight->setItalic();
+               $this->formatHeaderRight->setTop(2);
+               $this->formatHeaderRight->setTopColor('gray');
+               $this->formatHeaderRight->setBottom(2);
+               $this->formatHeaderRight->setBottomColor('gray');
+               $this->formatHeaderRight->setAlign('vcenter');
+               $this->formatHeaderRight->setAlign('right');
+               $this->formatFooter =& $this->addFormat();
+               $this->formatFooter->setTop(2);
+               $this->formatFooter->setTopColor('gray');
+       }
+       
+       function NumFormat($dec) 
+       {
+               if (!isset($this->formatAmount[$dec]))
+               {
+                       global $thoseps,$decseps;
+                       $dec = (int)$dec;
+                       $tsep = $thoseps[user_tho_sep()];
+                       $dsep = $decseps[user_dec_sep()];
+                       $format = "###{$tsep}###{$tsep}###{$tsep}##0";
+                       if ($dec>0)
+                               $format .= "{$dsep}".str_repeat('0',$dec);
+                       $this->formatAmount[$dec] =& $this->addFormat();
+                       $this->formatAmount[$dec]->setNumFormat($format);
+                       $this->formatAmount[$dec]->setAlign('right');
+               }
+               return $this->formatAmount[$dec];
+       }
+
+       function Font($style = 'normal')
+       {
+       }
+
+       function Info($params, $cols, $headers, $aligns,
+               $cols2 = null, $headers2 = null, $aligns2 = null)
+       {
+               global $app_title, $version, $power_by, $power_url;
+               $this->company = get_company_prefs();
+               $year = get_current_fiscalyear();
+               if ($year['closed'] == 0)
+                       $how = _("Active");
+               else
+                       $how = _("Closed");
+               $this->fiscal_year = sql2date($year['begin']) . " - " . sql2date($year['end']) . "  " . "(" . $how . ")";
+               $this->user = $_SESSION["wa_current_user"]->name;
+               $this->host = $_SERVER['SERVER_NAME'];
+               $this->params = $params;
+               $this->cols = $cols;
+               $this->headers = $headers;
+               $this->aligns = $aligns;
+               $this->cols2 = $cols2;
+               $this->headers2 = $headers2;
+               $this->aligns2 = $aligns2;
+               $this->numcols = count($this->headers);
+               $tcols = count($this->headers2);
+               if ($tcols > $this->numcols)
+                       $this->numcols = $tcols;
+               for ($i = 0; $i < $this->numcols; $i++)
+                       $this->sheet->setColumn($i, $i, $this->px2units($this->cols[$i + 1] - $this->cols[$i]));
+       }
+
+       function Header()
+       {
+               $this->y = 0;
+               $tcol = $this->numcols - 1;
+               $this->sheet->setRow($this->y, 20);
+               for ($i = 0; $i < $this->numcols; $i++)
+                       $this->sheet->writeBlank($this->y, $i, $this->formatTitle);
+               $this->sheet->writeString($this->y, 0, $this->title, $this->formatTitle);
+               $this->sheet->mergeCells($this->y, 0, $this->y, $tcol);
+               $this->NewLine();
+               $str = _("Print Out Date") . ':';
+               $this->sheet->writeString($this->y, 0, $str, $this->formatLeft);
+               $this->sheet->writeString($this->y, 1, Today() . "  ".Now(), $this->formatLeft);
+               $this->sheet->writeString($this->y, $tcol-1, $this->company['coy_name'], $this->formatLeft);
+               $this->sheet->mergeCells($this->y, $tcol-1, $this->y, $tcol);
+               $this->NewLine();
+               $str = _("Fiscal Year") . ':';
+               $this->sheet->writeString($this->y, 0, $str, $this->formatLeft);
+               $str = $this->fiscal_year;
+               $this->sheet->writeString($this->y, 1, $str, $this->formatLeft);
+               $this->sheet->writeString($this->y, $tcol-1, $this->host, $this->formatLeft);
+               $this->sheet->mergeCells($this->y, $tcol-1, $this->y, $tcol);
+               for ($i = 1; $i < count($this->params); $i++)
+               {
+                       if ($this->params[$i]['from'] != '')
+                       {
+                               $this->NewLine();
+                               $str = $this->params[$i]['text'] . ':';
+                               $this->sheet->writeString($this->y, 0, $str);
+                               $str = $this->params[$i]['from'];
+                               if ($this->params[$i]['to'] != '')
+                                       $str .= " - " . $this->params[$i]['to'];
+                               $this->sheet->writeString($this->y, 1, $str, $this->formatLeft);
+                               if ($i == 1)
+                               {
+                                       $this->sheet->writeString($this->y, $tcol-1, $this->user, $this->formatLeft);
+                                       $this->sheet->mergeCells($this->y, $tcol-1, $this->y, $tcol);
+                               }       
+                       }
+               }
+               if ($this->params[0] != '') // Comments
+               {
+                       $this->NewLine();
+                       $str = _("Comments") . ':';
+                       $this->sheet->writeString($this->y, 0, $str, $this->formatLeft);
+                       $this->sheet->writeString($this->y, 1, $this->params[0], $this->formatLeft);
+               }
+               $this->NewLine();
+               if ($this->headers2 != null)
+               {
+                       for ($i = 0, $j = 0; $i < $this->numcols; $i++)
+                       {
+                               if ($this->cols2[$j] >= $this->cols[$i] && $this->cols2[$j] <= $this->cols[$i + 1])
+                               {
+                                       if ($this->aligns2[$j] == "right")
+                                               $this->sheet->writeString($this->y, $i, $this->headers2[$j], $this->formatHeaderRight);
+                                       else    
+                                               $this->sheet->writeString($this->y, $i, $this->headers2[$j], $this->formatHeaderLeft);
+                                       $j++;   
+                               }
+                               else
+                                       $this->sheet->writeString($this->y, $i, "", $this->formatHeaderLeft);
+                       }               
+                       $this->NewLine();
+               }
+
+               for ($i = 0; $i < $this->numcols; $i++)
+               {
+                       if (!isset($this->headers[$i]))
+                               $header = "";
+                       else
+                               $header = $this->headers[$i];
+                       if ($this->aligns[$i] == "right")
+                               $this->sheet->writeString($this->y, $i, $header, $this->formatHeaderRight);
+                       else    
+                               $this->sheet->writeString($this->y, $i, $header, $this->formatHeaderLeft);
+               }
+               $this->NewLine();
+       }
+
+       function Header2($myrow, $branch, $sales_order, $bankaccount, $doctype)
+       {
+               return;
+       }
+
+       function AddImage($logo, $x, $y, $w, $h)
+       {
+               return;
+       }
+
+       function SetDrawColor($r, $g, $b)
+       {
+               return;
+       }
+
+       function SetTextColor($r, $g, $b)
+       {
+               return;
+       }
+
+       function Text($c, $txt, $n=0, $corr=0, $r=0)
+       {
+               return;
+       }
+
+       function TextWrap($xpos, $ypos, $len, $str, $align = 'left')
+       {
+               return;
+       }
+
+       function TextCol($c, $n, $txt, $corr=0, $r=0)
+       {
+               if ($this->aligns[$c] == 'right')
+                       $this->sheet->writeString($this->y, $c, $txt, $this->formatRight);
+               else    
+                       $this->sheet->writeString($this->y, $c, $txt, $this->formatLeft);
+               if ($n - $c > 1)
+                       $this->sheet->mergeCells($this->y, $c, $this->y, $n - 1);
+       }
+
+       function AmountCol($c, $n, $txt, $dec=0, $corr=0, $r=0) 
+       { 
+               if (!is_numeric($txt))
+                       $txt = 0;
+               $this->sheet->writeNumber($this->y, $c, $txt, $this->NumFormat($dec)); 
+       }
+       
+       function DateCol($c, $n, $txt, $conv=false, $corr=0, $r=0) 
+       {
+               if (!$conv)
+                       $txt = date2sql($txt);
+               list($year, $mo, $day) = explode("-", $txt);    
+               $date = $this->ymd2date((int)$year, (int)$mo, (int)$day);
+               $this->sheet->writeNumber($this->y, $c, $date, $this->formatDate);
+       }
+
+       function TextCol2($c, $n, $txt, $corr=0, $r=0)
+       {
+               $this->sheet->writeString($this->y, $c, $txt, $this->formatLeft);
+               if ($n - $c > 1)
+                       $this->sheet->mergeCells($this->y, $c, $this->y, $n - 1);
+       }
+
+       function TextColLines($c, $n, $txt, $corr=0, $r=0)
+       {
+               return;
+       }
+
+       function TextWrapLines($c, $width, $txt, $align='left')
+       {
+               return;
+       }
+
+       function LineTo($from, $row, $to, $row2)
+       {
+               return;
+       }
+
+       function Line($row, $height = 0)
+       {
+               return;
+       }
+
+       function NewLine($l=1, $np=0)
+       {
+               $this->y += $l;
+       }
+
+       function ymd2Date($year, $mon, $day) // XLS internal date representation is a number between 1900-01-01 and 2078-12-31
+       {                                                                               // if we need the time part too, we have to add this value after a decimalpoint.
+       $mo = array(0,31,28,31,30,31,30,31,31,30,31,30,31);
+       $BASE = 1900;
+               $MAXYEAR = 2075;
+               if (($year % 4) == 0)
+               $mo[2]++;
+       if ($mon < 1)
+           $mon = 1;
+       elseif ($mon > 12)
+           $mon = 12;
+       if ($day < 1)
+           $day = 1;
+       elseif ($day > $mo[$mon])
+           $day = $mo[$mon];
+       if ($year < $BASE)
+           $year = $BASE;
+       elseif ($year > $MAXYEAR)
+           $year = $MAXYEAR;
+       $jul = (int)$day;
+       for ($n = 1; $n < $mon; $n++)
+       {
+           $jul += $mo[$n];
+       }
+       for ($n = $BASE; $n < $year; $n++)
+       {
+           $jul += 365;
+           if (($n % 4) == 0)
+               $jul++;
+       }
+       return $jul;
+       }
+  
+       function px2units($px) // XLS app conversion. Not bulletproof.
+       {
+               $excel_column_width_factor = 256;
+               $unit_offset_length = 6.5;
+               return ($px / $unit_offset_length);
+       }       
+
+       function End($email=0, $subject=null, $myrow=null, $doctype = 0)
+       {
+               for ($i = 0; $i < $this->numcols; $i++)
+                       $this->sheet->writeBlank($this->y, $i, $this->formatFooter);
+               $this->sheet->mergeCells($this->y, 0, $this->y, $this->numcols - 1);
+               $this->close();
+               // first have a look through the directory, 
+               // and remove old temporary pdfs
+               if ($d = @opendir($this->path)) {
+                       while (($file = readdir($d)) !== false) {
+                               if (!is_file($this->path.'/'.$file) || $file == 'index.php') continue;
+                               // then check to see if this one is too old
+                               $ftime = filemtime($this->path.'/'.$file);
+                               // seems 3 min is enough for any report download, isn't it?
+                               if (time()-$ftime > 180){
+                                       unlink($this->path.'/'.$file);
+                               }
+                       }
+                       closedir($d);
+               }
+               meta_forward($_SERVER['PHP_SELF'], "xls=1&filename=$this->filename&unique=$this->unique_name");
+               exit();
+       }
+}
+
+?>
\ No newline at end of file
diff --git a/reporting/includes/printer_class.inc b/reporting/includes/printer_class.inc
new file mode 100644 (file)
index 0000000..1b5a14e
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+/*
+ * remote_printer class.
+ * All needed filters should be set for the printer in printercap file.
+ * Based on PrintSendLPR class by Mick Sear, eCreate
+ */
+class remote_printer {
+
+    var $host;
+    var $port;
+    var $timeout;
+       var $queue;
+       //
+       //      Setting connection parameters
+       //
+    function remote_printer($queue, $host='', $port=515, $timeout=20){
+               if ($host == '')
+                       $host = $_SERVER['REMOTE_ADDR']; // default is user's host
+        $this->host = $host;
+        $this->port = $port;
+        $this->timeout = $timeout;
+        $this->queue = $queue;
+    }
+       //
+       //      Send file to remote network printer.
+       // 
+    function print_file($fname){
+        
+               $queue = $this->queue;
+
+        //Private static function prints waiting jobs on the queue.
+        $ret = $this->flush_queue($queue);
+//             if($ret) return $ret;
+
+        //Open a new connection to send the control file and data.
+               $stream = fsockopen("tcp://".$this->host, $this->port, $errNo, $errStr, $this->timeout);
+        if(!$stream){
+            return _('Cannot open connection to printer');
+        }
+               if (!isset($_SESSION['_print_job'])) {
+                       $_SESSION['print_job'] = 0;
+               }
+        $job = $_SESSION['print_job']++;
+            //Set printer to receive file
+        fwrite($stream, chr(2).$queue."\n");
+               $ack = fread($stream, 1);
+        if ($ack != 0) {
+                       fclose($stream);
+               return _('Printer does not acept the job').' ('.ord($ack).')';
+               }
+            
+        // Send Control file.
+        $server = $_SERVER['SERVER_NAME'];
+        $ctrl = "H".$server."\nP". substr($_SESSION["wa_current_user"]->loginname,0,31) 
+                       ."\nfdfA".$job.$server."\n";
+        fwrite($stream, chr(2).strlen($ctrl)." cfA".$job.$server."\n");
+        $ack = fread($stream, 1);
+        if ($ack != 0) {
+                       fclose($stream);
+            return _('Error sending print job control file').' ('.ord($ack).')';
+               }
+           
+        fwrite($stream, $ctrl.chr(0)); //Write null to indicate end of stream
+        $ack = fread($stream, 1);
+        if ($ack != 0) {
+                       fclose($stream);
+               return _('Print control file not accepted').' ('.ord($ack).')';
+               }
+                       
+        $data = fopen($fname, "rb");    
+        fwrite($stream, chr(3).filesize($fname)." dfA".$job.$server."\n");
+        $ack = fread($stream, 1);
+        if ($ack != 0) {
+                       fclose($stream);
+               return _('Cannot send report to printer').' ('.ord($ack).')';
+               }
+                
+        while(!feof($data)){
+               if (fwrite($stream, fread($data, 8192))<8192) break;
+        }
+        fwrite($stream, chr(0)); //Write null to indicate end of stream
+        $ack = fread($stream, 1);
+        if ($ack != 0) {
+                       fclose($stream);
+            return _('No ack after report printout').' ('.ord($ack).')';
+               }
+             
+        fclose($data);                
+               fclose($stream);
+
+               return '';
+    }
+    //
+       //      Print all waiting jobs on remote printer queue.
+       //
+    function flush_queue($queue){
+               $stream = fsockopen("tcp://".$this->host, $this->port,$errNo, $errStr, $this->timeout);
+        if (!$stream){
+            return _('Cannot flush printing queue');
+                       // .':<br>' . $errNo." (".$errStr.")"; return 0 (success) even on failure
+        } else {
+            //Print any waiting jobs
+            fwrite($stream, chr(1).$queue."\n");            
+            while(!feof($stream)){
+               fread($stream, 1);
+            }
+        }
+               return false;
+    }
+
+}
+
+?>
diff --git a/reporting/prn_redirect.php b/reporting/prn_redirect.php
new file mode 100644 (file)
index 0000000..347e9ed
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+/*
+       Print request redirector. This file is fired via print link or 
+       print button in reporting module. 
+*/
+$path_to_root = "..";
+$page_security = 2;    // this level is later overriden in rep file
+include_once($path_to_root . "/includes/session.inc");
+
+if (isset($_GET['xls']))
+{
+       $filename = $_GET['filename'];
+       $unique_name = $_GET['unique'];
+       $path =  $comp_path.'/'.user_company(). '/pdf_files/';
+       header("Content-type: application/vnd.ms-excel");
+       header("Content-Disposition: attachment; filename=$filename" );
+       header("Expires: 0");
+       header("Cache-Control: must-revalidate, post-check=0,pre-check=0");
+       header("Pragma: public");
+       echo file_get_contents($path.$unique_name);
+       exit();
+}
+elseif (isset($_GET['xml']))
+{
+       $filename = $_GET['filename'];
+       $unique_name = $_GET['unique'];
+       $path =  $comp_path.'/'.user_company(). '/pdf_files/';
+       header("content-type: text/xml");
+       header("Content-Disposition: attachment; filename=$filename");
+       header("Expires: 0");
+       header("Cache-Control: must-revalidate, post-check=0,pre-check=0");
+       header("Pragma: public");
+       echo file_get_contents($path.$unique_name);
+       exit();
+}
+       
+if (!isset($_POST['REP_ID'])) {        // print link clicked
+       $def_pars = array(0, 0, '', '', 0, '', '', 0); //default values
+       $rep = $_POST['REP_ID'] = $_GET['REP_ID'];
+       for($i=0; $i<8; $i++) {
+               $_POST['PARAM_'.$i] = isset($_GET['PARAM_'.$i]) 
+                       ? $_GET['PARAM_'.$i] : $def_pars[$i];
+       }
+}
+$rep = $_POST['REP_ID'];
+$rep_file = $comp_path.'/'.user_company()."/reporting/rep$rep.php";
+if (!file_exists($rep_file)) {
+    $rep_file = $path_to_root ."/reporting/rep$rep.php";
+}
+require($rep_file);
+exit();
+
+?>
\ No newline at end of file
diff --git a/sales/create_recurrent_invoices.php b/sales/create_recurrent_invoices.php
new file mode 100644 (file)
index 0000000..8886e34
--- /dev/null
@@ -0,0 +1,166 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+$page_security = 3;
+$path_to_root="..";
+include_once($path_to_root . "/sales/includes/cart_class.inc");
+include_once($path_to_root . "/includes/session.inc");
+include_once($path_to_root . "/sales/includes/ui/sales_order_ui.inc");
+include_once($path_to_root . "/includes/ui.inc");
+include_once($path_to_root . "/reporting/includes/reporting.inc");
+
+$js = "";
+if ($use_popup_windows)
+       $js .= get_js_open_window(900, 600);
+
+page(_("Create and Print Recurrent Invoices"), false, false, "", $js);
+
+function set_last_sent($id, $date)
+{
+       $date = date2sql($date);
+       $sql = "UPDATE ".TB_PREF."recurrent_invoices SET last_sent='$date' WHERE id=$id";
+       db_query($sql,"The recurrent invoice could not be updated or added");
+}      
+       
+function create_recurrent_invoices($customer_id, $branch_id, $order_no, $tmpl_no)
+{
+       $doc = new Cart(30, array($order_no));
+
+       get_customer_details_to_order($doc, $customer_id, $branch_id);
+
+       $doc->trans_type = 30;
+       $doc->trans_no = 0;
+       $doc->document_date = Today(); // 2006-06-15. Added so Invoices and Deliveries get current day
+
+       $doc->due_date = get_invoice_duedate($doc->customer_id, $doc->document_date);
+       $doc->reference = references::get_next($doc->trans_type);
+       $doc->Comments='';
+
+       foreach ($doc->line_items as $line_no=>$item) {
+               $line = &$doc->line_items[$line_no];
+               $line->price = get_price($line->stock_id, $doc->customer_currency,
+                       $doc->sales_type, $doc->price_factor, $doc->document_date);
+       }       
+       $cart = $doc;
+       $cart->trans_type = 10;
+       $cart->reference = references::get_next($cart->trans_type);
+       $invno = $cart->write(1);
+       set_last_sent($tmpl_no, $cart->document_date);
+       return $invno;
+}
+
+if (isset($_GET['recurrent']))
+{
+       $invs = array();
+       $sql = "SELECT * FROM ".TB_PREF."recurrent_invoices WHERE id=".$_GET['recurrent'];
+
+       $result = db_query($sql,"could not get recurrent invoice");
+       $myrow = db_fetch($result);
+       if ($myrow['debtor_no'] == 0)
+       {
+               $cust = get_cust_branches_from_group($myrow['group_no']);
+               while ($row = db_fetch($cust))
+               {
+                       $invs[] = create_recurrent_invoices($row['debtor_no'], $row['branch_code'], $myrow['order_no'], $myrow['id']);
+               }       
+       }
+       else
+       {
+               $invs[] = create_recurrent_invoices($myrow['debtor_no'], $myrow['group_no'], $myrow['order_no'], $myrow['id']);
+       }
+       if (count($invs) > 0)
+       {
+               $min = min($invs);
+               $max = max($invs);
+       }
+       else 
+               $min = $max = 0;
+       display_notification(sprintf(_("%s recurrent invoice(s) created, # $min - # $max."), count($invs)));
+       if (count($invs) > 0)
+       {
+               $ar = array('PARAM_0' => $min,  'PARAM_1' => $max, 'PARAM_2' => "", 'PARAM_3' => get_first_bank_account(), 
+                       'PARAM_4' => 0, 'PARAM_5' => 0, 'PARAM_6' => "", 'PARAM_7' => 10);
+               display_note(print_link(_("&Print Recurrent Invoices # $min - # $max"), 107, $ar), 0, 1);
+       }
+}      
+
+//-------------------------------------------------------------------------------------------------
+function get_sales_group_name($group_no)
+{
+       $sql = "SELECT description FROM ".TB_PREF."groups WHERE id = $group_no";
+       $result = db_query($sql, "could not get group");
+       $row = db_fetch($result);
+       return $row[0];
+}
+
+$sql = "SELECT * FROM ".TB_PREF."recurrent_invoices ORDER BY description, group_no, debtor_no";
+$result = db_query($sql,"could not get recurrent invoices");
+
+start_table("$table_style width=70%");
+$th = array(_("Description"), _("Template No"),_("Customer"),_("Branch")."/"._("Group"),_("Days"),_("Monthly"),_("Begin"),_("End"),_("Last Created"),"");
+table_header($th);
+$k = 0;
+$today = add_days(Today(), 1);
+$due = false;
+while ($myrow = db_fetch($result)) 
+{
+       $begin = sql2date($myrow["begin"]);
+       $end = sql2date($myrow["end"]);
+       $last_sent = sql2date($myrow["last_sent"]);
+       if ($myrow['monthly'] > 0)
+               $due_date = begin_month($last_sent);
+       else
+               $due_date = $last_sent;
+       $due_date = add_months($due_date, $myrow['monthly']);
+       $due_date = add_days($due_date, $myrow['days']);
+       $overdue = date1_greater_date2($today, $due_date) && date1_greater_date2($today, $begin)
+               && date1_greater_date2($end, $today);
+       if ($overdue)
+       {
+               start_row("class='overduebg'");
+               $due = true;
+       }       
+       else    
+               alt_table_row_color($k);
+               
+       label_cell($myrow["description"]);
+       label_cell(get_customer_trans_view_str(30, $myrow["order_no"]));
+       if ($myrow["debtor_no"] == 0)
+       {
+               label_cell("");
+               label_cell(get_sales_group_name($myrow["group_no"]));
+       }       
+       else
+       {
+               label_cell(get_customer_name($myrow["debtor_no"]));
+               label_cell(get_branch_name($myrow['group_no']));
+       }       
+       label_cell($myrow["days"]);
+       label_cell($myrow['monthly']);
+       label_cell($begin);
+       label_cell($end);
+       label_cell($last_sent);
+       if ($overdue)
+               label_cell("<a href='$path_to_root/sales/create_recurrent_invoices.php?recurrent=" . $myrow["id"] . "'>" . _("Create Invoices") . "</a>");
+       else
+               label_cell("");
+       end_row();
+}
+end_table();
+if ($due)
+       display_note(_("Marked items are due."), 1, 0, "class='overduefg'");
+else
+       display_note(("No recurrent invoices are due."), 1, 0);
+
+echo '<br>';
+
+end_page();
+?>
diff --git a/sales/includes/db/sales_points_db.inc b/sales/includes/db/sales_points_db.inc
new file mode 100644 (file)
index 0000000..65e045e
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+function add_sales_point($name, $location, $account, $cash, $credit)
+{
+       $sql = "INSERT INTO ".TB_PREF."sales_pos (pos_name, pos_location, pos_account, cash_sale, credit_sale) VALUES (".db_escape($name)
+                       . ",".db_escape($location).",".db_escape($account)
+                       . ",$cash,$credit)";
+       db_query($sql, "could not add point of sale");
+}
+
+function update_sales_point($id, $name, $location, $account, $cash, $credit)
+{
+
+       $sql = "UPDATE ".TB_PREF."sales_pos SET pos_name=".db_escape($name)
+                               .",pos_location=".db_escape($location)
+                               .",pos_account=".db_escape($account)
+                               .",cash_sale =$cash"
+                               .",credit_sale =$credit"
+                               ." WHERE id = $id";
+       
+       db_query($sql, "could not update sales type");                  
+}
+
+function get_all_sales_points()
+{
+       $sql = "SELECT pos.*, loc.location_name, acc.bank_account_name FROM "
+               .TB_PREF."sales_pos as pos,"
+               .TB_PREF."locations as loc,"
+               .TB_PREF."bank_accounts as acc"
+               ." WHERE pos.pos_location=loc.loc_code AND pos.pos_account=acc.id";
+       
+       return db_query($sql, "could not get all POS definitions");
+} 
+
+function get_sales_point($id)
+{
+       $sql = "SELECT pos.*, loc.location_name, acc.bank_account_name, acc.account_code FROM "
+               .TB_PREF."sales_pos as pos,"
+               .TB_PREF."locations as loc,"
+               .TB_PREF."bank_accounts as acc"
+               ." WHERE pos.id=$id AND pos.pos_location=loc.loc_code AND pos.pos_account=acc.id";
+       
+       $result = db_query($sql, "could not get POS definition");
+       
+       return db_fetch($result);
+}
+
+function get_sales_point_name($id)
+{
+       $sql = "SELECT pos_name FROM ".TB_PREF."sales_pos WHERE id=$id";
+       
+       $result = db_query($sql, "could not get POS name");
+       
+       $row = db_fetch_row($result);
+       return $row[0];
+}
+
+function delete_sales_point($id)
+{
+       $sql="DELETE FROM ".TB_PREF."sales_pos WHERE id=$id";
+       db_query($sql,"The point of sale record could not be deleted");
+}
+
+?>
\ No newline at end of file
diff --git a/sales/manage/recurrent_invoices.php b/sales/manage/recurrent_invoices.php
new file mode 100644 (file)
index 0000000..eace174
--- /dev/null
@@ -0,0 +1,196 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+$page_security = 3;
+$path_to_root="../..";
+include($path_to_root . "/includes/session.inc");
+include($path_to_root . "/includes/ui.inc");
+
+$js = "";
+if ($use_popup_windows)
+       $js .= get_js_open_window(900, 600);
+if ($use_date_picker)
+       $js .= get_js_date_picker();
+
+page(_("Recurrent Invoices"), false, false, "", $js);
+
+simple_page_mode(true);
+
+if ($Mode=='ADD_ITEM' || $Mode=='UPDATE_ITEM') 
+{
+
+       $input_error = 0;
+
+       if (strlen($_POST['description']) == 0) 
+       {
+               $input_error = 1;
+               display_error(_("The area description cannot be empty."));
+               set_focus('description');
+       }
+
+       if ($input_error != 1)
+       {
+       if ($selected_id != -1) 
+       {
+               $sql = "UPDATE ".TB_PREF."recurrent_invoices SET 
+                       description=".db_escape($_POST['description']).", 
+                       order_no=".db_escape($_POST['order_no']).", 
+                       debtor_no=".db_escape($_POST['debtor_no']).", 
+                       group_no=".db_escape($_POST['group_no']).", 
+                       days=".input_num('days', 0).", 
+                       monthly=".input_num('monthly', 0).", 
+                       begin='".date2sql($_POST['begin'])."', 
+                       end='".date2sql($_POST['end'])."' 
+                       WHERE id = '$selected_id'";
+                       $note = _('Selected recurrent invoice has been updated');
+       } 
+       else 
+       {
+               $sql = "INSERT INTO ".TB_PREF."recurrent_invoices (description, order_no, debtor_no,
+                       group_no, days, monthly, begin, end, last_sent) VALUES (".db_escape($_POST['description']) . ", "
+                       .db_escape($_POST['order_no']).", ".db_escape($_POST['debtor_no']).", "
+                       .db_escape($_POST['group_no']).", ".input_num('days', 0).", ".input_num('monthly', 0).", '"
+                       .date2sql($_POST['begin'])."', '".date2sql($_POST['end'])."', '".date2sql(Add_Years($_POST['begin'], -5))."')";
+                       $note = _('New recurrent invoice has been added');
+       }
+    
+       db_query($sql,"The recurrent invoice could not be updated or added");
+               display_notification($note);            
+               $Mode = 'RESET';
+       }
+} 
+
+if ($Mode == 'Delete')
+{
+
+       $cancel_delete = 0;
+
+       if ($cancel_delete == 0) 
+       {
+               $sql="DELETE FROM ".TB_PREF."recurrent_invoices WHERE id='" . $selected_id . "'";
+               db_query($sql,"could not delete recurrent invoice");
+
+               display_notification(_('Selected recurrent invoice has been deleted'));
+       } //end if Delete area
+       $Mode = 'RESET';
+} 
+
+if ($Mode == 'RESET')
+{
+       $selected_id = -1;
+       unset($_POST);
+}
+//-------------------------------------------------------------------------------------------------
+function get_sales_group_name($group_no)
+{
+       $sql = "SELECT description FROM ".TB_PREF."groups WHERE id = $group_no";
+       $result = db_query($sql, "could not get group");
+       $row = db_fetch($result);
+       return $row[0];
+}
+
+$sql = "SELECT * FROM ".TB_PREF."recurrent_invoices ORDER BY description, group_no, debtor_no";
+$result = db_query($sql,"could not get recurrent invoices");
+
+start_form();
+start_table("$table_style width=70%");
+$th = array(_("Description"), _("Template No"),_("Customer"),_("Branch")."/"._("Group"),_("Days"),_("Monthly"),_("Begin"),_("End"),_("Last Created"),"", "");
+table_header($th);
+$k = 0;
+while ($myrow = db_fetch($result)) 
+{
+       $begin = sql2date($myrow["begin"]);
+       $end = sql2date($myrow["end"]);
+       $last_sent = sql2date($myrow["last_sent"]);
+       
+       alt_table_row_color($k);
+               
+       label_cell($myrow["description"]);
+       label_cell(get_customer_trans_view_str(30, $myrow["order_no"]));
+       if ($myrow["debtor_no"] == 0)
+       {
+               label_cell("");
+               label_cell(get_sales_group_name($myrow["group_no"]));
+       }       
+       else
+       {
+               label_cell(get_customer_name($myrow["debtor_no"]));
+               label_cell(get_branch_name($myrow['group_no']));
+       }       
+       label_cell($myrow["days"]);
+       label_cell($myrow['monthly']);
+       label_cell($begin);
+       label_cell($end);
+       label_cell($last_sent);
+       edit_button_cell("Edit".$myrow["id"], _("Edit"));
+       delete_button_cell("Delete".$myrow["id"], _("Delete"));
+       end_row();
+}
+end_table();
+
+end_form();
+echo '<br>';
+
+//-------------------------------------------------------------------------------------------------
+
+start_form();
+
+start_table($table_style2);
+
+if ($selected_id != -1) 
+{
+       if ($Mode == 'Edit') {
+               //editing an existing area
+               $sql = "SELECT * FROM ".TB_PREF."recurrent_invoices WHERE id='$selected_id'";
+
+               $result = db_query($sql,"could not get recurrent invoice");
+               $myrow = db_fetch($result);
+
+               $_POST['description']  = $myrow["description"];
+               $_POST['order_no']  = $myrow["order_no"];
+               $_POST['debtor_no']  = $myrow["debtor_no"];
+               $_POST['group_no']  = $myrow["group_no"];
+               $_POST['days']  = $myrow["days"];
+               $_POST['monthly']  = $myrow["monthly"];
+               $_POST['begin']  = sql2date($myrow["begin"]);
+               $_POST['end']  = sql2date($myrow["end"]);
+       } 
+       hidden("selected_id", $selected_id);
+}
+
+
+text_row_ex(_("Description:"), 'description', 50); 
+
+templates_list_row(_("Template:"), 'order_no');
+
+customer_list_row(_("Customer:"), 'debtor_no', null, " ", true);
+
+if ($_POST['debtor_no'] > 0)
+       customer_branches_list_row(_("Branch:"), $_POST['debtor_no'], 'group_no', null, false);
+else   
+       sales_groups_list_row(_("Sales Group:"), 'group_no', null, " ");
+
+small_amount_row(_("Days:"), 'days', 0, null, null, 0);
+
+small_amount_row(_("Monthly:"), 'monthly', 0, null, null, 0);
+
+date_row(_("Begin:"), 'begin');
+
+date_row(_("End:"), 'end', null, null, 0, 0, 5);
+
+end_table(1);
+
+submit_add_or_update_center($selected_id == -1, '', true);
+
+end_form();
+
+end_page();
+?>
diff --git a/sales/manage/sales_groups.php b/sales/manage/sales_groups.php
new file mode 100644 (file)
index 0000000..0fb676c
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+$page_security = 3;
+$path_to_root="../..";
+include($path_to_root . "/includes/session.inc");
+
+page(_("Sales Groups"));
+
+include($path_to_root . "/includes/ui.inc");
+
+simple_page_mode(true);
+
+if ($Mode=='ADD_ITEM' || $Mode=='UPDATE_ITEM') 
+{
+
+       $input_error = 0;
+
+       if (strlen($_POST['description']) == 0) 
+       {
+               $input_error = 1;
+               display_error(_("The area description cannot be empty."));
+               set_focus('description');
+       }
+
+       if ($input_error != 1)
+       {
+       if ($selected_id != -1) 
+       {
+               $sql = "UPDATE ".TB_PREF."groups SET description=".db_escape($_POST['description'])." WHERE id = '$selected_id'";
+                       $note = _('Selected sales group has been updated');
+       } 
+       else 
+       {
+               $sql = "INSERT INTO ".TB_PREF."groups (description) VALUES (".db_escape($_POST['description']) . ")";
+                       $note = _('New sales group has been added');
+       }
+    
+       db_query($sql,"The sales group could not be updated or added");
+               display_notification($note);            
+               $Mode = 'RESET';
+       }
+} 
+
+if ($Mode == 'Delete')
+{
+
+       $cancel_delete = 0;
+
+       // PREVENT DELETES IF DEPENDENT RECORDS IN 'debtors_master'
+
+       $sql= "SELECT COUNT(*) FROM ".TB_PREF."cust_branch WHERE group_no='$selected_id'";
+       $result = db_query($sql,"check failed");
+       $myrow = db_fetch_row($result);
+       if ($myrow[0] > 0) 
+       {
+               $cancel_delete = 1;
+               display_error(_("Cannot delete this group because customers have been created using this group."));
+       } 
+       if ($cancel_delete == 0) 
+       {
+               $sql="DELETE FROM ".TB_PREF."groups WHERE id='" . $selected_id . "'";
+               db_query($sql,"could not delete sales group");
+
+               display_notification(_('Selected sales group has been deleted'));
+       } //end if Delete area
+       $Mode = 'RESET';
+} 
+
+if ($Mode == 'RESET')
+{
+       $selected_id = -1;
+       unset($_POST);
+}
+//-------------------------------------------------------------------------------------------------
+
+$sql = "SELECT * FROM ".TB_PREF."groups ORDER BY description";
+$result = db_query($sql,"could not get groups");
+
+start_form();
+start_table("$table_style width=30%");
+$th = array(_("Group Name"), "", "");
+table_header($th);
+$k = 0; 
+
+while ($myrow = db_fetch($result)) 
+{
+       
+       alt_table_row_color($k);
+               
+       label_cell($myrow["description"]);
+       edit_button_cell("Edit".$myrow["id"], _("Edit"));
+       delete_button_cell("Delete".$myrow["id"], _("Delete"));
+       end_row();
+}
+
+
+end_table();
+end_form();
+echo '<br>';
+
+//-------------------------------------------------------------------------------------------------
+
+start_form();
+
+start_table($table_style2);
+
+if ($selected_id != -1) 
+{
+       if ($Mode == 'Edit') {
+               //editing an existing area
+               $sql = "SELECT * FROM ".TB_PREF."groups WHERE id='$selected_id'";
+
+               $result = db_query($sql,"could not get group");
+               $myrow = db_fetch($result);
+
+               $_POST['description']  = $myrow["description"];
+       }
+       hidden("selected_id", $selected_id);
+} 
+
+text_row_ex(_("Group Name:"), 'description', 30); 
+
+end_table(1);
+
+submit_add_or_update_center($selected_id == -1, '', true);
+
+end_form();
+
+end_page();
+?>
diff --git a/sales/manage/sales_points.php b/sales/manage/sales_points.php
new file mode 100644 (file)
index 0000000..ac949c0
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+$page_security = 15;
+$path_to_root="../..";
+include_once($path_to_root . "/includes/session.inc");
+
+page(_("POS settings"));
+
+include_once($path_to_root . "/includes/ui.inc");
+include_once($path_to_root . "/sales/includes/db/sales_points_db.inc");
+
+simple_page_mode(true);
+//----------------------------------------------------------------------------------------------------
+
+function can_process()
+{
+       if (strlen($_POST['name']) == 0)
+       {
+               display_error(_("The POS name cannot be empty."));
+               set_focus('pos_name');
+               return false;
+       }
+       if (!check_value('cash') && !check_value('credit'))
+       {
+               display_error(_("You must allow cash or credit sale."));
+               set_focus('credit');
+               return false;
+       }
+
+       return true;
+}
+
+//----------------------------------------------------------------------------------------------------
+
+if ($Mode=='ADD_ITEM' && can_process())
+{
+       add_sales_point($_POST['name'], $_POST['location'], $_POST['account'],
+               check_value('cash'), check_value('credit'));
+       display_notification(_('New point of sale has been added'));
+       $Mode = 'RESET';
+}
+
+//----------------------------------------------------------------------------------------------------
+
+if ($Mode=='UPDATE_ITEM' && can_process())
+{
+
+       update_sales_point($selected_id, $_POST['name'], $_POST['location'],
+               $_POST['account'], check_value('cash'), check_value('credit'));
+       display_notification(_('Selected point of sale has been updated'));
+       $Mode = 'RESET';
+}
+
+//----------------------------------------------------------------------------------------------------
+
+if ($Mode == 'Delete')
+{
+       delete_sales_point($selected_id);
+       display_notification(_('Selected point of sale has been deleted'));
+       $Mode = 'RESET';
+}
+
+if ($Mode == 'RESET')
+{
+       $selected_id = -1;
+       unset($_POST);
+}
+//----------------------------------------------------------------------------------------------------
+
+$result = get_all_sales_points();
+
+start_form();
+start_table("$table_style");
+
+$th = array (_('POS Name'), _('Credit sale'), _('Cash sale'), _('Location'), _('Default account'), 
+        '','');
+table_header($th);
+$k = 0;
+
+while ($myrow = db_fetch($result))
+{
+    alt_table_row_color($k);
+       label_cell($myrow["pos_name"], "nowrap");
+       label_cell($myrow['credit_sale'] ? _('Yes') : _('No'));
+       label_cell($myrow['cash_sale'] ? _('Yes') : _('No'));
+       label_cell($myrow["location_name"], "");
+       label_cell($myrow["bank_account_name"], "");
+       edit_button_cell("Edit".$myrow['id'], _("Edit"));
+       delete_button_cell("Delete".$myrow['id'], _("Delete"));
+       end_row();
+}
+
+end_table();
+end_form();
+echo '<br>';
+//----------------------------------------------------------------------------------------------------
+
+start_form();
+
+start_table($table_style2);
+
+if ($selected_id != -1)
+{
+
+       if ($Mode == 'Edit') {
+               $myrow = get_sales_point($selected_id);
+
+               $_POST['name']  = $myrow["pos_name"];
+               $_POST['location']  = $myrow["pos_location"];
+               $_POST['account']  = $myrow["pos_account"];
+               if ($myrow["credit_sale"]) $_POST['credit_sale']  = 1;
+               if ($myrow["cash_sale"]) $_POST['cash_sale'] = 1;
+       }
+       hidden('selected_id', $selected_id);
+} 
+
+text_row_ex(_("Point of Sale Name").':', 'name', 20, 30);
+check_row(_('Allowed credit sale'), 'credit', check_value('credit_sale'));
+check_row(_('Allowed cash sale'), 'cash',  check_value('cash_sale'));
+locations_list_row(_("POS location").':', 'location');
+cash_accounts_list_row(_("Default cash account").':', 'account');
+
+end_table(1);
+
+submit_add_or_update_center($selected_id == -1, '', true);
+
+end_form();
+
+end_page();
+
+?>
diff --git a/sql/alter2.1.php b/sql/alter2.1.php
new file mode 100644 (file)
index 0000000..371c3d6
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+/**********************************************************************
+    Copyright (C) FrontAccounting, LLC.
+       Released under the terms of the GNU General Public License, GPL, 
+       as published by the Free Software Foundation, either version 3 
+       of the License, or (at your option) any later version.
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+    See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
+***********************************************************************/
+class fa2_1 {
+       var $version = '2.1';   // version installed
+       var $description = 'Version 2.1';
+       var $sql = 'alter2.1.sql';
+       //
+       //      Install procedure. All additional changes 
+       //      not included in sql file should go here.
+       //
+       function install($pref, $force) 
+       {
+               global $db;
+       /*
+       Statement below is allowed only for MySQL >=4.0.4:
+       UPDATE `0_bank_trans`, `0_bank_accounts` 
+               SET 0_bank_trans.bank_act=0_bank_accounts.id 
+               WHERE 0_bank_trans.bank_act=0_bank_accounts.account_code;
+       */
+               $sql = "SELECT id, account_code FROM ".$pref."bank_accounts";
+               if(!($res = db_query($sql))) {
+                       display_error(_("Cannot retrieve bank accounts codes")
+                               .':<br>'. db_error_msg($db));
+                       return false;
+               }
+               while ($acc = db_fetch($res)) {
+                       $sql = "UPDATE ".$pref."bank_trans SET bank_act='"
+                               .$acc['id']."' WHERE bank_act=".$acc['account_code'];
+                       if (db_query($sql)==false) {
+                       display_error(_("Cannot update bank transactions")
+                               .':<br>'. db_error_msg($db));
+                               return false;
+                       }
+               }
+               // copy all item codes from stock_master into item_codes
+               $sql = "SELECT `stock_id`,`description`,`category_id` FROM ".$pref."stock_master";
+               $result = db_query($sql);
+               if (!$result) {
+                       display_error(_("Cannot select stock identificators")
+                               .':<br>'. db_error_msg($db));
+                       return false;
+               } else {
+                       while ($row = db_fetch_assoc($result)) {
+                               $sql = "INSERT IGNORE "
+                                       .$pref."item_codes (`item_code`,`stock_id`,`description`,`category_id`)
+                                       VALUES('".$row['stock_id']."','".$row['stock_id']."','"
+                                       .$row['description']."','".$row['category_id']."')";
+                               $res2 = db_query($sql);
+                               if (!$res2) {
+                                       display_error(_("Cannot insert stock id into item_codes")
+                                               .':<br>'. db_error_msg($db));
+                                       return false;
+                               }
+                       }
+               }
+               // remove obsolete bank_trans_types table 
+               // (DROP queries are skipped during non-forced upgrade)
+               $sql = "DROP TABLE IF EXISTS `0_bank_trans_types`";
+               db_query($sql);
+               //
+               //      Move all debtor and supplier trans tax details to new table
+               // (INSERT INTO t  SELECT ... FROM t ... available after 4.0.14)
+               // No easy way to restore net amount for 0% tax rate for moved
+               // FA 2.0 transactions, but who cares?
+               //
+       $move_sql =array( 
+       "debtor_trans_tax_details" =>
+               "SELECT tr.tran_date, tr.type, tr.trans_no, dt.tax_type_id, 
+                       dt.rate, dt.included_in_price, dt.amount, tr.reference as ref,
+                       tr.rate as ex_rate
+               FROM ".$pref."debtor_trans_tax_details dt       
+                       LEFT JOIN ".$pref."trans_tax_details tt
+                               ON dt.debtor_trans_no=tt.trans_no 
+                               AND dt.debtor_trans_type=tt.trans_type,
+                       ".$pref."debtor_trans tr
+               WHERE tt.trans_type is NULL
+                       AND dt.debtor_trans_no = tr.trans_no 
+                       AND dt.debtor_trans_type = tr.type",
+       
+       "supp_invoice_tax_items" =>
+               "SELECT tr.tran_date, tr.type, tr.trans_no, st.tax_type_id, 
+                       st.rate, st.included_in_price, st.amount, tr.supp_reference as ref,
+                       tr.rate as ex_rate
+                       FROM ".$pref."supp_invoice_tax_items st 
+                               LEFT JOIN ".$pref."trans_tax_details tt
+                                       ON st.supp_trans_no=tt.trans_no 
+                                       AND st.supp_trans_type=tt.trans_type,
+                                       ".$pref."supp_trans tr
+                               WHERE tt.trans_type is NULL
+                                       AND st.supp_trans_no = tr.trans_no 
+                                       AND st.supp_trans_type = tr.type");
+
+       foreach ($move_sql as $tbl => $sql) {
+               if (!check_table($pref, $tbl)){
+                       $res = db_query($sql, "Cannot retrieve trans tax details from $tbl");
+                       while ($row = db_fetch($res)) {
+                               $net_amount = $row['rate'] == 0 ?
+                                       0 : ($row['included_in_price'] ? 
+                                                       ($row['amount']/$row['rate']*(100-$row['rate']))
+                                                       :($row['amount']/$row['rate']*100));
+                               $sql2 = "INSERT INTO ".$pref."trans_tax_details 
+                               (trans_type,trans_no,tran_date,tax_type_id,rate,ex_rate,
+                                       included_in_price, net_amount, amount, memo)
+                               VALUES ('".$row['type']."','".$row['trans_no']."','"
+                                       .$row['tran_date']."','".$row['tax_type_id']."','"
+                                       .$row['rate']."','".$row['ex_rate']."','"
+                                       .$row['included_in_price']."','".$net_amount
+                                       ."','".$row['amount']."','".$row['ref']."')";
+                               db_query($sql2, "Cannot move trans tax details from $tbl");
+                       }
+                       db_query("DROP TABLE ".$pref.$tbl, "cannot remove $tbl");
+               }
+       }
+               
+               return true;
+       }
+       //
+       //      Checking before install
+       //
+       function pre_check($pref)
+       {
+       // We cannot perform successfull upgrade on system where the
+       // trans tax details tables was deleted during previous try.  
+               if (check_table($pref, 'debtor_trans_tax_details') 
+                       || check_table($pref, 'supp_invoice_tax_items')) {
+                       display_error(_("Seems that system upgrade to version 2.1 has 
+                       been performed for this company already.<br> If something has gone 
+                       wrong and you want to retry upgrade process you MUST perform 
+                       database restore from last backup file first."));
+
+                       return false;
+               } 
+
+               return true; // true when ok, fail otherwise
+       }
+       //
+       //      Test if patch was applied before.
+       //
+       function installed($pref) {
+               if (check_table($pref, 'item_codes')) return false;
+               if (check_table($pref, 'company', 'foreign_codes')) return false;
+               if (check_table($pref, 'suppliers', 'credit_limit')) return false;
+               if (check_table($pref, 'bank_trans', 'reconciled', 
+                       array('Type'=>'date'))) return false;
+               if (check_table($pref, 'trans_tax_details')) return false;
+               return true;
+       }
+};
+
+$install = new fa2_1;
+?>
\ No newline at end of file
diff --git a/sql/alter2.1.sql b/sql/alter2.1.sql
new file mode 100644 (file)
index 0000000..6e435d9
--- /dev/null
@@ -0,0 +1,274 @@
+#
+#      Database upgrade script Front Accounting 
+#      Source version: 2.0.x
+#      Target version: 2.1.0
+#      
+#      To make upgrades clean and failsafe:
+#      * Precede all CREATE TABLE statment with DROP TABLE IF EXISTS
+#      * Precede all ALTER TABLE statements using ADD column with respective
+#              ALTER TABLE with DROP column
+#      * Move all other DROP queries (e.g. removing obsolete tables) to installer
+#              - they are not executed during non-forced upgrade.
+#
+
+DROP TABLE IF EXISTS `0_attachments`;
+
+CREATE TABLE `0_attachments` (
+  `id` int(11) UNSIGNED NOT NULL auto_increment,
+  `description` varchar(60) NOT NULL default '',
+  `type_no` int(11) NOT NULL default '0',
+  `trans_no` int(11) NOT NULL default '0',
+  `unique_name` varchar(60) NOT NULL default '',
+  `tran_date` date NOT NULL default '0000-00-00',
+  `filename` varchar(60) NOT NULL default '',
+  `filesize` int(11) NOT NULL default '0',
+  `filetype` varchar(60) NOT NULL default '',
+  PRIMARY KEY  (`id`),
+  KEY `type_no` (`type_no`,`trans_no`)
+) TYPE=MyISAM AUTO_INCREMENT=1 ;
+
+DROP TABLE IF EXISTS `0_groups`;
+
+CREATE TABLE `0_groups` (
+  `id` smallint(6) UNSIGNED NOT NULL auto_increment,
+  `description` varchar(60) NOT NULL default '',
+  `inactive` tinyint(1) NOT NULL default 0,
+  PRIMARY KEY  (`id`),
+  UNIQUE KEY `description` (`description`)
+) TYPE=MyISAM AUTO_INCREMENT=1 ;
+
+INSERT INTO `0_groups` VALUES ('1', 'Small', '0');
+INSERT INTO `0_groups` VALUES ('2', 'Medium', '0');
+INSERT INTO `0_groups` VALUES ('3', 'Large', '0');
+
+DROP TABLE IF EXISTS `0_recurrent_invoices`;
+
+CREATE TABLE `0_recurrent_invoices` (
+  `id` smallint(6) UNSIGNED NOT NULL auto_increment,
+  `description` varchar(60) NOT NULL default '',
+  `order_no` int(11) UNSIGNED NOT NULL,
+  `debtor_no` int(11) UNSIGNED NULL default NULL,
+  `group_no` smallint(6) UNSIGNED NULL default NULL,
+  `days` int(11) NOT NULL default '0',
+  `monthly` int(11) NOT NULL default '0',
+  `begin` date NOT NULL default '0000-00-00',
+  `end` date NOT NULL default '0000-00-00',
+  `last_sent` date NOT NULL default '0000-00-00',
+  PRIMARY KEY  (`id`),
+  UNIQUE KEY `description` (`description`)
+) TYPE=InnoDB AUTO_INCREMENT=1 ;
+
+ALTER TABLE `0_cust_branch` ADD `group_no` int(11) NOT NULL default '0';
+
+ALTER TABLE `0_debtor_trans` ADD `dimension_id` int(11) NOT NULL default '0';
+ALTER TABLE `0_debtor_trans` ADD `dimension2_id` int(11) NOT NULL default '0';
+
+ALTER TABLE `0_bank_accounts` DROP PRIMARY KEY;
+ALTER TABLE `0_bank_accounts` ADD `id` SMALLINT(6) AUTO_INCREMENT PRIMARY KEY;
+ALTER TABLE `0_bank_accounts` ADD `last_reconciled_date` timestamp NOT NULL default '0000-00-00';
+ALTER TABLE `0_bank_accounts` ADD `ending_reconcile_balance` double NOT NULL default '0';
+
+ALTER TABLE `0_bank_trans` DROP COLUMN `bank_trans_type_id`;
+ALTER TABLE `0_bank_trans` ADD `reconciled` date default NULL;
+
+ALTER TABLE `0_users` ADD `query_size` TINYINT(1) DEFAULT '10';
+
+ALTER TABLE `0_users` ADD `graphic_links` TINYINT(1) DEFAULT '1';
+
+DROP TABLE IF EXISTS `0_sales_pos`;
+
+CREATE TABLE `0_sales_pos` (
+  `id` smallint(6) UNSIGNED NOT NULL auto_increment,
+  `pos_name` varchar(30) NOT NULL,
+  `cash_sale` tinyint(1) NOT NULL,
+  `credit_sale` tinyint(1) NOT NULL,
+  `pos_location` varchar(5) NOT NULL,
+  `pos_account` smallint(6) UNSIGNED NOT NULL,
+  `inactive` tinyint(1) NOT NULL default 0,
+  PRIMARY KEY  (`id`),
+  UNIQUE KEY(`pos_name`)
+) TYPE=MyISAM AUTO_INCREMENT=1;
+
+INSERT INTO `0_sales_pos` VALUES ('1', 'Default', '1', '1', 'DEF', '1', '0');
+
+ALTER TABLE `0_users` ADD `pos` SMALLINT(6) DEFAULT '1';
+
+DROP TABLE IF EXISTS `0_quick_entries`;
+
+CREATE TABLE `0_quick_entries` (
+  `id` smallint(6) UNSIGNED NOT NULL auto_increment,
+  `type` tinyint(1) NOT NULL default '0',
+  `description` varchar(60) NOT NULL,
+  `base_amount` double NOT NULL default '0',
+  `base_desc` varchar(60) NULL,
+  PRIMARY KEY  (`id`),
+  KEY `description` (`description`)
+) TYPE=MyISAM AUTO_INCREMENT=1;
+
+INSERT INTO `0_quick_entries` VALUES ('1', '1', 'Maintenance', '0', 'Amount');
+INSERT INTO `0_quick_entries` VALUES ('2', '1', 'Phone', '0', 'Amount');
+INSERT INTO `0_quick_entries` VALUES ('3', '2', 'Cash Sales', '0', 'Amount');
+
+DROP TABLE IF EXISTS `0_quick_entry_lines`;
+
+CREATE TABLE `0_quick_entry_lines` (
+  `id` smallint(6) UNSIGNED NOT NULL auto_increment,
+  `qid` smallint(6) UNSIGNED NOT NULL,
+  `amount` double default NULL default '0',
+  `action` varchar(2) NOT NULL,
+  `dest_id` varchar(11) NOT NULL,
+  `dimension_id` smallint(6) UNSIGNED NULL default NULL,
+  `dimension2_id` smallint(6) UNSIGNED NULL default NULL,
+  PRIMARY KEY  (`id`),
+  KEY `qid` (`qid`)
+) TYPE=MyISAM AUTO_INCREMENT=1;
+
+INSERT INTO `0_quick_entry_lines` VALUES ('1', '1','0','=', '6600', '0', '0');
+INSERT INTO `0_quick_entry_lines` VALUES ('2', '2','0','=', '6730', '0', '0');
+INSERT INTO `0_quick_entry_lines` VALUES ('3', '3','0','=', '3000', '0', '0');
+
+ALTER TABLE `0_users` ADD `print_profile` VARCHAR(30) NOT NULL DEFAULT '1';
+ALTER TABLE `0_users` ADD `rep_popup` TINYINT(1) DEFAULT '1';
+
+DROP TABLE IF EXISTS `0_print_profiles`;
+CREATE TABLE `0_print_profiles` (
+  `id` smallint(6) UNSIGNED NOT NULL auto_increment,
+  `profile` varchar(30) NOT NULL,
+  `report` varchar(5) NULL default NULL,
+  `printer` tinyint(3) UNSIGNED NULL default NULL,
+  PRIMARY KEY  (`id`),
+  UNIQUE KEY `profile` (`profile`,`report`)
+) TYPE=MyISAM AUTO_INCREMENT=10;
+
+INSERT INTO `0_print_profiles` VALUES ('1', 'Out of office', '', '0');
+INSERT INTO `0_print_profiles` VALUES ('2', 'Sales Department', '', '0');
+INSERT INTO `0_print_profiles` VALUES ('3', 'Central', '', '2');
+INSERT INTO `0_print_profiles` VALUES ('4', 'Sales Department', '104', '2');
+INSERT INTO `0_print_profiles` VALUES ('5', 'Sales Department', '105', '2');
+INSERT INTO `0_print_profiles` VALUES ('6', 'Sales Department', '107', '2');
+INSERT INTO `0_print_profiles` VALUES ('7', 'Sales Department', '109', '2');
+INSERT INTO `0_print_profiles` VALUES ('8', 'Sales Department', '110', '2');
+INSERT INTO `0_print_profiles` VALUES ('9', 'Sales Department', '201', '2');
+
+DROP TABLE IF EXISTS `0_printers`;
+
+CREATE TABLE `0_printers` (
+  `id` tinyint(3) UNSIGNED NOT NULL auto_increment,
+  `name` varchar(20) NOT NULL,
+  `description` varchar(60) NOT NULL,
+  `queue` varchar(20) NOT NULL,
+  `host` varchar(40) NOT NULL,
+  `port` smallint(11) unsigned NOT NULL,
+  `timeout` tinyint(3) unsigned NOT NULL,
+  PRIMARY KEY  (`id`),
+  UNIQUE KEY `name` (`name`)
+) TYPE=MyISAM AUTO_INCREMENT=4;
+
+INSERT INTO `0_printers` VALUES ('1', 'QL500', 'Label printer', 'QL500', 'server', '127', '20');
+INSERT INTO `0_printers` VALUES ('2', 'Samsung', 'Main network printer', 'scx4521F', 'server', '515', '5');
+INSERT INTO `0_printers` VALUES ('3', 'Local', 'Local print server at user IP', 'lp', '', '515', '10');
+
+DROP TABLE IF EXISTS `0_item_codes`;
+
+CREATE TABLE `0_item_codes` (
+  `id` int(11) UNSIGNED NOT NULL auto_increment,
+  `item_code` varchar(20) NOT NULL,
+  `stock_id` varchar(20) NOT NULL,
+  `description` varchar(200) NOT NULL default '',
+  `category_id` smallint(6) UNSIGNED NOT NULL,
+  `quantity` double NOT NULL default '1',
+  `is_foreign` tinyint(1) NOT NULL default 0,
+  `inactive` tinyint(1) NOT NULL default 0,
+  PRIMARY KEY  (`id`),
+  UNIQUE KEY(`stock_id`, `item_code`)
+) TYPE=MyISAM AUTO_INCREMENT=1;
+
+ALTER TABLE `0_company` ADD `foreign_codes` TINYINT(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_company` ADD `accumulate_shipping` TINYINT(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_company` ADD `legal_text` tinytext NOT NULL DEFAULT '';
+
+ALTER TABLE `0_suppliers` ADD `supp_address` tinytext NOT NULL DEFAULT '' AFTER `address`;
+
+ALTER TABLE `0_suppliers` ADD `phone` varchar(30) NOT NULL DEFAULT '' AFTER `supp_address`;
+
+ALTER TABLE `0_suppliers` ADD `fax` varchar(30) NOT NULL DEFAULT '' AFTER `phone`;
+
+ALTER TABLE `0_suppliers` ADD `gst_no` varchar(25) NOT NULL DEFAULT '' AFTER `fax`;
+
+ALTER TABLE `0_suppliers` ADD `contact` varchar(60) NOT NULL DEFAULT '' AFTER `gst_no`;
+
+ALTER TABLE `0_suppliers` ADD `credit_limit` double NOT NULL DEFAULT '0' AFTER `tax_group_id`;
+
+ALTER TABLE `0_suppliers` ADD `supp_account_no` varchar(40) NOT NULL DEFAULT '' AFTER `contact`;
+
+ALTER TABLE `0_suppliers` ADD `website` varchar(100) NOT NULL DEFAULT '' AFTER `email`;
+
+ALTER TABLE `0_suppliers` ADD `notes` tinytext NOT NULL DEFAULT '';
+
+ALTER TABLE `0_chart_types` DROP INDEX `name`, ADD INDEX `name` ( `name` );
+
+DROP TABLE IF EXISTS `0_sql_trail`; 
+
+CREATE TABLE IF NOT EXISTS `0_sql_trail` (
+  `id` int(11) UNSIGNED NOT NULL auto_increment,
+  `sql` text NOT NULL, 
+  `result` tinyint(1) NOT NULL, 
+  `msg` varchar(255) NOT NULL,
+  PRIMARY KEY (`id`)
+) TYPE = MyISAM;
+
+ALTER TABLE `0_tax_types` DROP COLUMN `out`;
+
+ALTER TABLE `0_chart_master` DROP COLUMN `tax_code`;
+
+ALTER TABLE `0_chart_master` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_currencies` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_bank_accounts` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_debtors_master` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_stock_master` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_workcentres` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_locations` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_sales_types` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_areas` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_salesman` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_shippers` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_credit_status` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_payment_terms` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_suppliers` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_stock_category` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+ALTER TABLE `0_item_units` ADD `inactive` tinyint(1) NOT NULL DEFAULT '0';
+
+DROP TABLE IF EXISTS `0_trans_tax_details`;
+
+CREATE TABLE `0_trans_tax_details` (
+  `id` int(11) NOT NULL auto_increment,
+  `trans_type` smallint(6) default NULL,
+  `trans_no` int(11) default NULL,
+  `tran_date` date NOT NULL,
+  `tax_type_id` int(11) NOT NULL default '0',
+  `rate` double NOT NULL default '0',
+  `ex_rate` double NOT NULL default '1',
+  `included_in_price` tinyint(1) NOT NULL default '0',
+  `net_amount` double NOT NULL default '0',
+  `amount` double NOT NULL default '0',
+  `memo` tinytext default NULL,
+  PRIMARY KEY  (`id`)
+) TYPE=InnoDB AUTO_INCREMENT=1 ;
+
diff --git a/themes/aqua/images/add.png b/themes/aqua/images/add.png
new file mode 100644 (file)
index 0000000..6332fef
Binary files /dev/null and b/themes/aqua/images/add.png differ
diff --git a/themes/aqua/images/ajax-loader.gif b/themes/aqua/images/ajax-loader.gif
new file mode 100644 (file)
index 0000000..3d185fe
Binary files /dev/null and b/themes/aqua/images/ajax-loader.gif differ
diff --git a/themes/aqua/images/cancel.png b/themes/aqua/images/cancel.png
new file mode 100644 (file)
index 0000000..2541d2b
Binary files /dev/null and b/themes/aqua/images/cancel.png differ
diff --git a/themes/aqua/images/credit.gif b/themes/aqua/images/credit.gif
new file mode 100644 (file)
index 0000000..4627583
Binary files /dev/null and b/themes/aqua/images/credit.gif differ
diff --git a/themes/aqua/images/delete.gif b/themes/aqua/images/delete.gif
new file mode 100644 (file)
index 0000000..327af2a
Binary files /dev/null and b/themes/aqua/images/delete.gif differ
diff --git a/themes/aqua/images/download.gif b/themes/aqua/images/download.gif
new file mode 100644 (file)
index 0000000..67823e9
Binary files /dev/null and b/themes/aqua/images/download.gif differ
diff --git a/themes/aqua/images/edit.gif b/themes/aqua/images/edit.gif
new file mode 100644 (file)
index 0000000..21a68c6
Binary files /dev/null and b/themes/aqua/images/edit.gif differ
diff --git a/themes/aqua/images/gl.png b/themes/aqua/images/gl.png
new file mode 100644 (file)
index 0000000..7d863f9
Binary files /dev/null and b/themes/aqua/images/gl.png differ
diff --git a/themes/aqua/images/help.gif b/themes/aqua/images/help.gif
new file mode 100644 (file)
index 0000000..58c0ffe
Binary files /dev/null and b/themes/aqua/images/help.gif differ
diff --git a/themes/aqua/images/invoice.gif b/themes/aqua/images/invoice.gif
new file mode 100644 (file)
index 0000000..aef0be2
Binary files /dev/null and b/themes/aqua/images/invoice.gif differ
diff --git a/themes/aqua/images/login.gif b/themes/aqua/images/login.gif
new file mode 100644 (file)
index 0000000..bd3f47c
Binary files /dev/null and b/themes/aqua/images/login.gif differ
diff --git a/themes/aqua/images/money.png b/themes/aqua/images/money.png
new file mode 100644 (file)
index 0000000..42c52d0
Binary files /dev/null and b/themes/aqua/images/money.png differ
diff --git a/themes/aqua/images/ok.gif b/themes/aqua/images/ok.gif
new file mode 100644 (file)
index 0000000..c566bb8
Binary files /dev/null and b/themes/aqua/images/ok.gif differ
diff --git a/themes/aqua/images/pdf.gif b/themes/aqua/images/pdf.gif
new file mode 100644 (file)
index 0000000..071bb35
Binary files /dev/null and b/themes/aqua/images/pdf.gif differ
diff --git a/themes/aqua/images/print.png b/themes/aqua/images/print.png
new file mode 100644 (file)
index 0000000..a350d18
Binary files /dev/null and b/themes/aqua/images/print.png differ
diff --git a/themes/aqua/images/receive.gif b/themes/aqua/images/receive.gif
new file mode 100644 (file)
index 0000000..6a92dd2
Binary files /dev/null and b/themes/aqua/images/receive.gif differ
diff --git a/themes/aqua/images/remove.png b/themes/aqua/images/remove.png
new file mode 100644 (file)
index 0000000..08f2493
Binary files /dev/null and b/themes/aqua/images/remove.png differ
diff --git a/themes/aqua/images/report.png b/themes/aqua/images/report.png
new file mode 100644 (file)
index 0000000..779ad58
Binary files /dev/null and b/themes/aqua/images/report.png differ
diff --git a/themes/aqua/images/right.gif b/themes/aqua/images/right.gif
new file mode 100644 (file)
index 0000000..eed1194
Binary files /dev/null and b/themes/aqua/images/right.gif differ
diff --git a/themes/aqua/images/sort_asc.gif b/themes/aqua/images/sort_asc.gif
new file mode 100644 (file)
index 0000000..8055b42
Binary files /dev/null and b/themes/aqua/images/sort_asc.gif differ
diff --git a/themes/aqua/images/sort_desc.gif b/themes/aqua/images/sort_desc.gif
new file mode 100644 (file)
index 0000000..e0f93f7
Binary files /dev/null and b/themes/aqua/images/sort_desc.gif differ
diff --git a/themes/aqua/images/sort_none.gif b/themes/aqua/images/sort_none.gif
new file mode 100644 (file)
index 0000000..19cc2a2
Binary files /dev/null and b/themes/aqua/images/sort_none.gif differ
diff --git a/themes/aqua/images/view.gif b/themes/aqua/images/view.gif
new file mode 100644 (file)
index 0000000..3776aa7
Binary files /dev/null and b/themes/aqua/images/view.gif differ
diff --git a/themes/cool/images/add.png b/themes/cool/images/add.png
new file mode 100644 (file)
index 0000000..6332fef
Binary files /dev/null and b/themes/cool/images/add.png differ
diff --git a/themes/cool/images/ajax-loader.gif b/themes/cool/images/ajax-loader.gif
new file mode 100644 (file)
index 0000000..28aabec
Binary files /dev/null and b/themes/cool/images/ajax-loader.gif differ
diff --git a/themes/cool/images/cancel.png b/themes/cool/images/cancel.png
new file mode 100644 (file)
index 0000000..2541d2b
Binary files /dev/null and b/themes/cool/images/cancel.png differ
diff --git a/themes/cool/images/credit.gif b/themes/cool/images/credit.gif
new file mode 100644 (file)
index 0000000..4627583
Binary files /dev/null and b/themes/cool/images/credit.gif differ
diff --git a/themes/cool/images/delete.gif b/themes/cool/images/delete.gif
new file mode 100644 (file)
index 0000000..327af2a
Binary files /dev/null and b/themes/cool/images/delete.gif differ
diff --git a/themes/cool/images/download.gif b/themes/cool/images/download.gif
new file mode 100644 (file)
index 0000000..67823e9
Binary files /dev/null and b/themes/cool/images/download.gif differ
diff --git a/themes/cool/images/edit.gif b/themes/cool/images/edit.gif
new file mode 100644 (file)
index 0000000..21a68c6
Binary files /dev/null and b/themes/cool/images/edit.gif differ
diff --git a/themes/cool/images/gl.png b/themes/cool/images/gl.png
new file mode 100644 (file)
index 0000000..7d863f9
Binary files /dev/null and b/themes/cool/images/gl.png differ
diff --git a/themes/cool/images/help.gif b/themes/cool/images/help.gif
new file mode 100644 (file)
index 0000000..58c0ffe
Binary files /dev/null and b/themes/cool/images/help.gif differ
diff --git a/themes/cool/images/invoice.gif b/themes/cool/images/invoice.gif
new file mode 100644 (file)
index 0000000..aef0be2
Binary files /dev/null and b/themes/cool/images/invoice.gif differ
diff --git a/themes/cool/images/login.gif b/themes/cool/images/login.gif
new file mode 100644 (file)
index 0000000..bd3f47c
Binary files /dev/null and b/themes/cool/images/login.gif differ
diff --git a/themes/cool/images/money.png b/themes/cool/images/money.png
new file mode 100644 (file)
index 0000000..42c52d0
Binary files /dev/null and b/themes/cool/images/money.png differ
diff --git a/themes/cool/images/ok.gif b/themes/cool/images/ok.gif
new file mode 100644 (file)
index 0000000..c566bb8
Binary files /dev/null and b/themes/cool/images/ok.gif differ
diff --git a/themes/cool/images/pdf.gif b/themes/cool/images/pdf.gif
new file mode 100644 (file)
index 0000000..071bb35
Binary files /dev/null and b/themes/cool/images/pdf.gif differ
diff --git a/themes/cool/images/print.png b/themes/cool/images/print.png
new file mode 100644 (file)
index 0000000..a350d18
Binary files /dev/null and b/themes/cool/images/print.png differ
diff --git a/themes/cool/images/receive.gif b/themes/cool/images/receive.gif
new file mode 100644 (file)
index 0000000..6a92dd2
Binary files /dev/null and b/themes/cool/images/receive.gif differ
diff --git a/themes/cool/images/remove.png b/themes/cool/images/remove.png
new file mode 100644 (file)
index 0000000..08f2493
Binary files /dev/null and b/themes/cool/images/remove.png differ
diff --git a/themes/cool/images/report.png b/themes/cool/images/report.png
new file mode 100644 (file)
index 0000000..779ad58
Binary files /dev/null and b/themes/cool/images/report.png differ
diff --git a/themes/cool/images/right.gif b/themes/cool/images/right.gif
new file mode 100644 (file)
index 0000000..eed1194
Binary files /dev/null and b/themes/cool/images/right.gif differ
diff --git a/themes/cool/images/sort_asc.gif b/themes/cool/images/sort_asc.gif
new file mode 100644 (file)
index 0000000..61bdf04
Binary files /dev/null and b/themes/cool/images/sort_asc.gif differ
diff --git a/themes/cool/images/sort_desc.gif b/themes/cool/images/sort_desc.gif
new file mode 100644 (file)
index 0000000..e1395ee
Binary files /dev/null and b/themes/cool/images/sort_desc.gif differ
diff --git a/themes/cool/images/sort_none.gif b/themes/cool/images/sort_none.gif
new file mode 100644 (file)
index 0000000..19cc2a2
Binary files /dev/null and b/themes/cool/images/sort_none.gif differ
diff --git a/themes/cool/images/view.gif b/themes/cool/images/view.gif
new file mode 100644 (file)
index 0000000..3776aa7
Binary files /dev/null and b/themes/cool/images/view.gif differ
diff --git a/themes/default/images/add.png b/themes/default/images/add.png
new file mode 100644 (file)
index 0000000..6332fef
Binary files /dev/null and b/themes/default/images/add.png differ
diff --git a/themes/default/images/ajax-loader.gif b/themes/default/images/ajax-loader.gif
new file mode 100644 (file)
index 0000000..40f18a9
Binary files /dev/null and b/themes/default/images/ajax-loader.gif differ
diff --git a/themes/default/images/cancel.png b/themes/default/images/cancel.png
new file mode 100644 (file)
index 0000000..2541d2b
Binary files /dev/null and b/themes/default/images/cancel.png differ
diff --git a/themes/default/images/credit.gif b/themes/default/images/credit.gif
new file mode 100644 (file)
index 0000000..4627583
Binary files /dev/null and b/themes/default/images/credit.gif differ
diff --git a/themes/default/images/delete.gif b/themes/default/images/delete.gif
new file mode 100644 (file)
index 0000000..327af2a
Binary files /dev/null and b/themes/default/images/delete.gif differ
diff --git a/themes/default/images/download.gif b/themes/default/images/download.gif
new file mode 100644 (file)
index 0000000..67823e9
Binary files /dev/null and b/themes/default/images/download.gif differ
diff --git a/themes/default/images/edit.gif b/themes/default/images/edit.gif
new file mode 100644 (file)
index 0000000..21a68c6
Binary files /dev/null and b/themes/default/images/edit.gif differ
diff --git a/themes/default/images/escape.png b/themes/default/images/escape.png
new file mode 100644 (file)
index 0000000..c14484d
Binary files /dev/null and b/themes/default/images/escape.png differ
diff --git a/themes/default/images/gl.png b/themes/default/images/gl.png
new file mode 100644 (file)
index 0000000..7d863f9
Binary files /dev/null and b/themes/default/images/gl.png differ
diff --git a/themes/default/images/help.gif b/themes/default/images/help.gif
new file mode 100644 (file)
index 0000000..58c0ffe
Binary files /dev/null and b/themes/default/images/help.gif differ
diff --git a/themes/default/images/invoice.gif b/themes/default/images/invoice.gif
new file mode 100644 (file)
index 0000000..aef0be2
Binary files /dev/null and b/themes/default/images/invoice.gif differ
diff --git a/themes/default/images/login.gif b/themes/default/images/login.gif
new file mode 100644 (file)
index 0000000..bd3f47c
Binary files /dev/null and b/themes/default/images/login.gif differ
diff --git a/themes/default/images/money.png b/themes/default/images/money.png
new file mode 100644 (file)
index 0000000..42c52d0
Binary files /dev/null and b/themes/default/images/money.png differ
diff --git a/themes/default/images/ok.gif b/themes/default/images/ok.gif
new file mode 100644 (file)
index 0000000..c566bb8
Binary files /dev/null and b/themes/default/images/ok.gif differ
diff --git a/themes/default/images/pdf.gif b/themes/default/images/pdf.gif
new file mode 100644 (file)
index 0000000..071bb35
Binary files /dev/null and b/themes/default/images/pdf.gif differ
diff --git a/themes/default/images/print.png b/themes/default/images/print.png
new file mode 100644 (file)
index 0000000..a350d18
Binary files /dev/null and b/themes/default/images/print.png differ
diff --git a/themes/default/images/receive.gif b/themes/default/images/receive.gif
new file mode 100644 (file)
index 0000000..6a92dd2
Binary files /dev/null and b/themes/default/images/receive.gif differ
diff --git a/themes/default/images/remove.png b/themes/default/images/remove.png
new file mode 100644 (file)
index 0000000..08f2493
Binary files /dev/null and b/themes/default/images/remove.png differ
diff --git a/themes/default/images/report.png b/themes/default/images/report.png
new file mode 100644 (file)
index 0000000..779ad58
Binary files /dev/null and b/themes/default/images/report.png differ
diff --git a/themes/default/images/right.gif b/themes/default/images/right.gif
new file mode 100644 (file)
index 0000000..eed1194
Binary files /dev/null and b/themes/default/images/right.gif differ
diff --git a/themes/default/images/sort_asc.gif b/themes/default/images/sort_asc.gif
new file mode 100644 (file)
index 0000000..8055b42
Binary files /dev/null and b/themes/default/images/sort_asc.gif differ
diff --git a/themes/default/images/sort_desc.gif b/themes/default/images/sort_desc.gif
new file mode 100644 (file)
index 0000000..e0f93f7
Binary files /dev/null and b/themes/default/images/sort_desc.gif differ
diff --git a/themes/default/images/sort_none.gif b/themes/default/images/sort_none.gif
new file mode 100644 (file)
index 0000000..19cc2a2
Binary files /dev/null and b/themes/default/images/sort_none.gif differ
diff --git a/themes/default/images/view.gif b/themes/default/images/view.gif
new file mode 100644 (file)
index 0000000..3776aa7
Binary files /dev/null and b/themes/default/images/view.gif differ