From c103f51c3ec9c4469786b0c1ab9147c1b868791b Mon Sep 17 00:00:00 2001
From: Reinhold Kainhofer <reinhold@kainhofer.com>
Date: Tue, 21 Apr 2015 19:41:21 +0200
Subject: [PATCH] Properly implement custom variable replacements

---
 Makefile                                      |   2 +-
 fields/vmordernumberreplacements.php          | 191 ++++++++
 .../en-GB/en-GB.plg_vmshopper_ordernumber.ini |   3 +
 .../en-GB.plg_vmshopper_ordernumber.sys.ini   |   3 +
 ordernumber.php                               | 463 ++++++++++++++----
 ordernumber.script.php                        |  15 +
 ordernumber.xml                               |  15 +-
 ordernumber/assets/css/ordernumber.css        |  65 +++
 ordernumber/assets/js/ordernumber.js          |  48 ++
 9 files changed, 693 insertions(+), 112 deletions(-)
 create mode 100644 fields/vmordernumberreplacements.php

diff --git a/Makefile b/Makefile
index 43021a8..c2839b0 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 BASE=ordernumber
 PLUGINTYPE=vmshopper
-VERSION=2.2
+VERSION=3.0
 
 PLUGINFILES=$(BASE).php $(BASE).script.php $(BASE).xml index.html $(BASE)/
 
diff --git a/fields/vmordernumberreplacements.php b/fields/vmordernumberreplacements.php
new file mode 100644
index 0000000..b1506cd
--- /dev/null
+++ b/fields/vmordernumberreplacements.php
@@ -0,0 +1,191 @@
+<?php
+defined('_JEXEC') or die();
+/**
+ *
+ * @package    VirtueMart
+ * @subpackage Plugins  - Fields
+ * @author Reinhold Kainhofer, Open Tools
+ * @link http://www.open-tools.net
+ * @copyright Copyright (c) 2014 Reinhold Kainhofer. All rights reserved.
+ * @license http://www.gnu.org/copyleft/gpl.html GNU/GPL, see LICENSE.txt
+ * VirtueMart is free software. This version may have been modified pursuant
+ * to the GNU General Public License, and as distributed it includes or
+ * is derivative of works licensed under the GNU General Public License or
+ * other free or open source software licenses.
+ */
+ 
+defined('DS') or define('DS', DIRECTORY_SEPARATOR);
+if (!class_exists( 'VmConfig' )) 
+    require(JPATH_ROOT.DS.'administrator'.DS.'components'.DS.'com_virtuemart'.DS.'helpers'.DS.'config.php');
+VmConfig::loadConfig();
+
+class JFormFieldVmOrdernumberReplacements extends JFormField {
+    var $_name = 'vmOrdernumberReplacements';
+    static $pluginpath = '/plugins/vmshopper/ordernumber/ordernumber/';
+
+    
+    protected $countertype;
+
+    // VM2 on J2 works, VM3 on J3 works out of the box, but
+    // VM3 on J2 does NOT work by simply calling vmJsApi::jQuery, because
+    // the JS is never added to the page header, so we have to add this manually
+    protected function loadjQuery() {
+        vmJsApi::jQuery();
+        // TODO: jquery::ui available only in J3:
+        JHtml::_('jquery.ui', array('core', 'sortable'));
+        // If we are on Joomla 2.5 and VM 3, manually add the script declarations 
+        // cached in vmJsApi to the document header:
+        if (version_compare(JVERSION, '3.0', 'lt') && defined('VM_VERSION') && VM_VERSION>=3) {
+            $document = JFactory::getDocument();
+            $scripts = vmJsApi::getJScripts();
+            foreach ($scripts as $name => $jsToAdd) {
+                if($jsToAdd['written']) continue;
+                $file = $jsToAdd['script'] ? $jsToAdd['script'] : $name;
+
+                if(strpos($file,'/')!==0){
+                    $file = vmJsApi::setPath($file,false,'');
+                } else if(strpos($file,'//')!==0){
+                    $file = JURI::root(true).$file;
+                }
+
+                $ver = '';
+                if(!empty($jsToAdd['ver'])) $ver = '?vmver='.$jsToAdd['ver'];
+                $document->addScript( $file .$ver,"text/javascript",$jsToAdd['defer'],$jsToAdd['async'] );
+                vmJsApi::removeJScript($name);
+            }
+        }
+    }
+
+    static function img_url($file) {
+        return JURI::root(true) . self::$pluginpath . 'assets/images/' . $file;
+    }
+    static function css_url($file) {
+        return JURI::root(true) . self::$pluginpath . 'assets/css/' . $file;
+    }
+    static function js_url($file) {
+        return JURI::root(true) . self::$pluginpath . 'assets/js/' . $file;
+    }
+    static function __($string) {
+		return JText::_($string);
+    }
+    
+    protected function makeJSTranslationsAvailable() {
+//         JText::script('PLG_ORDERNUMBER_JS_JSONERROR');
+    }
+    protected function getInput() {
+    
+        $html=array();
+        $doc = JFactory::getDocument()->addStyleSheet(self::css_url('ordernumber.css'));
+        $doc->addScript( self::js_url('ordernumber.js'));
+        $this->makeJSTranslationsAvailable();
+        $this->loadjQuery();
+        
+        
+        $value = $this->value;
+//         $html[] = "<pre> value: ".print_r($value,1)."</pre>";
+//         $html[] = "<pre>Form Field: ".print_r($this,1)."</pre>";
+        $variables = array();
+        if (!is_array($value))
+            $value = array();
+        
+        if (!empty($value)) {
+            $keys = array_keys($value);
+            foreach (array_keys($value[$keys[0]]) as $i) {
+                $entry = array();
+                foreach ($keys as $k) {
+                    $entry[$k] = $value[$k][$i];
+                }
+                $variables[] = $entry;
+            }
+        }
+        
+        $id = $this->id;
+        $name = $this->name;
+        
+        
+//         $html[] = "<pre>Variables: ".print_r($variables,1)."</pre>";
+        $html[] = '<table id="ordernumber_variables_template" style="display:none">';
+        $html[] = $this->create_replacements_row_html($name, array(), 'disabled');
+        $html[] = '</table>';
+        
+        $html[] = '<table id="ordernumber_variables" class="ordernumber_variables widefat wc_input_table sortable" cellspacing="0">';
+        $columns = array(
+            'variables_ifvar'    => self::__( 'If variable ...'),
+            'variables_ifop'     => '',
+            'variables_ifval'    => self::__( 'Value'),
+            'variables_then'     => self::__( ''),
+            'variables_thenvar'  => self::__( 'Set variable ...'),
+            'variables_thenval'  => self::__( 'to value ...'),
+            'sort'     => '',
+            'variables_settings' => '',
+        );
+        $html[] = '	<thead>';
+        $html[] = '		<tr class="ordernumber_variables_header">';
+        foreach ( $columns as $key => $column ) {
+        	$html[] = '<th class="' . $key . '">' . htmlspecialchars( $column ) . '</th>';
+        }
+        $html[] = '		</tr>';
+        $html[] = '		<tr id="ordernumber-replacements-empty-row" class="oton-empty-row-notice ' . (empty($variables)?"":"rowhidden") . '">';
+        $html[] = '			<td class="oton-empty-row-notice" colspan="8">';
+        $html[] = '				<em>a' . self::__('No custom variables have been defined.') . '</em>';
+        $html[] = '				<input type="hidden" name="' . $name . '" value="" ' . (empty($variables))?'':'disabled' . '>';
+        $html[] = '			</td>';
+        $html[] = '		</tr>';
+        $html[] = '	</thead>';
+        $html[] = '	<colgroup>';
+        foreach ($columns as $key => $column) {
+        	$html[] = '<col class="' . $key . '" />';
+        }
+        $html[] = '	</colgroup>';
+        $html[] = '';
+        $html[] = '	<tbody>';
+        foreach ($variables as $var) {
+        	$html[] = $this->create_replacements_row_html($name, $var);
+        }
+        $html[] = '	</tbody>';
+        $html[] = '	<tfoot>';
+        $html[] = '		<tr class="addreplacement_row">';
+        $html[] = '			<td colspan=8 class="variable_add">';
+        $html[] = '				<div class="ordernumber-variables-addbtn ordernumber-btn" onClick="ordernumberVariablesAddRow(\'ordernumber_variables_template\', \'ordernumber_variables\')">';
+        $html[] = '					<div class="ordernumber-ajax-loading"><img src="' . self::img_url( 'icon-16-new.png' ) . '" class="ordernumber-counter-addbtn" /></div>';
+        $html[] = self::__('Add new custom variable');
+        $html[] = '				</div>';
+        $html[] = '			</td>';
+        $html[] = '		</tr>';
+        $html[] = '	</tfoot>';
+        $html[] = '</table>';
+        return implode("\n", $html);
+    }
+    
+    protected function create_replacements_row_html($name, $values = array(), $disabled = '') {
+        $operator = (isset($values['conditionop'])?$values['conditionop']:'');
+        $operators = array(
+            'equals'       => '=', 
+            'contains'     => self::__('contains'), 
+            'smaller'      => '<',
+            'smallerequal' => '<=',
+            'larger'       => '>',
+            'largerequal'  => '>=', 
+            'startswith'   => self::__('starts with'),
+            'endswith'     => self::__('ends with'),
+        );
+        $html  = '
+        <tr>
+        	<td class="variables_ifvar"><input name="' . $name . '[conditionvar][]" value="' . (isset($values['conditionvar'])?$values['conditionvar']:'') . '" ' . $disabled . '/></td>
+        	<td class="variables_ifop"      ><select name="' . $name . '[conditionop][]" ' . $disabled . ' style="width: 100px">';
+        foreach ($operators as $op => $opname) {
+        	$html .= '		<option value="' . $op . '" ' . (($op === $operator)?'selected':'') . '>' . htmlspecialchars($opname) . '</option>';
+        }
+        $html .= '</select></td>
+        	<td class="variables_ifval"   ><input name="' . $name . '[conditionval][]" value="' . (isset($values['conditionval'])?$values['conditionval']:'') . '" ' . $disabled . '/></td>
+        	<td class="variables_then">=></td>
+        	<td class="variables_thenvar"><input name="' . $name . '[newvar][]"       value="' . (isset($values['newvar'])?$values['newvar']:'') .       '" ' . $disabled . '/></td>
+        	<td class="variables_thenval"><input name="' . $name . '[newval][]"       value="' . (isset($values['newval'])?$values['newval']:'') .       '" ' . $disabled . '/></td>
+        	<td class="sort"></td>
+        	<td class="variables_settings"><img src="' . self::img_url( 'icon-16-delete.png' ) . '" class="ordernumber-replacement-deletebtn ordernumber-btn"></td>
+        </tr>';
+        return $html;
+    }
+
+    
+}
\ No newline at end of file
diff --git a/language/en-GB/en-GB.plg_vmshopper_ordernumber.ini b/language/en-GB/en-GB.plg_vmshopper_ordernumber.ini
index 27b1e03..17d801d 100644
--- a/language/en-GB/en-GB.plg_vmshopper_ordernumber.ini
+++ b/language/en-GB/en-GB.plg_vmshopper_ordernumber.ini
@@ -52,9 +52,12 @@ PLG_ORDERNUMBER_COUNTERLIST_HEADER_VALUE="Counter value"
 PLG_ORDERNUMBER_COUNTERLIST_ADD="Add new counter"
 PLG_ORDERNUMBER_COUNTERLIST_EXISTS="Counter '%s' already exists."
 
+PLG_ORDERNUMBER_REPLACEMENTS_DESC="Here you can define contingent custom variables, which you can then use in the number format like any pre-defined variable."
+
 PLG_ORDERNUMBER_FIELDSET_ORDERNUMBER="Order Numbers"
 PLG_ORDERNUMBER_FIELDSET_INVOICENUMBER="Invoice Numbers"
 PLG_ORDERNUMBER_FIELDSET_CUSTOMERNUMBER="Customer Numbers"
+PLG_ORDERNUMBER_FIELDSET_REPLACEMENTS="Custom Variables"
 
 PLG_ORDERNUMBER_JS_NOT_AUTHORIZED="You are not authorized to modify order number counters."
 PLG_ORDERNUMBER_JS_NEWCOUNTER="Please enter the format/name of the new counter:"
diff --git a/language/en-GB/en-GB.plg_vmshopper_ordernumber.sys.ini b/language/en-GB/en-GB.plg_vmshopper_ordernumber.sys.ini
index 27b1e03..17d801d 100644
--- a/language/en-GB/en-GB.plg_vmshopper_ordernumber.sys.ini
+++ b/language/en-GB/en-GB.plg_vmshopper_ordernumber.sys.ini
@@ -52,9 +52,12 @@ PLG_ORDERNUMBER_COUNTERLIST_HEADER_VALUE="Counter value"
 PLG_ORDERNUMBER_COUNTERLIST_ADD="Add new counter"
 PLG_ORDERNUMBER_COUNTERLIST_EXISTS="Counter '%s' already exists."
 
+PLG_ORDERNUMBER_REPLACEMENTS_DESC="Here you can define contingent custom variables, which you can then use in the number format like any pre-defined variable."
+
 PLG_ORDERNUMBER_FIELDSET_ORDERNUMBER="Order Numbers"
 PLG_ORDERNUMBER_FIELDSET_INVOICENUMBER="Invoice Numbers"
 PLG_ORDERNUMBER_FIELDSET_CUSTOMERNUMBER="Customer Numbers"
+PLG_ORDERNUMBER_FIELDSET_REPLACEMENTS="Custom Variables"
 
 PLG_ORDERNUMBER_JS_NOT_AUTHORIZED="You are not authorized to modify order number counters."
 PLG_ORDERNUMBER_JS_NEWCOUNTER="Please enter the format/name of the new counter:"
diff --git a/ordernumber.php b/ordernumber.php
index 5ce7998..9b6facd 100644
--- a/ordernumber.php
+++ b/ordernumber.php
@@ -38,20 +38,21 @@ class plgVmShopperOrdernumber extends vmShopperPlugin {
     // We don't need this function, but the parent class declares it abstract, so we need to overload
     function plgVmOnUpdateOrderBEShopper($_orderID) {}
     
-    function _getCounter($nrtype, $format) {
+   function _getCounter($nrtype, $format, $default=0) {
         $db = JFactory::getDBO();
+        
         /* prevent sql injection attacks by escaping the user-entered format! Empty for global counter... */
         /* For global counting, simply read the empty number_format entries! */
-        $q = 'SELECT `count` FROM `'.$this->_tablename.'` WHERE `number_type`='.(int)$nrtype.' AND `number_format`='.$db->quote($format);
+        $q = 'SELECT `count` FROM `'.$this->_tablename.'` WHERE `number_type`='.$db->quote($nrtype).' AND `number_format`='.$db->quote($format);
         $db->setQuery($q);
         $existing = $db->loadResult();
-        $count = $existing?$existing:0;
+        $count = $existing?$existing:$default;
         return $count;
     }
     
     function _counterExists($nrtype, $format) {
         $db = JFactory::getDBO();
-        $q = 'SELECT `count` FROM `'.$this->_tablename.'` WHERE `number_type`='.(int)$nrtype.' AND `number_format`='.$db->quote($format);
+        $q = 'SELECT `count` FROM `'.$this->_tablename.'` WHERE `number_type`='.$db->quote($nrtype).' AND `number_format`='.$db->quote($format);
         $db->setQuery($q);
         return ($db->loadResult() != null);
     }
@@ -59,7 +60,7 @@ class plgVmShopperOrdernumber extends vmShopperPlugin {
     // Insert new counter value into the db
     function _addCounter($nrtype, $format, $value) {
         $db = JFactory::getDBO();
-        $q = 'INSERT INTO `'.$this->_tablename.'` (`count`, `number_type`, `number_format`) VALUES ('.(int)$value.','.(int)$nrtype.', '.$db->quote($format).')';
+        $q = 'INSERT INTO `'.$this->_tablename.'` (`count`, `number_type`, `number_format`) VALUES ('.(int)$value.','.$db->quote($nrtype).', '.$db->quote($format).')';
         $db->setQuery( $q );
         $db->query();
         return $db->getAffectedRows();
@@ -68,7 +69,7 @@ class plgVmShopperOrdernumber extends vmShopperPlugin {
     // Insert new counter value into the db or update existing one
     function _setCounter($nrtype, $format, $value) {
         $db = JFactory::getDBO();
-        $q = 'UPDATE `'.$this->_tablename.'` SET `count`= "'.(int)$value.'" WHERE `number_type`='.(int)$nrtype.' AND `number_format`='.$db->quote($format);
+        $q = 'UPDATE `'.$this->_tablename.'` SET `count`= "'.(int)$value.'" WHERE `number_type`='.$db->quote($nrtype).' AND `number_format`='.$db->quote($format);
         $db->setQuery( $q );
         $db->query();
         if ($db->getAffectedRows()<1) {
@@ -82,7 +83,7 @@ class plgVmShopperOrdernumber extends vmShopperPlugin {
     function _deleteCounter($nrtype, $format) {
         $db = JFactory::getDBO();
         $format = $db->escape ($format);
-        $q = 'DELETE FROM `'.$this->_tablename.'` WHERE `number_type`='.(int)$nrtype.' AND `number_format`='.$db->quote($format);
+        $q = 'DELETE FROM `'.$this->_tablename.'` WHERE `number_type`='.$db->quote($nrtype).' AND `number_format`='.$db->quote($format);
         $db->setQuery( $q );
         $db->query();
         return $db->getAffectedRows();
@@ -125,106 +126,361 @@ class plgVmShopperOrdernumber extends vmShopperPlugin {
 
 
 
-    /* Type 0 means order number, type 1 means invoice number, type 2 means customer number, 3 means order password */
-    function replace_fields ($fmt, $nrtype, $details) {
-        // First, replace all randomXXX[n] fields. This needs to be done with a regexp and a callback:
-        $fmt = preg_replace_callback ('/\[(random)(.*?)([0-9]*?)\]/', array($this, 'replaceRandom'), $fmt);
-        
-        $reps = array (
-            "[year]" => date ("Y"),
-            "[year2]" => date ("y"),
-            "[month]" => date("m"),
-            "[day]" => date("d"),
-            "[hour]" => date("H"),
-            "[hour12]" => date("h"),
-            "[ampm]" => date("a"),
-            "[minute]" => date("i"),
-            "[second]" => date("s"),
-            "[userid]" => $details->virtuemart_user_id
-        );
-        if (isset($details->virtuemart_vendor_id)) $reps["[vendorid]"] = $details->virtuemart_vendor_id;
+    protected function setupDateTimeReplacements (&$reps, $details, $nrtype) {
+    	$utime = microtime(true);
+    	$reps["[year]"] = date ("Y", $utime);
+    	$reps["[year2]"] = date ("y", $utime);
+    	$reps["[month]"] = date("m", $utime);
+    	$reps["[day]"] = date("d", $utime);
+    	$reps["[hour]"] = date("H", $utime);
+    	$reps["[hour12]"] = date("h", $utime);
+    	$reps["[ampm]"] = date("a", $utime);
+    	$reps["[minute]"] = date("i", $utime);
+    	$reps["[second]"] = date("s", $utime);
+    	$milliseconds = (int)(1000*($utime - (int)$utime));
+    	$millisecondsstring = sprintf('%03d', $milliseconds);
+    	$reps["[decisecond]"] = $millisecondsstring[0];
+    	$reps["[centisecond]"] = substr($millisecondsstring, 0, 2);
+    	$reps["[millisecond]"] = $millisecondsstring;
+    }
+
+    protected function setupStoreReplacements (&$reps, $details, $nrtype) {
+        if (isset($details->virtuemart_vendor_id)) 
+            $reps["[vendorid]"] = $details->virtuemart_vendor_id;
+    }
+    
+    protected function setupAddressReplacements(&$reps, $prefix, $details, $nrtype) {
+        if (isset($details->email))       $reps["[email]"] = $details->email;
+        if (isset($details->title))       $reps["[title]"] = $details->title;
+        if (isset($details->first_name))  $reps["[firstname]"] = $details->first_name;
+        if (isset($details->middle_name)) $reps["[middlename]"] = $details->middle_name;
+        if (isset($details->last_name))   $reps["[lastname]"] = $details->last_name;
+
+        if (isset($details->company))     $reps["[company]"] = $details->company;
+        if (isset($details->zip)) {
+            $reps["[zip]"] = $details->zip;
+            $reps["[postcode]"] = $details->zip;
+        }
+        if (isset($details->city))        $reps["[city]"] = $details->city;
         
-        if ($nrtype==0 or $nrtype == 1) { // Order nr and Invoice nr
-            if (isset($details->ip_address)) $reps["[ipaddress]"] = $details->ip_address;
+        if (isset($details->virtuemart_country_id)) {
+            $reps["[countryid]"] = $details->virtuemart_country_id;
+            $country = $this->getCountryFromID ($details->virtuemart_country_id);
+            if ($country) {
+                $reps["[country]"] = $country->country_name;
+                $reps["[countrycode2]"] = $country->country_2_code;
+                $reps["[countrycode3]"] = $country->country_3_code;
+            }
         }
-        if ($nrtype==1 or $nrtype==2) { // Invoice nr and Customer nr
-            if (isset($details->email)) $reps["[email]"] = $details->email;
-            if (isset($details->title)) $reps["[title]"] = $details->title;
-            if (isset($details->first_name)) $reps["[firstname]"] = $details->first_name;
-            if (isset($details->middle_name)) $reps["[middlename]"] = $details->middle_name;
-            if (isset($details->last_name)) $reps["[lastname]"] = $details->last_name;
 
-            if (isset($details->company)) $reps["[company]"] = $details->company;
-            if (isset($details->zip)) $reps["[zip]"] = $details->zip;
-            if (isset($details->city)) $reps["[city]"] = $details->city;
+        if (isset($details->virtuemart_state_id)) {
+            $reps["[stateid]"] = $details->virtuemart_state_id;
+            // TODO: Also extract the state name and abbreviations
+        }
+    }
+    
+    protected function setupOrderReplacements (&$reps, $details, $nrtype) {
+        // Customer numbers are created before any order is submitted, so we don't have any information available.
+        if ($nrtype=='customer_number') 
+			return;
+        // Only for Invoice:
+        if ($nrtype != 'order_number' && isset($details->order_number)) 
+            $reps["[ordernumber]"] = $details->order_number;
+        if (isset($details->virtuemart_order_id)) 
+            $reps["[orderid]"] = $details->virtuemart_order_id;
+        if (isset($details->order_status)) 
+            $reps["[orderstatus]"] = $details->order_status;
+            
         
-            if (isset($details->virtuemart_country_id)) $reps["[countryid]"] = $details->virtuemart_country_id;
-            if (isset($details->virtuemart_country_id)) {
-                $country = $this->getCountryFromID ($details->virtuemart_country_id);
-                if ($country) {
-                    $reps["[country]"] = $country->country_name;
-                    $reps["[countrycode2]"] = $country->country_2_code;
-                    $reps["[countrycode3]"] = $country->country_3_code;
+        $this->setupAddressReplacements($reps, "", $details, $nrtype);
+    
+// print("<pre>details for $nrtype: ".print_r($details,1)."</pre>");
+		if (isset($details->order_total))		$reps['[ordertotal]'] = $details->order_total;
+		if (isset($details->order_total))		$reps['[amount]'] = $details->order_total;
+		if (isset($details->order_subtotal))	$reps['[ordersubtotal]'] = $details->order_total;
+		if (isset($details->order_tax))			$reps['[ordersubtotal]'] = $details->order_tax;
+		if (isset($details->order_shipment))	$reps['[ordershipment]'] = $details->order_shipment;
+		if (isset($details->order_payment))		$reps['[orderpayment]'] = $details->order_payment;
+		if (isset($details->order_discount))	$reps['[orderdiscount]'] = $details->order_discount;
+		
+		$articles = 0;
+		$products = 0;
+		$skus = array();
+		$categories = array();
+		$menufacturers = array();
+		$vendors = array();
+		
+		// If we have a virtuemart_order_id already, load that order,
+		// otherwise assume the order is still in the cart
+		$items = array();
+		if (isset($details->virtuemart_order_id)) {
+			$orderModel = VmModel::getModel('orders');
+			$productModel = VmModel::getModel('product');
+			$order = (object)$orderModel->getOrder($details->virtuemart_order_id);
+// print("<pre>Order is: ".print_r($order,1)."</pre>");
+			foreach ($order->items as $i) {
+				$articles += $i->product_quantity;
+				$products += 1;
+
+				$p = $productModel->getProduct($i->virtuemart_product_id);
+// print("<pre>Order product is: ".print_r($p,1)."</pre>");
+				$skus[$p->product_sku] = 1;
+				foreach ($p->categories as $c) {
+					$categories[$c] = 1;
+				}
+				foreach ($p->virtuemart_manufacturer_id as $m) {
+					$manufacturers[$m] = 1;
+				}
+				$vendors[$p->virtuemart_vendor_id] = 1;
+			}
+		} else {
+			$cart = VirtueMartCart::getCart();
+// print("<pre>Cart is: ".print_r($cart,1)."</pre>");
+			foreach ($cart->products as $p) {
+				$articles += $p->quantity;
+				$products += 1;
+				$skus[$p->product_sku] = 1;
+				foreach ($p->categories as $c) {
+					$categories[$c] = 1;
+				}
+				foreach ($p->virtuemart_manufacturer_id as $m) {
+					$manufacturers[$m] = 1;
+				}
+				$vendors[$p->virtuemart_vendor_id] = 1;
+			}
+		}
+		$reps["[articles]"] = $articles;
+		$reps["[products]"] = $products;
+		$reps["[skus]"] = array_keys($skus);
+		$reps["[categories]"] = array_keys($categories);
+		$reps["[manufacturers]"] = array_keys($menufacturers);
+		$reps["[vendors]"] = array_keys($vendors);
+		
+    }
+   
+
+    protected function setupUserReplacements (&$reps, $details, $nrtype) {
+        $reps["[userid]"]      = $details->virtuemart_user_id;
+        if (isset($details->ip_address))     $reps["[ipaddress]"] = $details->ip_address;
+        // Customer number:
+        if (isset($details->username))       $reps["[username]"] = $details->username;
+        if (isset($details->name))           $reps["[name]"] = $details->name;
+        if (isset($details->user_is_vendor)) $reps["[user_is_vendor]"] = $details->user_is_vendor;
+    }
+    
+    protected function setupShippingReplacements(&$reps, $order, $nrtype) {
+		if (isset($details->virtuemart_paymentmethod_id))		$reps['[paymentmethod]'] = $details->virtuemart_paymentmethod_id;
+		if (isset($details->virtuemart_shipmentmethod_id))		$reps['[shipmentmethod]'] = $details->virtuemart_shipmentmethod_id;
+//         $reps["[shippingmethod]"] = $order->getShippingMethod();
+    }
+    
+    protected function setupThirdPartyReplacements (&$reps, $details, $nrtype) {
+        JPluginHelper::importPlugin('vmshopper');
+        JDispatcher::getInstance()->trigger('onVmOrdernumberGetVariables',array(&$reps, $nrtype, $details));
+    }
+    
+    protected function setupReplacements($nrtype, $details) {
+        $reps = array();
+        $this->setupDateTimeReplacements($reps, $details, $nrtype);
+        $this->setupStoreReplacements($reps, $details, $nrtype);
+        $this->setupOrderReplacements($reps, $details, $nrtype);
+        $this->setupUserReplacements($reps, $details, $nrtype);
+        $this->setupShippingReplacements($reps, $details, $nrtype);
+        $this->setupThirdPartyReplacements($reps, $details, $nrtype);
+// print("<pre>All replacements: ".print_r($reps,1)."</pre>");
+        return $reps;
+    }
+
+    protected function setupCustomVariables ($nrtype, $order, $reps, $customvars) {
+print("<pre>Custom Variables: ".print_r($customvars,1)."</pre>");
+        foreach ($customvars as $c) {
+            $conditionvar = strtolower($c['conditionvar']);
+            $op = $c['conditionop'];
+            
+            $found = false;
+            $match = false;
+            $compareval = null;
+            
+            if (isset($reps[$conditionvar])) {
+                $found = true;
+                $compareval = $reps[$conditionvar];
+            } elseif (isset($reps['['.$conditionvar.']'])) {
+                $found = true;
+                $compareval = $reps['['.$conditionvar.']'];
+            }/* elseif ($order && $compareval = $order->getData($conditionvar)) {
+                // TODO: Handle order property
+                $found = true;
+            }*/ else {
+                // TODO: Handly other possible properties!
+                // TODO: Print out warning that variable could not be found.
+            }
+            if ($found) {
+                $condval = $c['conditionval'];
+                switch ($op) {
+                    case 'nocondition':
+                            $match = true; break;
+                    case 'equals': 
+                            $match = ($compareval == $condval); break;
+                    case 'contains':
+                            if (is_array($compareval)) {
+                                $match = in_array($condval, $compareval);
+                            } else {
+                                $match = strpos ($compareval, $condval);
+                            }
+                            break;
+                    case 'smaller':
+                            $match = ($compareval<$condval); break;
+                    case 'smallerequal':
+                            $match = ($compareval<=$condval); break;
+                    case 'larger':
+                            $match = ($compareval>$condval); break;
+                    case 'largerequal':
+                            $match = ($compareval>=$condval); break;
+                    case 'startswith':
+                            $match = (substr("$compareval", 0, strlen("$condval")) === "$condval"); break;
+                    case 'endswith':
+                            $match = (substr("$compareval", -strlen("$condval")) === "$condval"); break;
                 }
+            } elseif (empty($conditionvar)) {
+                $match = true;
+            }
+            if ($match) {
+                $varname = '['.strtolower($c['newvar']).']';
+                $reps[$varname] = $c['newval'];
             }
-
-            if (isset($details->virtuemart_state_id)) $reps["[stateid]"] = $details->virtuemart_state_id;
-        }
-        if ($nrtype==1) {
-            // Only for Invoice:
-            if (isset($details->order_number)) $reps["[ordernumber]"] = $details->order_number;
-            if (isset($details->virtuemart_order_id)) $reps["[orderid]"] = $details->virtuemart_order_id;
-            if (isset($details->order_status)) $reps["[orderstatus]"] = $details->order_status;
         }
-        if ($nrtype==2) {
-            // Customer number:
-            if (isset($details->username)) $reps["[username]"] = $details->username;
-            if (isset($details->name)) $reps["[name]"] = $details->name;
-            if (isset($details->user_is_vendor)) $reps["[user_is_vendor]"] = $details->user_is_vendor;
+        return $reps;
+    }
+
+    // Allow the user to override the format like any other custom variable:
+    protected function setupNumberFormatString($fmt, $type, $order, $reps) {
+        if (isset($reps['['.$type.'_format]'])) {
+            return $reps['['.$type.'_format]'];
+        } else {
+            return $fmt;
         }
-        JPluginHelper::importPlugin('vmshopper');
-        JDispatcher::getInstance()->trigger('onVmOrdernumberGetVariables',array(&$reps, $fmt, $nrtype, $details));
+    }
+    
+    protected function doReplacements ($fmt, $reps) {
+        // First, replace all random...[n] fields. This needs to be done with a regexp and a callback:
+        $fmt = preg_replace_callback ('/\[(random)(.*?)([0-9]*?)\]/', array($this, 'replaceRandom'), $fmt);
+        // Only use string-valued variables for replacement (array-valued variables can be used in custom variable definitions!)
+        $reps = array_filter($reps, function($v) { return !is_array($v);} );
         return str_ireplace (array_keys($reps), array_values($reps), $fmt);
     }
+    
+    protected function extractCounterSettings ($fmt, $type, $ctrsettings) {
+		// First, extract all counter settings, i.e. all strings of the form [#####:startval/increment] or [####/increment:startval]
+		$regexp = '%\[(#+)(/([0-9]+))?(:([0-9]+))?(/([0-9]+))?\]%';
+		
+		if (preg_match($regexp, $fmt, $counters)) {
+			// $counters is an array of the form:
+			// Array (
+			// 		[0] => [#####:100/3]
+			// 		[1] => #####
+			// 		[2] => 
+			// 		[3] => 
+			// 		[4] => :100
+			// 		[5] => 100
+			// 		[6] => /3
+			// 		[7] => 3
+			// )
+			$ctrsettings["${type}_padding"] = strlen($counters[1]);
+			if (!empty($counters[2])) {
+				// $counters[2] contains the whole "/n" part, while $counters[3] contains just the step itself
+				$ctrsettings["${type}_step"] = $counters[3]; 
+			}
+			if (!empty($counters[4])) {
+				// $counters[4] contains the whole ":n" part, while $counters[5] contains just the start value itself
+				$ctrsettings["${type}_start"] = $counters[5]; 
+			}
+			
+			if (!empty($counters[6])) {
+				// $counters[6] contains the whole ":n" part, while $counters[7] contains just the start value itself
+				$ctrsettings["${type}_step"] = $counters[7]; 
+			}
+			
+			$fmt = preg_replace($regexp, "#", $fmt);
+		}
+		// Split at a | to get the number format and a possibly different counter increment format
+		// If a separate counter format is given after the |, use it, otherwise reuse the number format itself as counter format
+		$parts = explode ("|", $fmt);
+		$ctrsettings["${type}_format"] = $parts[0];
+		$ctrsettings["${type}_counter"] = ($ctrsettings["${type}_global"]=='yes')?"":$parts[(count($parts)>1)?1:0];
+		
+		return $ctrsettings;
+	}
 
-    /* Type 0 means order number, type 1 means invoice number, type 2 means customer number */
-    function format_number ($fmt, $details, $nrtype = 0, $global = 1, $padding = 1) {
-        // First, replace all variables:
-        $nr = $this->replace_fields ($fmt, $nrtype, $details);
+    /* replace the variables in the given format. $type indicates the type of number. */
+    function createNumber ($fmt, $type, $order, $customvars, $ctrsettings) {
+        $reps   = $this->setupReplacements ($type, $order);
+        $reps   = $this->setupCustomVariables ($type, $order, $reps, $customvars);
+        $format = $this->setupNumberFormatString($fmt, $type, $order, $reps);
+        $format = $this->doReplacements($format, $reps);
+        $ctrsettings = $this->extractCounterSettings ($format, $type, $ctrsettings);
 
-        // Split at a | to get the number format and a possibly different counter increment format
-        // If a separate counter format is given after the |, use it, otherwise reuse the number format itself as counter format
-        $parts = explode ("|", $nr);
-        $format = $parts[0];
+        // Increment the counter only if the format contains a placeholder for it!
+        if (strpos($ctrsettings["${type}_format"], "#") !== false) {
+            $countername = $ctrsettings["${type}_counter"];
+            // Look up the current counter
+            $count = $this->_getCounter($type, $countername, $ctrsettings["${type}_start"] - $ctrsettings["${type}_step"]) + $ctrsettings["${type}_step"];
+            $this->_setCounter($type, $countername, $count);
+            // return the format with the counter inserted
+            $number = str_replace ("#", sprintf('%0' . $ctrsettings["${type}_padding"] . 's', $count), $ctrsettings["${type}_format"]);
+        } else {
+            $number = $ctrsettings["${type}_format"];
+        }
+        return $number;
+    }
+    
+    function assignNumber($order, $type='ordernumber', $default="#") {
+        if ($this->params->get('customize_'.$type, 0)) {
+            $fmt     = $this->params->get ($type.'_format',  $default);
+            $ctrsettings = array(
+                "${type}_format"  => '',
+                "${type}_counter" => '',
+                "${type}_global"  => $this->params->get ($type.'_global',  0),
+                "${type}_padding" => $this->params->get ($type.'_padding',  0),
+                "${type}_step"    => 1,
+                "${type}_start"   => 1,
+            );
+            $cvar = $this->params->get ('replacements', array());
+            // Even though the replacements are created and stored as an array, they are retrieved as a stdClass object:
+            if (is_object($cvar)) $cvar = (array)$cvar;
+            if (!is_array($cvar))
+                $cvar = array();
+            // The customvars are stored in transposed form (for technical reasons, since there is no trigger 
+            // called when the corresponding form field from the plugin param is saved)
+            $customvars = array();
         
-        $counterfmt = ($global==1)?"":$parts[(count($parts)>1)?1:0];
+            if (!empty($cvar)) {
+                $keys = array_keys($cvar);
+                foreach (array_keys($cvar[$keys[0]]) as $i) {
+                    $entry = array();
+                    foreach ($keys as $k) {
+                        $entry[$k] = $cvar[$k][$i];
+                    }
+                    $customvars[] = $entry;
+                }
+            }
         
-        // Look up the current counter
-        $count = $this->_getCounter($nrtype, $counterfmt) + 1;
-        $this->_setCounter($nrtype, $counterfmt, $count);
 
-        // return the format with the counter inserted
-        return str_replace ("#", sprintf('%0' . $padding . 's', $count), $format);
+            $number = $this->createNumber ($fmt, $type, $order, $customvars, $ctrsettings);
+            return $number;
+        } else {
+            return false;
+        }
     }
 
-
     function plgVmOnUserOrder(&$orderDetails/*,&$data*/) {
-        // Is order number customization enabled?
-        if ($this->params->get('customize_order_number')) {
-          $nrtype = 0; /*order-nr*/
-          $fmt = $this->params->get ('order_number_format', "#");
-          $global = $this->params->get ('order_number_global', 1);
-          $padding = $this->params->get ('order_number_padding', 1);
-          $ordernr = $this->format_number ($fmt, $orderDetails, $nrtype, $global, $padding);
+        $ordernumber = $this->assignNumber($orderDetails, 'order_number', "#");
+        if ($ordernumber !== false) {
           // TODO: Check if ordernr already exists
-          $orderDetails->order_number = $ordernr;
+          $orderDetails->order_number = $ordernumber;
         }
-        // Is order password customization enabled?
-        if ($this->params->get('customize_order_password')) {
-          $nrtype = 3; /* order password */
-          $fmt = $this->params->get ('order_password_format', "[randomHex8]");
-          $passwd = $this->replace_fields ($fmt, $nrtype, $orderDetails);
-          $orderDetails->order_pass = $passwd;
+        $orderpwd = $this->assignNumber($orderDetails, 'order_password', "[randomHex8]");
+        if ($orderpwd !== false) {
+          $orderDetails->order_pass = $orderpwd;
         }
     }
 
@@ -237,14 +493,12 @@ class plgVmShopperOrdernumber extends vmShopperPlugin {
             $pdfInvoice = (int)VmConfig::get('pdf_invoice', 0); // backwards compatible
             $force_create_invoice = JFactory::getApplication()->input->getInt('create_invoice', 0);
             if ( in_array($orderDetails['order_status'],$orderstatusForInvoice)  or $pdfInvoice==1  or $force_create_invoice==1 ){
-                $nrtype = 1; /*invoice-nr*/
-                $fmt = $this->params->get ('invoice_number_format', "#");
-                $global = $this->params->get ('invoice_number_global', 1);
-                $padding = $this->params->get ('invoice_number_padding', 1);
-                $invoicenr = $this->format_number ($fmt, (object)$orderDetails, $nrtype, $global, $padding);
-                // TODO: Check if ordernr already exists
-                $data['invoice_number'] = $invoicenr;
-                return $data;
+                $invoicenr = $this->assignNumber((object)$orderDetails, 'invoice_number', "#");
+                if ($invoicenr !== false) {
+                    // TODO: Check if ordernr already exists
+                    $data['invoice_number'] = $invoicenr;
+                    return $data;
+                }
             }
         }
     }
@@ -252,19 +506,16 @@ class plgVmShopperOrdernumber extends vmShopperPlugin {
     // Customizing the customer numbers requires VM >= 2.0.15b, earlier versions 
     // left out the & and thus didn't allow changing the user data
     function plgVmOnUserStore(&$data) {
-        // Is order number customization enabled?
-        if ($this->params->get('customize_customer_number') && isset($data['customer_number_bycore']) && $data['customer_number_bycore']==1) {
-            $nrtype = 2; /*customer-nr*/
-            $fmt = $this->params->get ('customer_number_format', "#");
-            $global = $this->params->get ('customer_number_global', 1);
-            $padding = $this->params->get ('customer_number_padding', 1);
-            $customernr = $this->format_number ($fmt, (object)$data, $nrtype, $global, $padding);
-            // TODO: Check if ordernr already exists
-            $data['customer_number'] = $customernr;
-            return $data;
+        if (isset($data['customer_number_bycore']) && $data['customer_number_bycore']==1) {
+            $customernr = $this->assignNumber((object)$data, 'customer_number', "#");
+            if ($customernr !== false) {
+                // TODO: Check if ordernr already exists
+                $data['customer_number'] = $customernr;
+                return $data;
+            }
         }
     }
-
+ 
 
     /**
      * plgVmOnSelfCallBE ... Called to execute some plugin action in the backend (e.g. set/reset dl counter, show statistics etc.)
diff --git a/ordernumber.script.php b/ordernumber.script.php
index a950ff7..4e2fd10 100644
--- a/ordernumber.script.php
+++ b/ordernumber.script.php
@@ -75,6 +75,21 @@ class plgVmShopperOrdernumberInstallerScript
      */
     public function update(JAdapterInstance $adapter)
     {
+        $db = JFactory::getDBO();
+        $db->setQuery('ALTER TABLE `#__virtuemart_shopper_plg_ordernumber` CHANGE `number_type` `number_type` VARCHAR(30) NULL DEFAULT NULL;');
+        $db->query();
+        
+        $countertypes = array(
+            'order_number' => 0,
+            'invoice_number' => 1,
+            'customer_number' => 2,
+            'order_password' => 3,
+        );
+        foreach ($countertypes as $new => $old) {
+            $db->setQuery('update `#__virtuemart_shopper_plg_ordernumber` SET `number_type`="'.$new.'" WHERE `number_type`='.(int)$old.';');
+            $db->query();
+        }
+    }
 //         jimport( 'joomla.filesystem.file' ); 
 //         $file = JPATH_ROOT . DS . "administrator" . DS . "language" . DS . "en-GB" . DS . "en-GB.plg_vmshopper_ordernumber.sys.ini";
 //         if (JFile::exists($file)) JFile::delete($file); 
diff --git a/ordernumber.xml b/ordernumber.xml
index 1e9f956..2a2995c 100644
--- a/ordernumber.xml
+++ b/ordernumber.xml
@@ -7,10 +7,10 @@
     <authorUrl>http://www.open-tools.net/</authorUrl>
     <copyright>Copyright (C) 2012-2014 Reinhold Kainhofer. All rights reserved.</copyright>
     <license>http://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3</license>
-    <version>2.2</version>
-    <releaseDate>2014-12-07</releaseDate>
-    <releaseType>Minor update</releaseType>
-    <downloadUrl>http://www.open-tools.net/virtuemart-2-extensions/vm2-ordernumber-plugin.html</downloadUrl>
+    <version>3.0</version>
+    <releaseDate>2015-12-07</releaseDate>
+    <releaseType>Major update</releaseType>
+    <downloadUrl>http://open-tools.net/virtuemart/advanced-ordernumbers.html</downloadUrl>
 
     <description>VMSHOPPER_ORDERNUMBER_DESC</description>
 
@@ -83,7 +83,12 @@
                 </field>
                 <field name="customer_number_allcounters" type="VmOrdernumberCounters" label="PLG_ORDERNUMBER_ORDERNR_ALLCOUNTERS" countertype="customer_number" showon="customize_customer_number:1" />
             </fieldset>
+
+            <fieldset name="replacements" label="PLG_ORDERNUMBER_FIELDSET_REPLACEMENTS">
+                <field name="replacements_options" type="spacer" label="PLG_ORDERNUMBER_REPLACEMENTS_DESC" />
+                <field name="replacements" type="VmOrdernumberReplacements" label="" />
+            </fieldset>
         </fields>
     </config>
-    
+
 </extension>
diff --git a/ordernumber/assets/css/ordernumber.css b/ordernumber/assets/css/ordernumber.css
index 140beb3..27ecd74 100644
--- a/ordernumber/assets/css/ordernumber.css
+++ b/ordernumber/assets/css/ordernumber.css
@@ -31,3 +31,68 @@ div.ordernumber-ajax-loading, div.ordernumber-ajax-loading img.vmordernumber-btn
 div.ordernumber-ajax-loading img {
     z-index:0;
 }
+
+
+
+/*  Counter custom variable replacements */
+table.ordernumber_variables {
+    border: 1px solid #888888;
+	width: inherit;
+}
+
+table.ordernumber_variables td, table.ordernumber_variables th {
+    padding: 0px;
+	vertical-align: middle;
+}
+/* table.ordernumber_variables td.sort:before { */
+/*     float: none; */
+/* 	display: inline-block; */
+/* } */
+
+table.ordernumber_variables thead th {
+    text-align: center;
+	width: auto;
+}
+td.counter_value {
+    text-align: center;
+}
+table.ordernumber_variables input {
+	background-color: rgba(255,255,255,0.75);
+}
+
+table.ordernumber_variables thead > tr:nth-child(odd) > th,
+table.ordernumber_variables tfoot > tr.addreplacement_row > td {
+    background: #E0E0E0;
+}
+table.ordernumber_variables tbody > tr:nth-child(even) > td {
+    background: #F0F0F0;
+}
+table.ordernumber_variables tbody tr td input {
+	width: 100%;
+}
+.ordernumber-btn {
+    cursor: pointer;
+}
+
+table.ordernumber_variables img {
+    padding: 0;
+    margin: 0;    
+}
+tr.rowhidden {
+	display: none;
+}
+
+/* Adjust the columns of the replacements table */
+col.variables_ifvar, col.variables_ifval {
+	width: 15%;
+}
+col.variables_ifop {
+	width: 10%;
+}
+col.variables_thenvar, col.variables_thenval {
+	width: 25%;
+}
+.variables_then, .variables_settings {
+	text-align: center;
+	width: 20px;
+}
diff --git a/ordernumber/assets/js/ordernumber.js b/ordernumber/assets/js/ordernumber.js
index c7d64f8..1fa21ea 100644
--- a/ordernumber/assets/js/ordernumber.js
+++ b/ordernumber/assets/js/ordernumber.js
@@ -1,3 +1,8 @@
+/**********************************************************************************
+ * 
+ *  Javascript for the counter modification table
+ * 
+ **********************************************************************************/
 var updateMessages = function(messages, area) {
     jQuery( "#system-message-container #system-message ."+area+"-message").remove();
     // Extract the messages from the returned string, add the ordernumber-message class (so the next ajax call
@@ -144,3 +149,46 @@ var ajaxAddCounter = function (btn, nrtype) {
         });
     }
 }
+
+
+
+
+/**********************************************************************************
+ * 
+ *  Javascript for the Custom Variables table
+ * 
+ **********************************************************************************/
+
+var ordernumberVariablesAddRow = function (template, element) {
+	var cl = jQuery("#" + template + " tr").clone(true);
+	// Enable all form controls
+	jQuery(cl).find('input,select,button,img').removeAttr('disabled');
+
+	// select boxes handled by the chosen juery plugin cannot simply be cloned,
+	// instead we need to re-initialize chosen!
+	jQuery(cl).find('select').removeClass("chzn-done").removeAttr("id").css("display", "block").next().remove();
+	jQuery(cl).find('select').chosen({width: "50px"});
+	// Now insert this new row into the table
+	jQuery(cl).appendTo("table#" + element + " tbody");
+	jQuery("tr#ordernumber-replacements-empty-row")
+		.addClass("rowhidden")
+		.find('input')
+		.attr('disabled', 'disabled');
+}
+
+jQuery(document).ready (function () {
+	jQuery('img.ordernumber-replacement-deletebtn').click(
+		function () {
+			jQuery(this).closest('tr').remove();
+			var count = jQuery(this).closest('table').find('tbody tr').length;
+			if (count==0) {
+				jQuery("tr#ordernumber-replacements-empty-row")
+					.removeClass("rowhidden")
+					.find('input,select,button,img')
+					.removeAttr('disabled');
+			}
+		}
+	);
+
+	jQuery("#ordernumber_variables tbody").sortable();
+});
-- 
GitLab