diff --git a/fields/vmordernumbercounters.php b/fields/vmordernumbercounters.php
index b96e01f17230e0ce3b97ae1bacd3862004df25e2..91da6154f9652d25557bc9d33061895fd116f994 100644
--- a/fields/vmordernumbercounters.php
+++ b/fields/vmordernumbercounters.php
@@ -18,31 +18,29 @@ 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();
+if (!class_exists('OrdernumberHelperJoomla'))
+	require_once (dirname(dirname(__FILE__)) . DS . 'ordernumber_helper_joomla.php');
 
 class JFormFieldVmOrdernumberCounters extends JFormField {
     var $_name = 'vmOrdernumberCounters';
-    
+
     protected $countertype;
 
     public function __get($name)
     {
-        switch ($name)
-        {
+        switch ($name) {
             case 'countertype':
                 return $this->$name;
         }
-
         return parent::__get($name);
     }
 
     public function __set($name, $value)
     {
-        switch ($name)
-        {
+        switch ($name) {
             case 'countertype':
                 $this->$name = (string) $value;
                 break;
-
             default:
                 parent::__set($name, $value);
         }
@@ -59,86 +57,32 @@ class JFormFieldVmOrdernumberCounters extends JFormField {
         return $return;
     }
     
-    
-    // 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();
-        // 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;
-                }
+	protected function makeJSTranslationsAvailable() {
+		JText::script('PLG_ORDERNUMBER_JS_NOT_AUTHORIZED');
+		JText::script('PLG_ORDERNUMBER_JS_NEWCOUNTER');
+		JText::script('PLG_ORDERNUMBER_JS_EDITCOUNTER');
+		JText::script('PLG_ORDERNUMBER_JS_INVALID_COUNTERVALUE');
+		JText::script('PLG_ORDERNUMBER_JS_MODIFY_FAILED');
+		JText::script('PLG_ORDERNUMBER_JS_DELETECOUNTER');
+		JText::script('PLG_ORDERNUMBER_JS_DELETE_FAILED');
+		JText::script('PLG_ORDERNUMBER_JS_ADD_FAILED');
+		JText::script('PLG_ORDERNUMBER_JS_JSONERROR');
+	}
+	
+	protected function getInput() {
+		$helper = OrdernumberHelperJoomla::getHelper();
 
-                $ver = '';
-                if(!empty($jsToAdd['ver'])) $ver = '?vmver='.$jsToAdd['ver'];
-                $document->addScript( $file .$ver,"text/javascript",$jsToAdd['defer'],$jsToAdd['async'] );
-                vmJsApi::removeJScript($name);
-            }
-        }
-    }
-
-    protected function makeJSTranslationsAvailable() {
-        JText::script('PLG_ORDERNUMBER_JS_NOT_AUTHORIZED');
-        JText::script('PLG_ORDERNUMBER_JS_NEWCOUNTER');
-        JText::script('PLG_ORDERNUMBER_JS_EDITCOUNTER');
-        JText::script('PLG_ORDERNUMBER_JS_INVALID_COUNTERVALUE');
-        JText::script('PLG_ORDERNUMBER_JS_MODIFY_FAILED');
-        JText::script('PLG_ORDERNUMBER_JS_DELETECOUNTER');
-        JText::script('PLG_ORDERNUMBER_JS_DELETE_FAILED');
-        JText::script('PLG_ORDERNUMBER_JS_ADD_FAILED');
-        JText::script('PLG_ORDERNUMBER_JS_JSONERROR');
-    }
-    protected function getInput() {
-        $pluginpath = '/plugins/vmshopper/ordernumber/ordernumber/';
-        $doc = JFactory::getDocument()->addStyleSheet(JURI::root(true) . $pluginpath . 'assets/css/ordernumber.css');
-        $this->makeJSTranslationsAvailable();
-        $this->loadjQuery();
-        $doc->addScript(JURI::root(true).$pluginpath . 'assets/js/ordernumber.js');
-        
-        // Look up the current counters
-        $db = JFactory::getDBO();
-        $db->setQuery('SELECT `number_format`, `count` FROM `#__virtuemart_shopper_plg_ordernumber` WHERE `number_type`='.$db->quote($this->countertype) . ' ORDER BY `number_format`;' );
-        $counters = $db->loadObjectList();
-        // Joomla 2.x uses <li> for the params and float:left on the controls, so we need to add that too
-        $float = "";
-        if (version_compare(JVERSION, '3.0', 'lt')) {
-            $float = "float: left; ";
-        }
+		$doc = JFactory::getDocument()->addStyleSheet($helper->urlPath('css', 'ordernumber.css'));
+// 		$this->makeJSTranslationsAvailable();
+		$helper->loadjQuery();
+		$doc->addScript($helper->urlPath('js', 'ordernumber.js'));
         
-        $html=array();
-        $html[] = "<img src='".JURI::root(true).$pluginpath . "assets/images/loading.gif' class='vm-ordernumber-loading' style=\"display: none; position: absolute; top: 2px; left: 0px; z-index: 9999;\"/><table class=\"vmordernumber-countertable table-striped \" style=\"display: inline-table; $float\">";
-        $html[] = "  <tr>";
-        $html[] = "    <th class='counter_format'>".JText::_('PLG_ORDERNUMBER_COUNTERLIST_HEADER_COUNTER')."</th>";
-        $html[] = "    <th class='counter_value'>".JText::_('PLG_ORDERNUMBER_COUNTERLIST_HEADER_VALUE'). "</th>";
-        $html[] = "    <th class='counter_buttons'></th>";
-        $html[] = "  </tr>";
-        $html[] = "  <colgroup><col class='counter_type'><col style=\"text-align: center\" ><col ></colgroup>";
-        foreach ($counters as $c) {
-            $displayfmt = $c->number_format;
-            if ($displayfmt=="") {
-                $displayfmt = JText::_ ('PLG_ORDERNUMBER_COUNTERLIST_GLOBAL');
-            }
-            $html[] = "  <tr class='counter_row counter_type_$this->countertype'>";
-            $html[] = "    <td class='counter_format'>" . (string)$displayfmt . "</td>";
-            $html[] = "    <td class='counter_value'>" . (string)$c->count . "</td>";
-            $html[] = "    <td class='counter_buttons'><div class='ordernumber-ajax-loading'><img src='" .JURI::root(true).$pluginpath . "assets/images/icon-16-edit.png' class='vmordernumber-counter-editbtn vmordernumber-btn' onClick='ajaxEditCounter(this, " . json_encode($this->countertype) . ", ".json_encode($c->number_format).", $c->count)' /></div><div class='ordernumber-ajax-loading'><img src='" . JURI::root(true).$pluginpath . "assets/images/icon-16-delete.png' class='vmordernumber-counter-deletebtn vmordernumber-btn' onClick='ajaxDeleteCounter(this, ".json_encode($this->countertype).", ".json_encode($c->number_format).", $c->count)' /></div></td>";
-            $html[] = "  </tr>";
-        }
-        $html[] = "  <tr class='addcounter_row'>";
-        $html[] = "    <td colspan=3 class='counter_add'><div class='vmordernumber-counter-addbtn vmordernumber-btn' onClick='ajaxAddCounter(this, " . json_encode($this->countertype).")'><div class='ordernumber-ajax-loading'><img src='" . JURI::root(true).$pluginpath . "assets/images/icon-16-new.png' class='vmordernumber-counter-addbtn' /></div>" . JText::_('PLG_ORDERNUMBER_COUNTERLIST_ADD') . "</div></td>";
-        $html[] = "  </tr>";
-        $html[] = "</table>";
-        return implode("\n", $html);
-    }
-}
\ No newline at end of file
+		// Look up the current counters
+		$db = JFactory::getDBO();
+		$db->setQuery('SELECT `number_format`, `count` FROM `#__virtuemart_shopper_plg_ordernumber` WHERE `number_type`='.$db->quote($this->countertype) . ' ORDER BY `number_format`;' );
+		$counters = $db->loadObjectList();
+       
+		return $helper->counter_modification_create_table ($this->countertype, $counters);
+	}
+}
diff --git a/fields/vmordernumberreplacements.php b/fields/vmordernumberreplacements.php
index 315f2d56ddc725669d881c2386252f9b6baf6c28..b88dcc949aa8cd09e5dcda0b8ee94c37deb1d38e 100644
--- a/fields/vmordernumberreplacements.php
+++ b/fields/vmordernumberreplacements.php
@@ -15,180 +15,24 @@ defined('_JEXEC') or die();
  */
  
 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();
+// if (!class_exists( 'VmConfig' )) 
+//     require(JPATH_ROOT.DS.'administrator'.DS.'components'.DS.'com_virtuemart'.DS.'helpers'.DS.'config.php');
+// VmConfig::loadConfig();
+if (!class_exists('OrdernumberHelperJoomla'))
+	require_once (dirname(dirname(__FILE__)) . DS . 'ordernumber_helper_joomla.php');
 
 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:
-        if (version_compare(JVERSION, '3.0', 'lt')) {
-        } else {
-            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'));
-        $this->loadjQuery();
-        $doc->addScript( self::js_url('ordernumber.js'));
-        $this->makeJSTranslationsAvailable();
-        
-        
-        $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;
+	protected function getInput() {
+		$helper = OrdernumberHelperJoomla::getHelper();
+		$doc = JFactory::getDocument()->addStyleSheet($helper->urlPath('css', 'ordernumber.css'));
+		$helper->loadjQuery();
+		$doc->addScriptDeclaration($helper->createJSSetup());
+		$doc->addScript( $helper->urlPath('js', 'ordernumber.js'));
         
-        
-//         $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::__('PLG_ORDERNUMBER_REPL_IFVAR'),
-            'variables_ifop'     => '',
-            'variables_ifval'    => self::__('PLG_ORDERNUMBER_REPL_IFVAL'),
-            'variables_then'     => self::__(''),
-            'variables_thenvar'  => self::__('PLG_ORDERNUMBER_REPL_SETVAR'),
-            'variables_thenval'  => self::__('PLG_ORDERNUMBER_REPL_TOVAL'),
-            '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>' . self::__('PLG_ORDERNUMBER_REPL_NOCUSTOMVARS') . '</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::__('PLG_ORDERNUMBER_REPL_ADDVAR');
-        $html[] = '				</div>';
-        $html[] = '			</td>';
-        $html[] = '		</tr>';
-        $html[] = '	</tfoot>';
-        $html[] = '</table>';
-        return implode("\n", $html);
+		$variables = $helper->transposeCustomVariables($this->value);
+ 		return $helper->custom_variables_create_table($this->name, $variables);
     }
     
-    protected function create_replacements_row_html($name, $values = array(), $disabled = '') {
-        $operator = (isset($values['conditionop'])?$values['conditionop']:'');
-        $operators = array(
-            'equals'       => '=', 
-            'contains'     => self::__('PLG_ORDERNUMBER_REPL_OP_CONTAINS'), 
-            'smaller'      => '<',
-            'smallerequal' => '<=',
-            'larger'       => '>',
-            'largerequal'  => '>=', 
-            'startswith'   => self::__('PLG_ORDERNUMBER_REPL_OP_STARTS'),
-            'endswith'     => self::__('PLG_ORDERNUMBER_REPL_OP_ENDS'),
-        );
-        $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 fc29d641038f58c2dfcc29e21c813cb4fc71badf..d70ac7a74e6ded896cfb1b60d77bd7ae4487ad55 100644
--- a/language/en-GB/en-GB.plg_vmshopper_ordernumber.ini
+++ b/language/en-GB/en-GB.plg_vmshopper_ordernumber.ini
@@ -59,15 +59,15 @@ 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:"
-PLG_ORDERNUMBER_JS_DELETECOUNTER="Really delete counter '{0}' with value '{1}'?"
-PLG_ORDERNUMBER_JS_EDITCOUNTER="{0}Please enter the new value for the counter '{1}' (current value: {2}):"
-PLG_ORDERNUMBER_JS_INVALID_COUNTERVALUE="You entered an invalid value for the counter.\n\n"
-PLG_ORDERNUMBER_JS_MODIFY_FAILED="Failed modifying counter {0}"
-PLG_ORDERNUMBER_JS_DELETE_FAILED="Failed deleting counter {0}"
-PLG_ORDERNUMBER_JS_ADD_FAILED="Failed adding counter {0}"
-PLG_ORDERNUMBER_JS_JSONERROR="Error reading response from server:"
+ORDERNUMBER_JS_NOT_AUTHORIZED="You are not authorized to modify order number counters."
+ORDERNUMBER_JS_NEWCOUNTER="Please enter the format/name of the new counter:"
+ORDERNUMBER_JS_DELETECOUNTER="Really delete counter '{0}' with value '{1}'?"
+ORDERNUMBER_JS_EDITCOUNTER="{0}Please enter the new value for the counter '{1}' (current value: {2}):"
+ORDERNUMBER_JS_INVALID_COUNTERVALUE="You entered an invalid value for the counter.\n\n"
+ORDERNUMBER_JS_MODIFY_FAILED="Failed modifying counter {0}"
+ORDERNUMBER_JS_DELETE_FAILED="Failed deleting counter {0}"
+ORDERNUMBER_JS_ADD_FAILED="Failed adding counter {0}"
+ORDERNUMBER_JS_JSONERROR="Error reading response from server:"
 
 PLG_ORDERNUMBER_REPL_IFVAR="If variable ..."
 PLG_ORDERNUMBER_REPL_IFVAL="Value"
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 fc29d641038f58c2dfcc29e21c813cb4fc71badf..d70ac7a74e6ded896cfb1b60d77bd7ae4487ad55 100644
--- a/language/en-GB/en-GB.plg_vmshopper_ordernumber.sys.ini
+++ b/language/en-GB/en-GB.plg_vmshopper_ordernumber.sys.ini
@@ -59,15 +59,15 @@ 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:"
-PLG_ORDERNUMBER_JS_DELETECOUNTER="Really delete counter '{0}' with value '{1}'?"
-PLG_ORDERNUMBER_JS_EDITCOUNTER="{0}Please enter the new value for the counter '{1}' (current value: {2}):"
-PLG_ORDERNUMBER_JS_INVALID_COUNTERVALUE="You entered an invalid value for the counter.\n\n"
-PLG_ORDERNUMBER_JS_MODIFY_FAILED="Failed modifying counter {0}"
-PLG_ORDERNUMBER_JS_DELETE_FAILED="Failed deleting counter {0}"
-PLG_ORDERNUMBER_JS_ADD_FAILED="Failed adding counter {0}"
-PLG_ORDERNUMBER_JS_JSONERROR="Error reading response from server:"
+ORDERNUMBER_JS_NOT_AUTHORIZED="You are not authorized to modify order number counters."
+ORDERNUMBER_JS_NEWCOUNTER="Please enter the format/name of the new counter:"
+ORDERNUMBER_JS_DELETECOUNTER="Really delete counter '{0}' with value '{1}'?"
+ORDERNUMBER_JS_EDITCOUNTER="{0}Please enter the new value for the counter '{1}' (current value: {2}):"
+ORDERNUMBER_JS_INVALID_COUNTERVALUE="You entered an invalid value for the counter.\n\n"
+ORDERNUMBER_JS_MODIFY_FAILED="Failed modifying counter {0}"
+ORDERNUMBER_JS_DELETE_FAILED="Failed deleting counter {0}"
+ORDERNUMBER_JS_ADD_FAILED="Failed adding counter {0}"
+ORDERNUMBER_JS_JSONERROR="Error reading response from server:"
 
 PLG_ORDERNUMBER_REPL_IFVAR="If variable ..."
 PLG_ORDERNUMBER_REPL_IFVAL="Value"
diff --git a/ordernumber.php b/ordernumber.php
index 5d328f3d8d7b4b49a5a12cd051f12235625bdcbb..40abd09ee24c9997144bd130c92ed27248976753 100644
--- a/ordernumber.php
+++ b/ordernumber.php
@@ -6,21 +6,39 @@
  * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
 **/
 
-defined('_JEXEC') or     die( 'Direct Access to ' . basename( __FILE__ ) . ' is not allowed.' ) ;
+if ( !defined( 'ABSPATH' ) and !defined('_JEXEC') ) { 
+	die( 'Direct Access to ' . basename( __FILE__ ) . ' is not allowed.' ) ;
+}
 if (!class_exists('vmShopperPlugin')) 
     require(JPATH_VM_PLUGINS . DS . 'vmshopperplugin.php');
-if (!class_exists( 'VmConfig' )) 
-    require(JPATH_ROOT.DS.'administrator'.DS.'components'.DS.'com_virtuemart'.DS.'helpers'.DS.'config.php');
-VmConfig::loadConfig();
+// if (!class_exists( 'VmConfig' )) 
+//     require(JPATH_ROOT.DS.'administrator'.DS.'components'.DS.'com_virtuemart'.DS.'helpers'.DS.'config.php');
+// VmConfig::loadConfig();
+if (!class_exists('OrdernumberHelperJoomla'))
+	require_once (dirname(__FILE__) . DS . 'ordernumber_helper_joomla.php');
 
 class plgVmShopperOrdernumber extends vmShopperPlugin {
+	protected $helper = null;
 
-    function __construct(& $subject, $config) {
-        parent::__construct($subject, $config);
-        /* Create the database table */
-        $this->tableFields = array_keys ($this->getTableSQLFields ());
-    }
+	function __construct(& $subject, $config) {
+		parent::__construct($subject, $config);
+		/* Create the database table */
+		$this->tableFields = array_keys ($this->getTableSQLFields ());
+		$this->helper = new OrdernumberHelperJoomla();
+		$this->helper->tableName = $this->_tablename;
+        
+		$this->helper->registerCallback('setupStoreReplacements',		array($this, 'setupStoreReplacements'));
+		$this->helper->registerCallback('setupOrderReplacements',		array($this, 'setupOrderReplacements'));
+		$this->helper->registerCallback('setupUserReplacements',		array($this, 'setupUserReplacements'));
+		$this->helper->registerCallback('setupShippingReplacements',	array($this, 'setupShippingReplacements'));
+		$this->helper->registerCallback('setupThirdPartyReplacements',	array($this, 'setupThirdPartyReplacements'));
+
+	}
 
+    /**
+     * Glue functions for the OrdernumberHelper
+     */
+     
     public function getVmPluginCreateTableSQL () {
         return $this->createTableSQL ('VM Shopper plugin: custom order and invoice numbers');
     }
@@ -38,86 +56,8 @@ 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, $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`='.$db->quote($nrtype).' AND `number_format`='.$db->quote($format);
-        $db->setQuery($q);
-        $existing = $db->loadResult();
-        $count = $existing?$existing:$default;
-        return $count;
-    }
-    
-    function _counterExists($nrtype, $format) {
-        $db = JFactory::getDBO();
-        $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);
-    }
-    
-    // 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.','.$db->quote($nrtype).', '.$db->quote($format).')';
-        $db->setQuery( $q );
-        $db->query();
-        return $db->getAffectedRows();
-    }
-
-    // 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`='.$db->quote($nrtype).' AND `number_format`='.$db->quote($format);
-        $db->setQuery( $q );
-        $db->query();
-        if ($db->getAffectedRows()<1) {
-            return $this->_addCounter($nrtype, $format, $value);
-        } else {
-            return $db->getAffectedRows();
-        }
-    }
-
-    // Insert new counter value into the db or update existing one
-    function _deleteCounter($nrtype, $format) {
-        $db = JFactory::getDBO();
-        $format = $db->escape ($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();
-    }
-
-    /* Return a random "string" of the given length taken from the given alphabet */
-    static function randomString($alphabet, $len) {
-        $alen = strlen($alphabet);
-        $r = "";
-        for ($n=0; $n<$len; $n++) {
-            $r .= $alphabet[mt_rand(0, $alen-1)];
-        }
-        return $r;
-    }
-
-    function replaceRandom ($match) {
-        /* the regexp matches (random)(Type)(Len) as match, Type and Len is optional */
-        $len = ($match[3]?$match[3]:1);
-        // Fallback: If no Type is given, use Digit
-        $alphabet = "0123456789";
-        // Select the correct alphabet depending on Type
-        switch (strtolower($match[2])) {
-            case "digit": $alphabet = "0123456789"; break;
-            case "hex": $alphabet = "0123456789abcdef"; break;
-            case "letter": $alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; break;
-            case "uletter": $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; break;
-            case "lletter": $alphabet = "abcdefghijklmnopqrstuvwxyz"; break;
-            case "alphanum": $alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; break;
-        }
-        return self::randomString ($alphabet, $len);
-    }
-    
     /* Extract the country information from the given ID */
-    static function getCountryFromID ($country_id) {
+    protected static function getCountryFromID ($country_id) {
         $db = JFactory::getDBO();
         $query = 'SELECT * FROM `#__virtuemart_countries` WHERE `virtuemart_country_id` = ' . (int)$country_id;
         $db->setQuery($query);
@@ -126,24 +66,6 @@ class plgVmShopperOrdernumber extends vmShopperPlugin {
 
 
 
-    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;
@@ -255,7 +177,6 @@ class plgVmShopperOrdernumber extends vmShopperPlugin {
 		
     }
    
-
     protected function setupUserReplacements (&$reps, $details, $nrtype) {
         // TODO: Implement shopper group!
         $reps["[userid]"]      = $details->virtuemart_user_id;
@@ -275,160 +196,7 @@ class plgVmShopperOrdernumber extends vmShopperPlugin {
         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);
-        return $reps;
-    }
-
-    protected function setupCustomVariables ($nrtype, $order, $reps, $customvars) {
-        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'];
-            }
-        }
-        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;
-        }
-    }
-    
-    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"]==1)?"":$parts[(count($parts)>1)?1:0];
-		
-		return $ctrsettings;
-	}
 
-    /* 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);
-
-// JFactory::getApplication()->enqueueMessage("<pre>Replacements for $type:".print_r($reps,1)."</pre>", 'error');
-        // 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);
@@ -440,28 +208,8 @@ class plgVmShopperOrdernumber extends vmShopperPlugin {
                 "${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();
-        
-            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;
-                }
-            }
-        
-
-            $number = $this->createNumber ($fmt, $type, $order, $customvars, $ctrsettings);
+            $customvars = $this->helper->transposeCustomVariables($this->params->get ('replacements', array()));
+            $number = $this->helper->createNumber ($fmt, $type, $order, $customvars, $ctrsettings);
             return $number;
         } else {
             return false;
@@ -542,30 +290,23 @@ class plgVmShopperOrdernumber extends vmShopperPlugin {
         $json['success'] = 0; // default: unsuccessfull
         switch ($action) {
             case "deleteCounter":
-                $json['success'] = $this->_deleteCounter($nrtype, $counter);
+                $json['success'] = $this->helper->deleteCounter($nrtype, $counter);
                 break;
             case "addCounter":
                 $value = vRequest::getInt('value',0);
-                if ($this->_counterExists($nrtype, $counter)) {
+                if ($this->helper->counterExists($nrtype, $counter)) {
                     $json['error'] = JText::sprintf('PLG_ORDERNUMBER_COUNTERLIST_EXISTS', $counter);
                     $json['success'] = false;
                 } else {
-                    $json['success'] = $this->_addCounter($nrtype, $counter, $value);
+                    $json['success'] = $this->helper->addCounter($nrtype, $counter, $value);
                     // Return the table row for the new counter in the JSON:
-                    $pluginpath = '/plugins/vmshopper/ordernumber/ordernumber/';
-                    $displayfmt = ($counter=="") ? JText::_('PLG_ORDERNUMBER_COUNTERLIST_GLOBAL') : $counter;
-                    $html=array();
-                    $html[] = "<tr class='counter_row counter_type_$nrtype'>";
-                    $html[] = "  <td class='counter_format'>" . (string)$displayfmt . "</td>";
-                    $html[] = "  <td class='counter_value'>" . (string)$value . "</td>";
-                    $html[] = "  <td class='counter_buttons'><img src='" .JURI::root(true).$pluginpath . "assets/images/icon-16-edit.png' class='vmordernumber-counter-editbtn vmordernumber-btn' onClick='ajaxEditCounter(this, ".json_encode($nrtype).", ".json_encode($counter).", $value)' /><img src='" . JURI::root(true).$pluginpath . "assets/images/icon-16-delete.png' class='vmordernumber-counter-deletebtn vmordernumber-btn' onClick='ajaxDeleteCounter(this, ".json_encode($nrtype).", ".json_encode($counter).", $value)' /></td>";
-                    $html[] = "</tr>";
-                    $json['newrow'] = implode("\n", $html);
+					$json['row'] = $this->helper->counter_modification_create_row($nrtype, $counter, $value);
                 }
                 break;
             case "setCounter":
                 $value = vRequest::getInt('value');
-                $json['success'] = $this->_setCounter($nrtype, $counter, $value);
+                $json['success'] = $this->helper->setCounter($nrtype, $counter, $value);
+				$json['row'] = $this->helper->counter_modification_create_row($nrtype, $counter, $value);
                 break;
         }
         
diff --git a/ordernumber/assets/css/ordernumber.css b/ordernumber/assets/css/ordernumber.css
index 27ecd74be2df0a382aba16c35906505dfb63214b..953f955b119f348a03a3c4c2895dbbb239cf5cbe 100644
--- a/ordernumber/assets/css/ordernumber.css
+++ b/ordernumber/assets/css/ordernumber.css
@@ -1,12 +1,10 @@
-td.counter_value {
-    text-align: center;
-}
 
-table.vmordernumber-countertable {
+table.ordernumber-countertable {
     border: 1px solid #888888;
+    display: inline-table;
 }
 
-table.vmordernumber-countertable.table-striped tbody > tr:nth-child(odd) > th {
+table.ordernumber-countertable.table-striped tbody > tr:nth-child(odd) > th {
     background: #E0E0E0;
 }
 .vmordernumber-btn {
@@ -16,15 +14,18 @@ table.vmordernumber-countertable.table-striped tbody > tr:nth-child(odd) > th {
 col.counter_type, th.counter_type, td.counter_type {
     display:none;
 }
+td.counter_value {
+    text-align: center;
+}
 
-fieldset table.vmordernumber-countertable img {
+fieldset table.ordernumber-countertable img {
     padding: 0;
     margin: 0;    
 }
-div.ordernumber-ajax-loading, div.vmordernumber-counter-addbtn {
+div.ordernumber-ajax-loading, div.ordernumber-counter-addbtn {
     display: inline;
 }
-div.ordernumber-ajax-loading, div.ordernumber-ajax-loading img.vmordernumber-btn {
+div.ordernumber-ajax-loading, div.ordernumber-ajax-loading img.ordernumber-btn {
     position: relative;
     top: 0; left: 0;
 }
@@ -32,6 +33,14 @@ div.ordernumber-ajax-loading img {
     z-index:0;
 }
 
+img.ordernumber-loading {
+	display: none; 
+	position: absolute; 
+	top: 2px; 
+	left: 0px; 
+	z-index: 9999;
+}
+
 
 
 /*  Counter custom variable replacements */
@@ -96,3 +105,6 @@ col.variables_thenvar, col.variables_thenval {
 	text-align: center;
 	width: 20px;
 }
+
+
+
diff --git a/ordernumber/assets/js/ordernumber.js b/ordernumber/assets/js/ordernumber.js
index c83dfb22cc9744d3b5c25c4e31e749ff3048226e..ea9e7fb5943ad63a21507fbf4b075da26fc5c74d 100644
--- a/ordernumber/assets/js/ordernumber.js
+++ b/ordernumber/assets/js/ordernumber.js
@@ -3,21 +3,6 @@
  *  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
-    // can remove them again) and then move the messages to the original message container.
-    // Things are complicated by the fact that no #system-message element exists if no messages were printed so far
-    var newmessages = jQuery( messages ).find("div.alert, .message").addClass(area+"-message");
-    if (!jQuery( "#system-message-container #system-message").length && newmessages.length) {
-        if (jQuery(newmessages).first().prop("tagName")=="dt") { // Joomla 2.x:
-            jQuery( "#system-message-container" ).append( "<dl id='system-message'></div>" );
-        } else {
-            jQuery( "#system-message-container" ).append( "<div id='system-message'></div>" );
-        }
-    }
-    newmessages.appendTo( "#system-message-container #system-message");
-}
 String.Format = function() {
   var s = arguments[0];
   for (var i = 0; i < arguments.length - 1; i++) {       
@@ -28,18 +13,19 @@ String.Format = function() {
 }
 
 var getCounterData = function (btn) {
-    var row=jQuery(btn).parents("tr.counter_row");
-    return { row: row };
+    return { row: jQuery(btn).closest("tr.counter_row") };
 }
 var handleJSONResponse = function (json, counter) {
-    updateMessages(json['messages'], "ordernumber");
-    if (!json.authorized) {
-        alert(Joomla.JText._('PLG_ORDERNUMBER_JS_NOT_AUTHORIZED', "You are not authorized to modify order number counters."));
-    } else if (json.error) {
-        alert(json.error);
-    } else {
-        // TODO: Which other error checks can we do?
-    }
+	if ('updateMessages' in ajax_ordernumber) { 
+		ajax_ordernumber.updateMessages(json['messages'], "ordernumber");
+	}
+	if (!json.authorized) {
+		alert(ajax_ordernumber.ORDERNUMBER_JS_NOT_AUTHORIZED);
+	} else if (json.error) {
+		alert(json.error);
+	} else {
+		// TODO: Which other error checks can we do?
+	}
 }
 var ajaxEditCounter = function (btn, nrtype, ctr, value) {
     var counter = getCounterData(btn);
@@ -49,36 +35,43 @@ var ajaxEditCounter = function (btn, nrtype, ctr, value) {
     var value = NaN;
     var msgprefix = "";
     while (isNaN(value) && (value != null)) {
-        var editprompt = Joomla.JText._('PLG_ORDERNUMBER_JS_EDITCOUNTER', "{0}Please enter the new value for the counter '{1}' (current value: {2}):");
+        var editprompt = ajax_ordernumber.ORDERNUMBER_JS_EDITCOUNTER;
         value = prompt (String.Format(editprompt, msgprefix, counter.counter, counter.value), counter.value);
         if (value != null)
             value = parseInt(value);
         if (isNaN(value)) 
-            msgprefix = Joomla.JText._('PLG_ORDERNUMBER_JS_INVALID_COUNTERVALUE', "You entered an invalid value for the counter.\n\n");
+            msgprefix = ajax_ordernumber.ORDERNUMBER_JS_INVALID_COUNTERVALUE;
     }
     if (value != null) {
-        var loading = jQuery("img.vm-ordernumber-loading").first().clone().insertAfter(btn).show();
+        var loading = jQuery("img.ordernumber-loading").first().clone().insertAfter(btn).show();
         jQuery.ajax({
             type: "POST",
             cache: false,
             dataType: "text", // Read text, but interpret as JSON later in the done method (prevents a warning!)
-            url: "index.php?option=com_virtuemart&view=plugin&type=vmshopper&name=ordernumber&action=setCounter&format=raw",
-            data: { nrtype: counter.type, counter: counter.counter, value: value },
+            url: ajax_ordernumber.ajax_url,
+            data: { 
+				action: 'setCounter',
+				nrtype: counter.type, 
+				counter: counter.counter, 
+				value: value 
+			},
             success: function( data ) {
                 try {
                     var json = jQuery.parseJSON(data);
                     handleJSONResponse(json, counter);
                 } catch (e) {
-                    alert(Joomla.JText._('PLG_ORDERNUMBER_JS_JSONERROR')+"\n"+e);
+                    alert(ajax_ordernumber.ORDERNUMBER_JS_JSONERROR+"\n"+e);
                     return;
                 }
                 if (json.success>0) {
-                    jQuery(counter.row).children(".counter_value").text(value);
+					// replace the whole row with the html returned by the AJAX call:
+					jQuery(counter.row).replaceWith(json.row);
+//                     jQuery(counter.row).find(".counter_value").text(value);
                 } else {
-                    alert (String.Format(Joomla.JText._('PLG_ORDERNUMBER_JS_MODIFY_FAILED', "Failed modifying counter {0}"), counter.counter));
+                    alert (String.Format(ajax_ordernumber.ORDERNUMBER_JS_MODIFY_FAILED, counter.counter));
                 }
             },
-            error: function() { alert (String.Format(Joomla.JText._('PLG_ORDERNUMBER_JS_MODIFY_FAILED', "Failed modifying counter {0}"), counter.counter)); },
+            error: function() { alert (String.Format(ajax_ordernumber.ORDERNUMBER_JS_MODIFY_FAILED, counter.counter)); },
             complete: function() { jQuery(loading).remove(); },
         });
     }
@@ -88,63 +81,71 @@ var ajaxDeleteCounter = function (btn, nrtype, ctr, value) {
     counter.type=nrtype;
     counter.counter=ctr;
     counter.value=value;
-    var proceed = confirm (String.Format(Joomla.JText._('PLG_ORDERNUMBER_JS_DELETECOUNTER', "Really delete counter '{0}' with value '{1}'?"), counter.counter, counter.value));
+    var proceed = confirm (String.Format(ajax_ordernumber.ORDERNUMBER_JS_DELETECOUNTER, counter.counter, counter.value));
     if (proceed == true) {
-        var loading = jQuery("img.vm-ordernumber-loading").first().clone().insertAfter(btn).show();
+        var loading = jQuery("img.ordernumber-loading").first().clone().insertAfter(btn).show();
         jQuery.ajax({
             type: "POST",
             cache: false,
             dataType: "text", // Read text, but interpret as JSON later in the done method (prevents a warning!)
-            url: "index.php?option=com_virtuemart&view=plugin&type=vmshopper&name=ordernumber&action=deleteCounter&format=raw",
-            data: { nrtype: counter.type, counter: counter.counter },
+            url: ajax_ordernumber.ajax_url,
+            data: { 
+				action: 'deleteCounter',
+				nrtype: counter.type, 
+				counter: counter.counter 
+			},
             success: function( data ) {
                 try {
                     var json = jQuery.parseJSON(data);
                     handleJSONResponse(json, counter);
                 } catch (e) {
-                    alert(Joomla.JText._('PLG_ORDERNUMBER_JS_JSONERROR')+"\n"+e);
+                    alert(ajax_ordernumber.ORDERNUMBER_JS_JSONERROR+"\n"+e);
                     return;
                 }
                 if (json.success>0) {
                     jQuery(counter.row).fadeOut(1500, function() { jQuery(counter.row).remove(); });
                 } else {
-                    alert (String.Format(Joomla.JText._('PLG_ORDERNUMBER_JS_DELETE_FAILED', "Failed deleting counter {0}"), counter.counter));
+                    alert (String.Format(ajax_ordernumber.ORDERNUMBER_JS_DELETE_FAILED, counter.counter));
                 }
             },
-            error: function() { alert (String.Format(Joomla.JText._('PLG_ORDERNUMBER_JS_DELETE_FAILED', "Failed deleting counter {0}"), counter.counter)); },
+            error: function() { alert (String.Format(ajax_ordernumber.ORDERNUMBER_JS_DELETE_FAILED, counter.counter)); },
             complete: function() { jQuery(loading).remove(); },
         });
     }
 }
 var ajaxAddCounter = function (btn, nrtype) {
     var row = jQuery(btn).parents("tr.addcounter_row");
-    var countername = prompt (Joomla.JText._('PLG_ORDERNUMBER_JS_NEWCOUNTER', "Please enter the format/name of the new counter:"));
+    var countername = prompt (ajax_ordernumber.ORDERNUMBER_JS_NEWCOUNTER);
     if (countername != null) {
-        var loading = jQuery("img.vm-ordernumber-loading").first().clone().insertAfter(jQuery(btn).find("img.vmordernumber-counter-addbtn")).show();
+        var loading = jQuery("img.ordernumber-loading").first().clone().insertAfter(jQuery(btn).find("img.ordernumber-counter-addbtn")).show();
         jQuery.ajax({
             type: "POST",
             cache: false,
             dataType: "text", // Read text, but interpret as JSON later in the done method (prevents a warning!)
-            url: "index.php?option=com_virtuemart&view=plugin&type=vmshopper&name=ordernumber&action=addCounter&format=raw",
-            data: { nrtype: nrtype, counter: countername },
+            url: ajax_ordernumber.ajax_url,
+            data: { 
+				action: "addCounter",
+				nrtype: nrtype, 
+				counter: countername 
+			},
             success: function( data ) {
                 var json = data ? jQuery.parseJSON(data) : null;
                 try {
                     var json = jQuery.parseJSON(data);
                     handleJSONResponse(json, null);
                 } catch (e) {
-                    alert(Joomla.JText._('PLG_ORDERNUMBER_JS_JSONERROR')+"\n"+e);
+                    alert(ajax_ordernumber.ORDERNUMBER_JS_JSONERROR+"\n"+e);
                     return;
                 }
                 if (json.success>0) {
-                    if (json.newrow) {
-                        jQuery(row).before(jQuery(json.newrow));
+                    if (json.row) {
+                        jQuery(row).before(jQuery(json.row));
                     }
                 } else {
-                    alert (String.Format(Joomla.JText._('PLG_ORDERNUMBER_JS_ADD_FAILED', "Failed adding counter {0}"), countername));
+                    alert (String.Format(ajax_ordernumber.ORDERNUMBER_JS_ADD_FAILED, countername));
                 }
             },
-            error: function() { alert (String.Format(Joomla.JText._('PLG_ORDERNUMBER_JS_ADD_FAILED', "Failed adding counter {0}"), countername)); },
+            error: function() { alert (String.Format(ajax_ordernumber.ORDERNUMBER_JS_ADD_FAILED, countername)); },
             complete: function() { jQuery(loading).remove(); },
         });
     }
diff --git a/ordernumber_helper.php b/ordernumber_helper.php
new file mode 100644
index 0000000000000000000000000000000000000000..49b407120d28e2c88d4d9b42b161b3a46da08daf
--- /dev/null
+++ b/ordernumber_helper.php
@@ -0,0 +1,508 @@
+<?php
+/**
+ * Advanced Ordernumbers generic helper class (e-commerce system agnostic)
+ * Reinhold Kainhofer, Open Tools, office@open-tools.net
+ * @copyright (C) 2012-2015 - Reinhold Kainhofer
+ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
+**/
+
+defined('_JEXEC') or	 die( 'Direct Access to ' . basename( __FILE__ ) . ' is not allowed.' ) ;
+
+class OrdernumberHelper {
+	protected $_callbacks = array();
+	public $_styles = array(
+		'counter-table-class' => "table-striped",
+		'counter-table-style' => "",
+		'variable-table-class' => "",
+		'variable-table-style' => "",
+	);
+	/**
+	 * An array containing all language keys for the translations used in the JavaScript code.
+	 * Make sure to set those in the ajax_ordernumber JavaScript array!
+	 */
+	public $jstranslations = array(
+		"ORDERNUMBER_JS_NOT_AUTHORIZED", "ORDERNUMBER_JS_INVALID_COUNTERVALUE", "ORDERNUMBER_JS_JSONERROR", 
+		"ORDERNUMBER_JS_NEWCOUNTER", "ORDERNUMBER_JS_EDITCOUNTER", "ORDERNUMBER_JS_DELETECOUNTER",
+		"ORDERNUMBER_JS_ADD_FAILED", "ORDERNUMBER_JS_MODIFY_FAILED", "ORDERNUMBER_JS_DELETE_FAILED",
+		);
+		
+	/**
+	 * The URL to call for AJAX calls
+	 */
+	public $ajax_url = "";
+   
+	function __construct() {
+		// Set up 
+		$this->registerCallback ("setupDateTimeReplacements", array($this, "setupDateTimeReplacements"));
+	}
+	
+	function getStyle($key) {
+		if (isset($this->_styles[$key])) {
+			return $this->_styles[$key];
+		} else {
+			return '';
+		}
+	}
+	/* Callback handling */
+	
+	/**
+	 * Register a callback for one of the known callback hooks. 
+	 * Valid callbacks are (together with their arguments):
+	 *   - translate($string)
+	 *   - getCounter($type, $countername, $default)
+	 *   - setCounter($type, $countername, $value)
+	 *   - setupDateTimeReplacements(&$reps, $details, $nrtype);
+	 *   - setupStoreReplacements(&$reps, $details, $nrtype);
+	 *   - setupOrderReplacements(&$reps, $details, $nrtype);
+	 *   - setupUserReplacements(&$reps, $details, $nrtype);
+	 *   - setupShippingReplacements(&$reps, $details, $nrtype);
+	 *   - setupThirdPartyReplacements(&$reps, $details, $nrtype);
+	 
+	 *   - urlPath($path, $type)
+	 *  @param string $callback 
+	 *     The name of the callback hook (string)
+	 *  @param function $func 
+	 *     The function (usually a member of the plugin object) for the callback
+	 *  @return none
+	 */
+	public function registerCallback($callback, $func) {
+		$this->callbacks[$callback] = $func;
+	}
+	
+	public function __($string) {
+		if (isset($this->callbacks["translate"])) {
+			return $this->callbacks["translate"]($string);
+		} else {
+			return $string;
+		}
+	}
+
+	public function urlPath($type, $file) {
+		if (isset($this->callbacks['urlPath'])) {
+			return $this->callbacks['urlPath']($type, $file);
+		} else {
+			throw new Exception('No callback defined for urlPath(type, file)!');
+		}
+	}
+	
+	protected function replacementsCallback ($func, &$reps, $details, $nrtype) {
+		if (isset($this->callbacks[$func])) {
+			return $this->callbacks[$func]($reps, $details, $nrtype);
+		}
+	}
+
+	protected function getCounter($type, $countername, $default) {
+		if (isset($this->callbacks['getCounter'])) {
+			return $this->callbacks['getCounter']($type, $countername, $default);
+		} else {
+			throw new Exception('No callback defined for getCounter(type, countername, default)!');
+		}
+	}
+	
+	protected function setCounter($type, $countername, $value) {
+		if (isset($this->callbacks['getCounter'])) {
+			return $this->callbacks['getCounter']($type, $countername, $value);
+		} else {
+			throw new Exception('No callback defined for setCounter(type, countername, value)!');
+		}
+	}
+	
+	/* Return a random "string" of the given length taken from the given alphabet */
+	protected static function randomString($alphabet, $len) {
+		$alen = strlen($alphabet);
+		$r = "";
+		for ($n=0; $n<$len; $n++) {
+			$r .= $alphabet[mt_rand(0, $alen-1)];
+		}
+		return $r;
+	}
+
+	protected function replaceRandom ($match) {
+		/* the regexp matches (random)(Type)(Len) as match, Type and Len is optional */
+		$len = ($match[3]?$match[3]:1);
+		// Fallback: If no Type is given, use Digit
+		$alphabet = "0123456789";
+		// Select the correct alphabet depending on Type
+		switch (strtolower($match[2])) {
+			case "digit": $alphabet = "0123456789"; break;
+			case "hex": $alphabet = "0123456789abcdef"; break;
+			case "letter": $alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; break;
+			case "uletter": $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; break;
+			case "lletter": $alphabet = "abcdefghijklmnopqrstuvwxyz"; break;
+			case "alphanum": $alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; break;
+		}
+		return self::randomString ($alphabet, $len);
+	}
+
+	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 setupReplacements($nrtype, $details) {
+		$reps = array();
+		// The following callbacks directly modify the replacements!
+		$this->replacementsCallback("setupDateTimeReplacements", $reps, $details, $nrtype);
+		$this->replacementsCallback("setupStoreReplacements", $reps, $details, $nrtype);
+		$this->replacementsCallback("setupOrderReplacements", $reps, $details, $nrtype);
+		$this->replacementsCallback("setupUserReplacements", $reps, $details, $nrtype);
+		$this->replacementsCallback("setupShippingReplacements", $reps, $details, $nrtype);
+		$this->replacementsCallback("setupThirdPartyReplacements", $reps, $details, $nrtype);
+		return $reps;
+	}
+
+	protected function setupCustomVariables ($nrtype, $order, $reps, $customvars) {
+		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'];
+			}
+		}
+		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;
+		}
+	}
+	
+	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;
+	}
+
+	/**
+	 * Create a number of given type for the given format. Optionally, custom variable definitions and counter formatting can be passed.
+	 *   @param fmt The Format of the number (containing variables as [variable] and the counter as # or [####:initial/step])
+	 *   @param type The type of the number format, typically order_number, invoice_number, etc. (depending on the e-commerce suite)
+	 *   @param order The e-commerce-suite specific object describing the order. This will simply be passed on to the replacement hooks function for further data extraction during variable setup
+	 *   @param customvars Definitions (conditions and values) for custom variables. An array of arrays with keys conditionvar, conditionop, conditionval, newvar, newval
+	 *   @param ctrsettings Counter formatting defaults (will be overridden by an explicit counter formating variable of [####:initial/step] in the format). Array keys are: $type_format, $type_counter, $type_global, $type_padding, $type_step, $type_start
+	 *   @return A new number for the given format. The incremented counter has been properly stored.
+	 */
+	public 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);
+
+// JFactory::getApplication()->enqueueMessage("<pre>Replacements for $type:".print_r($reps,1)."</pre>", 'error');
+		// 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;
+	}
+	
+	
+	/**
+	 * Create the counter modification HTML table
+	 *     @param $type string
+	 *          
+	 *     @param
+	 */
+	public function counter_modification_create_table($type, $counters) {
+/*		$pluginpath = '/plugins/shopper/ordernumber/ordernumber/';
+        $doc = JFactory::getDocument()->addStyleSheet(JURI::root(true) . $pluginpath . 'assets/css/ordernumber.css');
+        $this->makeJSTranslationsAvailable();
+        $this->loadjQuery();
+        $doc->addScript(JURI::root(true).$pluginpath . 'assets/js/ordernumber.js');
+        
+        // Look up the current counters
+        $db = JFactory::getDBO();
+        $db->setQuery('SELECT `number_format`, `count` FROM `#__virtuemart_shopper_plg_ordernumber` WHERE `number_type`='.$db->quote($type) . ' ORDER BY `number_format`;' );
+        $counters = $db->loadObjectList();
+        // Joomla 2.x uses <li> for the params and float:left on the controls, so we need to add that too
+        $float = "";
+        if (version_compare(JVERSION, '3.0', 'lt')) {
+            $float = "float: left; ";
+        }
+*/
+        $html=array();
+        $html[] = "<img src='" . $this->urlPath ('images', 'loading.gif') . "' class='ordernumber-loading' style=\"display: none; position: absolute; top: 2px; left: 0px; z-index: 9999;\"/>";
+        $html[] = "<table class=\"ordernumber-countertable " . $this->getStyle('counter-table-class') . "\" " . $this->getStyle('counter-table-style') . ">";
+        $html[] = "	<tr>";
+        $html[] = "		<th class='counter_format'>" . $this->__ ('PLG_ORDERNUMBER_COUNTERLIST_HEADER_COUNTER')."</th>";
+        $html[] = "		<th class='counter_value'>" . $this->__ ('PLG_ORDERNUMBER_COUNTERLIST_HEADER_VALUE'). "</th>";
+        $html[] = "		<th class='counter_buttons'></th>";
+        $html[] = "	</tr>";
+        $html[] = "	<colgroup><col class='counter_type'><col style=\"text-align: center\" ><col ></colgroup>";
+        foreach ($counters as $c) {
+            $html[] = $this->counter_modification_create_row ($type, $c->number_format, $c->count);
+        }
+        $html[] = "	<tr class='addcounter_row'>";
+        $html[] = "		<td colspan=3 class='counter_add'>";
+        $html[] = "			<div class='ordernumber-counter-addbtn ordernumber-btn' onClick='ajaxAddCounter(this, " . json_encode($type).")'>";
+        $html[] = "				<div class='ordernumber-ajax-loading'>";
+        $html[] = "					<img src='" . $this->urlPath('images', 'icon-16-new.png') . "' class='ordernumber-counter-addbtn' />";
+        $html[] = "				</div>" . $this->__('PLG_ORDERNUMBER_COUNTERLIST_ADD');
+        $html[] = "			</div>";
+        $html[] = "		</td>";
+        $html[] = "  </tr>";
+        $html[] = "</table>";
+        return implode("\n", $html);
+    }
+
+    public function counter_modification_create_row ($type, $counter, $value) {
+		$html=array();
+		$html[] = "	<tr class='counter_row counter_row_$type'>";
+		$html[] = "		<td class='counter_format'>" . (string)(($counter=="")?($this->__ ('PLG_ORDERNUMBER_COUNTERLIST_GLOBAL')):$counter) . "</td>";
+		$html[] = "		<td class='counter_value'>" . (string)$value . "</td>";
+		$html[] = "		<td class='counter_buttons'>";
+		$html[] = "			<div class='ordernumber-ajax-loading'>";
+		$html[] = "				<img src='" . $this->urlPath('images', 'icon-16-edit.png') . "' class='ordernumber-counter-editbtn ordernumber-btn' ";
+		$html[] = "					onClick='ajaxEditCounter(this, " . json_encode($type) . ", ".json_encode($counter).", $value)' />";
+		$html[] = "			</div>";
+		$html[] = "			<div class='ordernumber-ajax-loading'>";
+		$html[] = "				<img src='" . $this->urlPath ('images', 'icon-16-delete.png') . "' class='ordernumber-counter-deletebtn ordernumber-btn' ";
+		$html[] = "					onClick='ajaxDeleteCounter(this, ".json_encode($type).", ".json_encode($counter).", $value)' />";
+		$html[] = "			</div>";
+		$html[] = "		</td>";
+		$html[] = "	</tr>";
+		return implode("\n", $html);
+	}
+	
+	/**
+	 * Create the html table (with AJAX) to define and modify custom variable definitions.
+	 * The returned HTML code assumes that the caller properly adds the ordernumber.css 
+	 * and ordernumber.js to the page and loads the jQuery library.
+	 *    @param name string
+	 *        The HTML post/request variable name for the control.
+	 *    @param variables array
+	 *        The current list of custom variable replacements
+	 *
+	 *    @retval string
+	 *        The HTML code for the custom variable definition table.
+	 */
+    public function custom_variables_create_table($name, $variables) {
+        $html=array();
+//         $html[] = "<pre>Variables: ".print_r($variables,1)."</pre>";
+        $html[] = '<table id="ordernumber_variables_template" style="display:none">';
+        $html[] = $this->custom_variables_create_row($name, array(), 'disabled');
+        $html[] = '</table>';
+        
+        $html[] = '<table id="ordernumber_variables" class="ordernumber_variables ' . $this->getStyle('variable-table-class') . '" cellspacing="0" ' . $this->getStyle('variable-table-style') . '>';
+        $columns = array(
+            'variables_ifvar'    => $this->__('PLG_ORDERNUMBER_REPL_IFVAR'),
+            'variables_ifop'     => '',
+            'variables_ifval'    => $this->__('PLG_ORDERNUMBER_REPL_IFVAL'),
+            'variables_then'     => $this->__(''),
+            'variables_thenvar'  => $this->__('PLG_ORDERNUMBER_REPL_SETVAR'),
+            'variables_thenval'  => $this->__('PLG_ORDERNUMBER_REPL_TOVAL'),
+            '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>' . $this->__('PLG_ORDERNUMBER_REPL_NOCUSTOMVARS') . '</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->custom_variables_create_row($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="' . $this->urlPath('images', 'icon-16-new.png' ) . '" class="ordernumber-counter-addbtn" /></div>';
+        $html[] = $this->__('PLG_ORDERNUMBER_REPL_ADDVAR');
+        $html[] = '				</div>';
+        $html[] = '			</td>';
+        $html[] = '		</tr>';
+        $html[] = '	</tfoot>';
+        $html[] = '</table>';
+        return implode("\n", $html);
+    }
+    
+    public function custom_variables_create_row($name, $values = array(), $disabled = '') {
+        $operator = (isset($values['conditionop'])?$values['conditionop']:'');
+        $operators = array(
+            'nocondition'  => $this->__('PLG_ORDERNUMBER_REPL_OP_NOCOND'),
+            'equals'       => '=', 
+            'contains'     => $this->__('PLG_ORDERNUMBER_REPL_OP_CONTAINS'), 
+            'smaller'      => '<',
+            'smallerequal' => '<=',
+            'larger'       => '>',
+            'largerequal'  => '>=', 
+            'startswith'   => $this->__('PLG_ORDERNUMBER_REPL_OP_STARTS'),
+            'endswith'     => $this->__('PLG_ORDERNUMBER_REPL_OP_ENDS'),
+        );
+        $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="' . $this->urlPath('images', 'icon-16-delete.png' ) . '" class="ordernumber-replacement-deletebtn ordernumber-btn"></td>
+        </tr>';
+        return $html;
+    }
+
+    /**
+     * Modify the json that contains JavaScript setup code to be used by ordernumber.js
+     */
+	function addCustomJS(&$json) {}
+	function appendJS() {}
+	function createJSSetup() {
+		static $done = 0; // <= prevent double js code
+		$json = array();
+		$json['ajax_url'] = $this->ajax_url;
+		
+		foreach ($this->jstranslations as $key) {
+			$json[$key] = $this->__($key);
+		}
+		$this->addCustomJS($json);
+		$js='/* <![CDATA[ */
+var ajax_ordernumber = ' . json_encode($json) . ';
+';
+		$js .= $this->appendJS();
+		$js .= '/* ]]> */
+';
+		$done = 1;
+		return $js;
+	}
+	
+}
diff --git a/ordernumber_helper_joomla.php b/ordernumber_helper_joomla.php
new file mode 100644
index 0000000000000000000000000000000000000000..c0152977edba724146434eb08fa83c8f6283b0c3
--- /dev/null
+++ b/ordernumber_helper_joomla.php
@@ -0,0 +1,178 @@
+<?php
+/**
+ * Advanced Ordernumbers generic helper class (e-commerce system agnostic)
+ * Reinhold Kainhofer, Open Tools, office@open-tools.net
+ * @copyright (C) 2012-2015 - Reinhold Kainhofer
+ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
+**/
+
+defined('_JEXEC') or	 die( 'Direct Access to ' . basename( __FILE__ ) . ' is not allowed.' ) ;
+
+if (!class_exists( 'VmConfig' )) 
+    require(JPATH_ROOT.DS.'administrator'.DS.'components'.DS.'com_virtuemart'.DS.'helpers'.DS.'config.php');
+VmConfig::loadConfig();
+
+if (!class_exists( 'OrdernumberHelper' )) 
+	require_once (dirname(__FILE__) . DS . 'ordernumber_helper.php');
+
+class OrdernumberHelperJoomla extends OrdernumberHelper {
+	public $tableName = '';
+	
+	function __construct() {
+		parent::__construct();
+		// Joomla-specific Defaults for the HTML tables
+		$this->_styles['counter-table-class'] = "table-striped";
+		
+		$this->ajax_url = "index.php?option=com_virtuemart&view=plugin&type=vmshopper&name=ordernumber&format=raw";
+		
+		// Joomla 2.x uses <li> for the params and float:left on the controls, so we need to add that too
+		if (version_compare(JVERSION, '3.0', 'lt')) {
+			$this->_styles['counter-table-style'] = "style=\"float: left;\"";
+		}
+ 		
+	}
+
+	static function getHelper() {
+		static $helper = null;
+		if (!$helper) {
+			$helper = new OrdernumberHelperJoomla();
+		}
+		return $helper;
+    }
+	
+	// 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
+	public function loadjQuery() {
+		vmJsApi::jQuery();
+		// TODO: jquery::ui available only in J3:
+		if (version_compare(JVERSION, '3.0', 'lt')) {
+		} else {
+			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);
+			}
+		}
+	}
+    
+
+	public static function transposeCustomVariables($cvar) {
+		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();
+        
+		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;
+			}
+		}
+		return $customvars;
+	}
+	
+	public function __($string) {
+// print("<pre>translating: $string</pre>");
+		return JText::_($string);
+	}
+    function urlPath($type, $file) {
+		static $pluginpath = '/plugins/vmshopper/ordernumber/ordernumber/';
+		return JURI::root(true) . $pluginpath . 'assets/' . $type . '/' . $file;
+    }
+ 	
+	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`='.$db->quote($nrtype).' AND `number_format`='.$db->quote($format);
+		$db->setQuery($q);
+		$existing = $db->loadResult();
+		$count = $existing?$existing:$default;
+		return $count;
+	}
+    
+	function counterExists($nrtype, $format) {
+		$db = JFactory::getDBO();
+		$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);
+	}
+    
+	// 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.','.$db->quote($nrtype).', '.$db->quote($format).')';
+		$db->setQuery( $q );
+		$db->query();
+		return $db->getAffectedRows();
+	}
+
+	// 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`='.$db->quote($nrtype).' AND `number_format`='.$db->quote($format);
+		$db->setQuery( $q );
+		$db->query();
+		if ($db->getAffectedRows()<1) {
+			return $this->addCounter($nrtype, $format, $value);
+		} else {
+			return $db->getAffectedRows();
+		}
+	}
+
+	// Insert new counter value into the db or update existing one
+	function deleteCounter($nrtype, $format) {
+		$db = JFactory::getDBO();
+		$format = $db->escape ($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();
+	}
+
+	function appendJS() {
+		// Extract the messages from the returned string, add the ordernumber-message class (so the next ajax call
+		// can remove them again) and then move the messages to the original message container.
+		// Things are complicated by the fact that no #system-message element exists if no messages were printed so far
+		return 'ajax_ordernumber.updateMessages = function(messages, area) {
+    jQuery( "#system-message-container #system-message ."+area+"-message").remove();
+    var newmessages = jQuery( messages ).find("div.alert, .message").addClass(area+"-message");
+    if (!jQuery( "#system-message-container #system-message").length && newmessages.length) {
+        if (jQuery(newmessages).first().prop("tagName")=="dt") { // Joomla 2.x:
+            jQuery( "#system-message-container" ).append( "<dl id=\'system-message\'></div>" );
+        } else {
+            jQuery( "#system-message-container" ).append( "<div id=\'system-message\'></div>" );
+        }
+    }
+    newmessages.appendTo( "#system-message-container #system-message");
+}';
+	}
+
+
+}