diff --git a/assets/css/ordernumber-counter.css b/assets/css/ordernumber-counter.css
deleted file mode 100644
index 522485d9b4b370aecdfd8660a3657b7e76b8c669..0000000000000000000000000000000000000000
--- a/assets/css/ordernumber-counter.css
+++ /dev/null
@@ -1,47 +0,0 @@
-
-table.wc_ordernumber_counters {
-    border: 1px solid #888888;
-	width: inherit;
-}
-
-table.wc_ordernumber_counters td, table.wc_ordernumber_counters th {
-    padding-bottom: 0px;
-    padding-top: 0px;
-}
-table.wc_ordernumber_counters thead th {
-    text-align: center;
-	width: auto;
-}
-td.counter_value {
-    text-align: center;
-}
-
-table.wc_ordernumber_counters thead > tr:nth-child(odd) > th,
-table.wc_ordernumber_counters tfoot > tr.addcounter_row > td {
-    background: #E0E0E0;
-}
-table.wc_ordernumber_counters tbody > tr:nth-child(even) > td {
-    background: #F0F0F0;
-}
-.ordernumber-btn {
-    cursor: pointer;
-}
-
-col.counter_type, th.counter_type, td.counter_type {
-    display:none;
-}
-
-table.wc_ordernumber_counters img {
-    padding: 0;
-    margin: 0;    
-}
-div.ordernumber-ajax-loading, div.ordernumber-counter-addbtn {
-    display: inline;
-}
-div.ordernumber-ajax-loading, div.ordernumber-ajax-loading img.ordernumber-btn {
-    position: relative;
-    top: 0; left: 0;
-}
-div.ordernumber-ajax-loading img {
-    z-index:0;
-}
diff --git a/assets/css/ordernumber-variables.css b/assets/css/ordernumber-variables.css
deleted file mode 100644
index df2fa261f18016ec9847277e8f841914cabf90a8..0000000000000000000000000000000000000000
--- a/assets/css/ordernumber-variables.css
+++ /dev/null
@@ -1,57 +0,0 @@
-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 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;
-}
diff --git a/assets/js/ordernumber-counter.js b/assets/js/ordernumber-counter.js
deleted file mode 100644
index a098ba787e38d8cbd75d9601019f28a84385818b..0000000000000000000000000000000000000000
--- a/assets/js/ordernumber-counter.js
+++ /dev/null
@@ -1,138 +0,0 @@
-
-String.Format = function () {
-  var s = arguments[0];
-  for (var i = 0; i < arguments.length - 1; i++ ) {       
-    var reg = new RegExp ("\\{" + i + "\\}", "gm");             
-    s = s.replace (reg, arguments[i + 1]);
-  }
-  return s;
-}
-
-var getCounterData = function (btn) {
-    var row = jQuery (btn).parents ("tr.counter_row");
-    return { row: row };
-}
-var handleJSONResponse = function (json, counter) {
-//     updateMessages (json['messages'], "ordernumber");
-    if (!json.authorized && !json.success) {
-        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);
-	counter.type = nrtype;
-	counter.counter = ctr;
-	counter.value = value;
-	var value = NaN;
-	var msgprefix = "";
-	while (isNaN (value) && (value != null)) {
-		value = prompt (String.Format (ajax_ordernumber.ORDERNUMBER_JS_EDITCOUNTER, msgprefix, counter.counter, counter.value), counter.value);
-		if (value != null)
-			value = parseInt (value);
-		if (isNaN (value)) 
-			msgprefix = ajax_ordernumber.ORDERNUMBER_JS_INVALID_COUNTERVALUE;
-	}
-	if (value != null) {
-		var loading = jQuery ("img.ordernumber-loading").first ().clone ().insertAfter (btn).show ();
-		jQuery.ajax ({
-			type: "POST",
-			url: ajax_ordernumber.ajax_url,
-			data: {
-				action: 'set_counter',
-				nrtype: counter.type, 
-				counter: counter.counter, 
-				value: value,
-			},
-			success: function ( json ) {
-				try {
-					handleJSONResponse (json, counter);
-				} catch (e) {
-					alert (ajax_ordernumber.ORDERNUMBER_JS_JSONERROR + "\n" + e);
-					return;
-				}
-				if (json.success>0) {
-					jQuery (counter.row).children (".counter_value").text (value);
-				} else {
-					alert (String.Format (ajax_ordernumber.ORDERNUMBER_JS_MODIFY_FAILED, counter.counter));
-				}
-			},
-			error: function () { alert (String.Format (ajax_ordernumber.ORDERNUMBER_JS_MODIFY_FAILED, counter.counter)); },
-			complete: function () { jQuery (loading).remove (); },
-		});
-	}
-}
-var ajaxDeleteCounter = function (btn, nrtype, ctr, value) {
-	var counter = getCounterData (btn);
-	counter.type = nrtype;
-	counter.counter = ctr;
-	counter.value = value;
-	var proceed = confirm (String.Format (ajax_ordernumber.ORDERNUMBER_JS_DELETECOUNTER, counter.counter, counter.value));
-	if (proceed == true) {
-		var loading = jQuery ("img.ordernumber-loading").first ().clone ().insertAfter (btn).show ();
-		jQuery.ajax ({
-			type: "POST",
-			dataType: "json",
-			url: ajax_ordernumber.ajax_url,
-			data: {
-				action: 'delete_counter',
-				nrtype: counter.type, 
-				counter: counter.counter,
-			},
-			success: function ( json ) {
-				try {
-					handleJSONResponse (json, counter);
-				} catch (e) {
-					alert (ajax_ordernumber.ORDERNUMBER_JS_JSONERROR + "\n" + e);
-					return;
-				}
-				if (json.success>0) {
-					jQuery (counter.row).fadeOut (1200, function () { jQuery (counter.row).remove (); });
-				} else {
-					alert (String.Format (ajax_ordernumber.ORDERNUMBER_JS_DELETE_FAILED, 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 (ajax_ordernumber.ORDERNUMBER_JS_NEWCOUNTER);
-	if (countername != null) {
-		var loading = jQuery ("img.ordernumber-loading").first ().clone ().insertAfter (jQuery (btn).find ("img.ordernumber-counter-addbtn")).show ();
-		jQuery.ajax ({
-			type: "POST",
-			dataType: "json",
-			url: ajax_ordernumber.ajax_url,
-			data: {
-				action: 'add_counter',
-				nrtype: nrtype, 
-				counter: countername,
-				value: 0,
-			},
-			
-			success: function ( json ) {
-				try {
-					handleJSONResponse (json, null);
-				} catch (e) {
-					alert (ajax_ordernumber.ORDERNUMBER_JS_JSONERROR + "\n" + e);
-					return;
-				}
-				if (json.success>0) {
-					if (json.newrow) {
-						jQuery (row).before (jQuery (json.newrow));
-					}
-				} else {
-					alert (String.Format (ajax_ordernumber.ORDERNUMBER_JS_ADD_FAILED, countername));
-				}
-			},
-			error: function () { alert (String.Format (ajax_ordernumber.ORDERNUMBER_JS_ADD_FAILED, countername)); },
-			complete: function () { jQuery (loading).remove (); },
-		});
-	}
-}
diff --git a/assets/js/ordernumber-variables.js b/assets/js/ordernumber-variables.js
deleted file mode 100644
index b9508ff3e8f7bcb5ac13ba6d02aeebaa9b1c8faf..0000000000000000000000000000000000000000
--- a/assets/js/ordernumber-variables.js
+++ /dev/null
@@ -1,25 +0,0 @@
-var ordernumberVariablesAddRow = function (template, element) {
-	jQuery("#" + template + " tr").clone(true)
-		.appendTo("table#" + element + " tbody")
-		.find('input,select,button,img')
-		.removeAttr('disabled');
-	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');
-			}
-		}
-	);
-});
diff --git a/library/css/ordernumber.css b/library/css/ordernumber.css
new file mode 100644
index 0000000000000000000000000000000000000000..17a098bbfa9e293d630ca4bdb3963cf984a86847
--- /dev/null
+++ b/library/css/ordernumber.css
@@ -0,0 +1,131 @@
+
+table.ordernumber-countertable {
+    border: 1px solid #888888;
+    display: inline-table;
+	width: inherit;
+}
+
+table.ordernumber-countertable td, table.ordernumber-countertable th {
+    padding-bottom: 0px;
+    padding-top: 0px;
+}
+table.ordernumber-countertable thead th {
+    text-align: center;
+	width: auto;
+}
+table.ordernumber-countertable thead > tr:nth-child(odd) > th,
+table.ordernumber-countertable tfoot > tr.addcounter_row > td {
+    background: #E0E0E0;
+}
+table.ordernumber-countertable tbody > tr:nth-child(even) > td {
+    background: #F0F0F0;
+}
+table.ordernumber-countertable.table-striped tbody > tr:nth-child(odd) > th {
+    background: #E0E0E0;
+}
+.ordernumber-btn {
+    cursor: pointer;
+}
+
+col.counter_type, th.counter_type, td.counter_type {
+    display:none;
+}
+td.counter_value {
+    text-align: center;
+}
+
+fieldset table.ordernumber-countertable img {
+    padding: 0;
+    margin: 0;    
+}
+div.ordernumber-ajax-loading, div.ordernumber-counter-addbtn {
+    display: inline;
+}
+div.ordernumber-ajax-loading, div.ordernumber-ajax-loading img.ordernumber-btn {
+    position: relative;
+    top: 0; left: 0;
+}
+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 */
+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/assets/images/icon-16-delete.png b/library/images/icon-16-delete.png
similarity index 100%
rename from assets/images/icon-16-delete.png
rename to library/images/icon-16-delete.png
diff --git a/assets/images/icon-16-edit.png b/library/images/icon-16-edit.png
similarity index 100%
rename from assets/images/icon-16-edit.png
rename to library/images/icon-16-edit.png
diff --git a/assets/images/icon-16-new.png b/library/images/icon-16-new.png
similarity index 100%
rename from assets/images/icon-16-new.png
rename to library/images/icon-16-new.png
diff --git a/assets/images/loading.gif b/library/images/loading.gif
similarity index 100%
rename from assets/images/loading.gif
rename to library/images/loading.gif
diff --git a/assets/images/loading.png b/library/images/loading.png
similarity index 100%
rename from assets/images/loading.png
rename to library/images/loading.png
diff --git a/library/js/ordernumber.js b/library/js/ordernumber.js
new file mode 100644
index 0000000000000000000000000000000000000000..1e816b57a0816b04961c65398eed8801f5ececbf
--- /dev/null
+++ b/library/js/ordernumber.js
@@ -0,0 +1,226 @@
+/**********************************************************************************
+ * The global ajax_ordernumber object should have the following entries:
+ *   - Translations: 
+ *     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
+ *   - ajax_url: The URL for all AJAX calls
+
+ * Optional entries (callback functions) are:
+ *  - updateMessages(messages, cssidentifier)
+ *  - parseAjaxResponse(response) => return json
+ *  - modifyAjaxArgs(ajaxargs)    => return ajaxargs with modified arguments for jquery.ajax calls
+ */
+ 
+/**********************************************************************************
+ * 
+ *  Javascript for the counter modification table
+ * 
+ **********************************************************************************/
+String.Format = function() {
+  var s = arguments[0];
+  for (var i = 0; i < arguments.length - 1; i++) {       
+    var reg = new RegExp("\\{" + i + "\\}", "gm");             
+    s = s.replace(reg, arguments[i + 1]);
+  }
+  return s;
+}
+
+var getCounterData = function (btn) {
+    var row = jQuery(btn).closest("tr.counter_row");
+    return { row: row };
+}
+var handleJSONResponse = function (json, counter) {
+	if ('updateMessages' in ajax_ordernumber) { 
+		ajax_ordernumber.updateMessages(json['messages'], "ordernumber");
+	}
+	if (!json.authorized && !json.success) {
+		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);
+    counter.type=nrtype;
+    counter.counter=ctr;
+    counter.value=value;
+    var value = NaN;
+    var msgprefix = "";
+    while (isNaN(value) && (value != null)) {
+        value = prompt (String.Format(ajax_ordernumber.ORDERNUMBER_JS_EDITCOUNTER, msgprefix, counter.counter, counter.value), counter.value);
+        if (value != null)
+            value = parseInt(value);
+        if (isNaN(value)) 
+            msgprefix = ajax_ordernumber.ORDERNUMBER_JS_INVALID_COUNTERVALUE;
+    }
+    if (value != null) {
+        var loading = jQuery("img.ordernumber-loading").first().clone().insertAfter(btn).show();
+        var ajaxargs = {
+            type: "POST",
+            url: ajax_ordernumber.ajax_url,
+            data: { 
+				action: 'setCounter',
+				nrtype: counter.type, 
+				counter: counter.counter, 
+				value: value 
+			},
+			success: function ( json ) {
+                try {
+					if ('parseAjaxResponse' in ajax_ordernumber) { 
+						json = ajax_ordernumber.parseAjaxResponse(json);
+					}
+                    handleJSONResponse(json, counter);
+                } catch (e) {
+                    alert(ajax_ordernumber.ORDERNUMBER_JS_JSONERROR+"\n"+e);
+                    return;
+                }
+                if (json.success>0) {
+					// 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(ajax_ordernumber.ORDERNUMBER_JS_MODIFY_FAILED, counter.counter));
+                }
+            },
+            error: function() { alert (String.Format(ajax_ordernumber.ORDERNUMBER_JS_MODIFY_FAILED, counter.counter)); },
+            complete: function() { jQuery(loading).remove(); },
+        };
+		if ('modifyAjaxArgs' in ajax_ordernumber) { 
+			ajaxargs = ajax_ordernumber.modifyAjaxArgs(ajaxargs);
+		}
+		jQuery.ajax(ajaxargs);
+    }
+}
+var ajaxDeleteCounter = function (btn, nrtype, ctr, value) {
+    var counter = getCounterData(btn);
+    counter.type=nrtype;
+    counter.counter=ctr;
+    counter.value=value;
+    var proceed = confirm (String.Format(ajax_ordernumber.ORDERNUMBER_JS_DELETECOUNTER, counter.counter, counter.value));
+    if (proceed == true) {
+        var loading = jQuery("img.ordernumber-loading").first().clone().insertAfter(btn).show();
+        var ajaxargs = {
+            type: "POST",
+			dataType: "json",
+            url: ajax_ordernumber.ajax_url,
+            data: { 
+				action: 'deleteCounter',
+				nrtype: counter.type, 
+				counter: counter.counter 
+			},
+			success: function ( json ) {
+                try {
+					if ('parseAjaxResponse' in ajax_ordernumber) { 
+						json = ajax_ordernumber.parseAjaxResponse(json);
+					}
+                    handleJSONResponse(json, counter);
+                } catch (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(ajax_ordernumber.ORDERNUMBER_JS_DELETE_FAILED, counter.counter));
+                }
+            },
+            error: function() { alert (String.Format(ajax_ordernumber.ORDERNUMBER_JS_DELETE_FAILED, counter.counter)); },
+            complete: function() { jQuery(loading).remove(); },
+        };
+		if ('modifyAjaxArgs' in ajax_ordernumber) { 
+			ajaxargs = ajax_ordernumber.modifyAjaxArgs(ajaxargs);
+		}
+		jQuery.ajax(ajaxargs);
+    }
+}
+var ajaxAddCounter = function (btn, nrtype) {
+    var row = jQuery(btn).parents("tr.addcounter_row");
+    var countername = prompt (ajax_ordernumber.ORDERNUMBER_JS_NEWCOUNTER);
+    if (countername != null) {
+        var loading = jQuery("img.ordernumber-loading").first().clone().insertAfter(jQuery(btn).find("img.ordernumber-counter-addbtn")).show();
+        var ajaxargs = {
+            type: "POST",
+			dataType: "json",
+            url: ajax_ordernumber.ajax_url,
+            data: { 
+				action: "addCounter",
+				nrtype: nrtype, 
+				counter: countername 
+			},
+			
+			success: function ( json ) {
+                try {
+					if ('parseAjaxResponse' in ajax_ordernumber) { 
+						json = ajax_ordernumber.parseAjaxResponse(json);
+					}
+                    handleJSONResponse(json, null);
+                } catch (e) {
+                    alert(ajax_ordernumber.ORDERNUMBER_JS_JSONERROR+"\n"+e);
+                    return;
+                }
+                if (json.success>0) {
+                    if (json.row) {
+                        jQuery(row).before(jQuery(json.row));
+                    }
+                } else {
+                    alert (String.Format(ajax_ordernumber.ORDERNUMBER_JS_ADD_FAILED, countername));
+                }
+            },
+            error: function() { alert (String.Format(ajax_ordernumber.ORDERNUMBER_JS_ADD_FAILED, countername)); },
+            complete: function() { jQuery(loading).remove(); },
+        };
+// console.log("Ajaxargs", ajaxargs);
+		if ('modifyAjaxArgs' in ajax_ordernumber) { 
+			ajaxargs = ajax_ordernumber.modifyAjaxArgs(ajaxargs);
+		}
+		jQuery.ajax(ajaxargs);
+    }
+}
+
+
+
+
+/**********************************************************************************
+ * 
+ *  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');
+
+	if (jQuery.fn.chosen) {
+		// 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 () {
+			var count = jQuery(this).closest('table').find('tbody tr').length;
+			if (count<=1) {
+				jQuery("tr#ordernumber-replacements-empty-row")
+					.removeClass("rowhidden")
+					.find('input,select,button,img')
+					.removeAttr('disabled');
+			}
+			jQuery(this).closest('tr').remove();
+		}
+	);
+
+	jQuery("#ordernumber_variables tbody").sortable();
+});
diff --git a/library/ordernumber_helper.php b/library/ordernumber_helper.php
new file mode 100644
index 0000000000000000000000000000000000000000..ecfb0f99ec4f45c5e60760d2d656ed9fad7e4be2
--- /dev/null
+++ b/library/ordernumber_helper.php
@@ -0,0 +1,563 @@
+<?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
+**/
+
+if ( !defined( 'ABSPATH' ) && !defined('_JEXEC') ) { 
+	die( 'Direct Access to ' . basename( __FILE__ ) . ' is not allowed.' );
+}
+
+class OrdernumberHelper {
+	static $_version = "0.1";
+	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;
+		}
+	}
+
+	/**
+	 * Provide human-readable default values for the translatable strings.
+	 * Some systems use the translation key as fallback if no translation is found,
+	 * so we need to convert it to a human-readable value.
+	 */
+	public function readableString($string) {
+		static $readable_strings = array(
+			"PLG_ORDERNUMBER_COUNTERLIST_HEADER_VALUE"   => 'Counter value',
+			"PLG_ORDERNUMBER_COUNTERLIST_HEADER_COUNTER" => 'Counter name',
+			"PLG_ORDERNUMBER_COUNTERLIST_ADD"            => 'Add new counter',
+			"PLG_ORDERNUMBER_COUNTERLIST_GLOBAL"         => '[Global]',
+			"PLG_ORDERNUMBER_REPL_IFVAR"                 => 'If variable ...',
+			"PLG_ORDERNUMBER_REPL_IFVAL"                 => 'Value',
+			"PLG_ORDERNUMBER_REPL_SETVAR"                => 'Set variable ...',
+			"PLG_ORDERNUMBER_REPL_TOVAL"                 => 'to value ...',
+			"PLG_ORDERNUMBER_REPL_NOCUSTOMVARS"          => 'No custom variables have been defined.',
+			"PLG_ORDERNUMBER_REPL_ADDVAR"                => 'Add new custom variable',
+			"PLG_ORDERNUMBER_REPL_OP_NOCOND"             => 'No condition',
+			"PLG_ORDERNUMBER_REPL_OP_CONTAINS"           => 'contains',
+			"PLG_ORDERNUMBER_REPL_OP_STARTS"             => 'starts with',
+			"PLG_ORDERNUMBER_REPL_OP_ENDS"               => 'ends with'
+		);
+		// Use the human-readable text as default rather than the generic identifier.
+		// Otherwise, one always has to create a language file for every language, as 
+		// the fallback would be the identifier.
+		if (isset($readable_strings[$string]))
+			return $readable_strings[$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)!');
+		}
+	}
+	
+	public function getAllCounters($type) {
+		if (isset($this->callbacks['getCounter'])) {
+			return $this->callbacks['getCounter']($type);
+		} else {
+			throw new Exception ('No callback defined for getAllCounters(type)!');
+		}
+	}
+	
+	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;
+	}
+	
+	/* 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) {
+        $html=array();
+        $html[] = "<img src='" . $this->urlPath ('images', 'loading.gif') . "' class='ordernumber-loading' />";
+        $html[] = "<table class=\"ordernumber-countertable " . $this->getStyle('counter-table-class') . "\" " . $this->getStyle('counter-table-style') . ">";
+        $html[] = "<thead>";
+        $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[] = "</thead>";
+        $html[] = "	<colgroup><col class='counter_type'><col style=\"text-align: center\" ><col ></colgroup>";
+        $html[] = "<tbody>";
+        foreach ($counters as $c) {
+			$cc = (object)$c;
+            $html[] = $this->counter_modification_create_row ($type, $cc->name, $cc->value);
+        }
+        $html[] = "</tbody>";
+        $html[] = "<tfoot>";
+        $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[] = "</tfoot>";
+        $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'>" . htmlentities((string)(($counter=="")?($this->__ ('PLG_ORDERNUMBER_COUNTERLIST_GLOBAL')):$counter)) . "</td>";
+		$html[] = "		<td class='counter_value'>" . htmlentities((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).", " . json_encode($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).", " . json_encode($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'])?htmlentities($values['conditionvar']):'') . '" ' . htmlentities($disabled) . '/></td>
+        	<td class="variables_ifop"      ><select name="' . $name . '[conditionop][]" ' . htmlentities($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']:'') . '" ' . htmlentities($disabled) . '/></td>
+        	<td class="variables_then">=></td>
+        	<td class="variables_thenvar"><input name="' . $name . '[newvar][]"       value="' . (isset($values['newvar'])?$values['newvar']:'') .       '" ' . htmlentities($disabled) . '/></td>
+        	<td class="variables_thenval"><input name="' . $name . '[newval][]"       value="' . (isset($values['newval'])?$values['newval']:'') .       '" ' . htmlentities($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_woocommerce.php b/ordernumber_helper_woocommerce.php
new file mode 100644
index 0000000000000000000000000000000000000000..a33ab2c389c7062d99c0e021343973f314ae62cd
--- /dev/null
+++ b/ordernumber_helper_woocommerce.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * Advanced Ordernumbers generic helper class for WooCommerce
+ * 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
+**/
+
+if ( !defined( 'ABSPATH' ) ) { 
+	die( 'Direct Access to ' . basename( __FILE__ ) . ' is not allowed.' );
+}
+if (!class_exists( 'OrdernumberHelper' )) 
+	require_once (dirname(__FILE__) . '/library/ordernumber_helper.php');
+
+class OrdernumberHelperWooCommerce extends OrdernumberHelper {
+	public static $ordernumber_counter_prefix = '_ordernumber_counter_';
+
+	function __construct() {
+		parent::__construct();
+		load_plugin_textdomain('opentools-ordernumbers', false, basename( dirname( __FILE__ ) ) . '/languages' );
+		// WC-specific Defaults for the HTML tables
+		$this->_styles['counter-table-class']  = "widefat";
+		$this->_styles['variable-table-class'] = "widefat wc_input_table sortable";
+		
+	}
+
+	static function getHelper() {
+		static $helper = null;
+		if (!$helper) {
+			$helper = new OrdernumberHelperWooCommerce();
+		}
+		return $helper;
+    }
+	
+	/**
+	 * HELPER FUNCTIONS, WooCommerce-specific
+	 */
+	public function __($string) {
+		$string = $this->readableString($string);
+		return __($string, 'opentools-advanced-ordernumbers');
+	}
+	function urlPath($type, $file) {
+		return plugins_url('library/' . $type . '/' . $file, __FILE__);
+    }
+    
+    public function print_admin_styles() {
+		wp_register_style('ordernumber-styles',  $this->urlPath('css', 'ordernumber.css'));
+		wp_enqueue_style('ordernumber-styles');
+	}
+	
+	public function print_admin_scripts() {
+		wp_register_script( 'ordernumber-script', $this->urlPath('js', 'ordernumber.js',  __FILE__), array('jquery') );
+		wp_enqueue_script( 'ordernumber-script');
+		
+		// Handle the translations:
+		$localizations = array( 'ajax_url' => admin_url( 'admin-ajax.php' ) );
+		
+		$localizations['ORDERNUMBER_JS_JSONERROR'] = $this->__("Error reading response from server:");
+		$localizations['ORDERNUMBER_JS_NOT_AUTHORIZED'] = $this->__("You are not authorized to modify order number counters.");
+		$localizations['ORDERNUMBER_JS_NEWCOUNTER'] = $this->__("Please enter the format/name of the new counter:");
+		$localizations['ORDERNUMBER_JS_ADD_FAILED'] = $this->__("Failed adding counter {0}");
+		$localizations['ORDERNUMBER_JS_INVALID_COUNTERVALUE'] = $this->__("You entered an invalid value for the counter.\n\n");
+		
+		$localizations['ORDERNUMBER_JS_EDITCOUNTER'] = $this->__("{0}Please enter the new value for the counter '{1}' (current value: {2}):");
+		$localizations['ORDERNUMBER_JS_MODIFY_FAILED'] = $this->__("Failed modifying counter {0}");
+		$localizations['ORDERNUMBER_JS_DELETECOUNTER'] = $this->__("Really delete counter '{0}' with value '{1}'?");
+		$localizations['ORDERNUMBER_JS_DELETE_FAILED'] = $this->__("Failed deleting counter {0}");
+
+		// in JavaScript, object properties are accessed as ajax_object.ajax_url, ajax_object.we_value
+		wp_localize_script( 'ordernumber-script', 'ajax_ordernumber', $localizations );
+	}
+
+
+
+
+ 	function getAllCounters($type) {
+		$counters = array();
+		$pfxlen = strlen(self::$ordernumber_counter_prefix );
+		foreach (wp_load_alloptions() as $name => $value) {
+			if (substr($name, 0, $pfxlen) == self::$ordernumber_counter_prefix) {
+				$parts = explode('-', substr($name, $pfxlen), 2);
+				if (sizeof($parts)==1) {
+					array_unshift($parts, 'ordernumber');
+				}
+// print("<h1>$type Counter: $name with value $value encountered, prefix=".self::$ordernumber_counter_prefix.", parts=".print_r($parts,1)."</h1>");
+				if ($parts[0]==$type) {
+					$counters[] = array(
+						'type'  => $parts[0],
+						'name'  => $parts[1],
+						'value' => $value,
+					);
+				}
+			}
+		}
+		return $counters;
+	}
+
+    function getCounter($type, $format, $default=0) {
+		return get_option (self::$ordernumber_counter_prefix.$type.'-'.$format, $default);
+	}
+    
+	function addCounter($type, $format, $value) {
+		return $this->setCounter($type, $format, $value);
+	}
+
+	function setCounter($type, $format, $value) {
+		return update_option(self::$ordernumber_counter_prefix.$type.'-'.$format, $value);
+	}
+
+	function deleteCounter($type, $format) {
+		return delete_option(self::$ordernumber_counter_prefix.$type.'-'.$format);
+	}
+
+
+}
diff --git a/ordernumbers_woocommerce.php b/ordernumbers_woocommerce.php
new file mode 100644
index 0000000000000000000000000000000000000000..d6b2c6c8d392fb1b3216a451f9c8950081604a4f
--- /dev/null
+++ b/ordernumbers_woocommerce.php
@@ -0,0 +1,487 @@
+<?php
+/**
+ * This is the actual ordernumber plugin class for WooCommerce.
+ * Copyright (C) 2015 Reinhold Kainhofer, Open Tools
+ * Author: Open Tools, Reinhold Kainhofer
+ * Author URI: http://open-tools.net
+ * License: GPL2+
+*/
+if ( ! defined( 'ABSPATH' ) ) { 
+	exit; // Exit if accessed directly
+}
+class OpenToolsOrdernumbers {
+	public $ordernumber_meta = "_oton_number_";
+	public $ordernumber_new_placeholder = "[New Order]";
+	
+	protected $helper = null;
+	protected $settings = array();
+	
+	/**
+	 * Construct the plugin object
+	 */
+	public function __construct()
+	{
+		$this->helper = OrdernumberHelperWooCommerce::getHelper();
+		$this->initializeSettings();
+		$this->initializeHooks();
+		
+		$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'));
+	}
+
+	/**
+	 * Install all neccessary filters and actions for this plugin
+	 */
+	protected function initializeHooks() {
+		$helper = OrdernumberHelperWooCommerce::getHelper();
+		// CONFIGURATION SCREENS
+		add_filter( 'woocommerce_get_sections_checkout',                array($this, 'add_admin_section'));
+		// The checkout settings page assumes all subpages are payment gateways, so we have to override this and manually pass our settings:
+		add_action( 'woocommerce_settings_checkout',                    array( $this, 'settings_output' ) );
+		add_action( 'woocommerce_settings_save_checkout',               array( $this, 'settings_save' ) );
+		add_action( 'woocommerce_admin_field_ordernumber_counters',     array( $this, 'admin_field_counters' ) );
+		add_action( 'woocommerce_admin_field_ordernumber_variables',    array( $this, 'admin_field_variables' ) );
+		add_action( 'pre_update_option_ordernumber_variables',          array( $this, 'update_option_variables'));
+		
+		add_action( 'woocommerce_order_actions',                        array( $this, 'add_order_action_new_number' ) );
+		add_action( 'woocommerce_order_action_assign_new_ordernumber',  array( $this, 'order_action_assign_new_ordernumber' ) );
+
+		add_action( 'admin_print_styles-woocommerce_page_wc-settings',  array($helper, 'print_admin_styles'));
+		add_action( 'admin_print_scripts-woocommerce_page_wc-settings', array($this, 'print_admin_scripts'));
+		
+		// AJAX counter modifications
+		add_action( 'wp_ajax_setCounter',    array($this, 'counter_set_callback') );
+		add_action( 'wp_ajax_addCounter',    array($this, 'counter_add_callback') );
+		add_action( 'wp_ajax_deleteCounter', array($this, 'counter_delete_callback') );
+
+		// Add the ordernumber post meta to the search in the backend
+		add_filter( 'woocommerce_shop_order_search_fields', array($this, 'order_search_fields'));
+		// Sort the order list in the backend by order number rather than ID, make sure this is called LAST so we modify the defaults passed as arguments
+		add_filter( 'manage_edit-shop_order_sortable_columns', array( $this, 'modify_order_column_sortkey' ), 9999 );
+
+		// When a new order is created, we immediately assign the order number:
+		add_action( 'wp_insert_post', array(&$this, 'check_assignNumber'), 10, 3);
+// 		add_action( 'save_post', array(&$this, 'check_assignNumber'), 10, 3);
+		// The filter to actually return the order number for the given order
+		add_filter ('woocommerce_order_number', array(&$this, 'get_ordernumber'), 10, 2/*<= Also get the order object! */);
+	}
+	
+
+	/**
+	 * Setup all options and the corresponding settings UI to configure this plugin, using the WP settings API
+	 */
+	protected function initializeSettings() {
+		// Init settings
+		$this->settings = array(
+			array(
+				'name' 		=> $this->helper->__( 'Advanced Order Numbers'),
+				'desc'		=> $this->helper->__( 'Configure the format and the counters of the order numbers in WooCommerce.'),
+				'type' 		=> 'title',
+				'id' 		=> 'ordernumber_options'
+			),
+
+			array(
+				'name' 		=> $this->helper->__( 'Customize Order Numbers'),
+				'desc' 		=> $this->helper->__( 'Check to use custom order numbers rather than the default wordpress post ID.'),
+				'id' 		=> 'customize_ordernumber',
+				'type' 		=> 'checkbox',
+				'default'	=> 'no'
+			),
+			array(
+				'title'		=> $this->helper->__( 'Order number format'),
+				'desc' 		=> $this->helper->__( 'The format for the order numbers (variables can be entered as [...], the counter is indicated by the #). To use a different counter name than displayed, put the custom counter name after a |, e.g. "[year]-[month]/#|[year]" to use the month in the order number, but reset the counter only yearly. Advanced settings for the counter can be added as [#####:start/step], e.g. [#:100] to start new counters at 100, or [#/5] to increment the counter by 5. The number of # in the format determine how many digits are shown at least, e.g. [########] will always show at least 8 digits for the counter, e.g. 00000014.'),
+				'desc_tip'	=> true,
+				'id' 		=> 'ordernumber_format',
+				'default'	=> '#',
+				'type' 		=> 'text',
+				'css'		=> 'width: 100%',
+			),
+			array(
+				'title'		=> $this->helper->__( 'Use global counter'),
+				'desc' 		=> $this->helper->__( 'A global counter is independent of the number format and will never reset. A non-global counter runs within the number format and will start from the inital value whenever any of the variables used in the format changes (to be precise: a new counter will be used, so it is possible to have multiple counters running in parallel).'),
+				'desc_tip'	=> true,
+				'id' 		=> 'ordernumber_global',
+				'type' 		=> 'checkbox',
+				'default'	=> 'no',
+			),
+			array(
+				'name' 		=> $this->helper->__( 'All order number counters'),
+				'desc'		=> $this->helper->__( 'View and modify the current counter values. The counter value is the value used for the previous number. All changes are immediately applied!'),
+				'desc_tip'	=> true,
+				'id' 		=> 'ordernumber_counters',
+				'type' 		=> 'ordernumber_counters',
+				'nrtype' 	=> 'ordernumber',
+			),
+			array( 'type' => 'sectionend', 'id' => 'ordernumber_options' ),
+			
+			// TODO: customize order password, and other numbers!
+
+			array(
+				'name' 		=> $this->helper->__( 'Custom Variables'),
+				'desc'		=> $this->helper->__( 'Define your own (conditional) variables for use in the number formats'),
+				'type' 		=> 'title',
+				'id' 		=> 'ordernumber_variables'
+			),
+			array(
+				'id' 		=> 'ordernumber_variables',
+				'type' 		=> 'ordernumber_variables',
+			),
+			array( 'type' => 'sectionend', 'id' => 'ordernumber_variables' ),
+
+			array(
+				'name' 		=> $this->helper->__( 'Current Counters'),
+				'type' 		=> 'title',
+				'id' 		=> 'ordernumber_counters'
+			),
+			array( 'type' => 'sectionend', 'id' => 'ordernumber_counters' ),
+
+		);
+		// Default options
+
+		add_option ('customize_ordernumber', 'no');
+		add_option ('ordernumber_format',    "#");
+		add_option ('ordernumber_global',    'no');
+		
+ 		add_option ('ordernumber_variables',  array());
+	}
+
+	/**
+	 * Insert our own section in the checkout setting page. Rearrange the sections array to make sure our settings
+	 * come second place, directly after the default page with the '' key and before all the payment gateways
+	 */
+	function add_admin_section($sections) {
+		$newsections = array();
+		foreach ($sections as $sec => $name ) {
+			$newsections[$sec] = $name;
+			if ($sec == '') {
+				$newsections['ordernumber'] = $this->helper->__('Order Numbers');
+			}
+		}
+		return $newsections;
+	}
+	
+	public function settings_output() {
+		global $current_section;
+		if ($current_section == 'ordernumber') {
+			$settings = $this->settings;
+			WC_Admin_Settings::output_fields( $settings );
+		}
+	}
+
+	public function settings_save() {
+		global $current_section;
+		if ($current_section == 'ordernumber') {
+			$settings = $this->settings;
+			WC_Admin_Settings::save_fields( $settings );
+		}
+	}
+	
+	/** 
+	 * Print the JS for the counter values and counter variables tables to the page header in the WC backend admin setting page
+	 */
+	public function print_admin_scripts() {
+		$this->helper->print_admin_scripts();
+		wp_register_script( 'ordernumber-admin', plugins_url('assets/js/ordernumber-config.js', __FILE__), array('jquery'));
+		wp_enqueue_script( 'ordernumber-admin');
+	}
+
+	/**
+	 * Render the Custom Variables configuration table
+	 */
+	public function admin_field_variables($settings) {
+		$variables = get_option( $settings['id'], array() );
+		if (!is_array($variables)) {
+			$variables = array();
+		} ?>
+		<tr valign="top">
+		    <td class="forminp forminp-<?php echo sanitize_title( $settings['type'] ) ?>" colspan="2">
+				<?php
+					print $this->helper->custom_variables_create_table($settings['id'], $variables);
+				?>
+			</td>
+		</tr> 
+		<?php
+	}
+
+	/** 
+	 * Store the variable replacements array into the options. Need to transpose the array before we can store it into the options...
+	 * This filter is called directly before the option is saved.
+	 */
+	public function update_option_variables ($value) {
+		return OrdernumberHelper::transposeCustomVariables($value);
+	}
+
+	
+	/**
+	 * Render the Counter Values modification table
+	 */
+	public function admin_field_counters ($settings) {
+		// Description handling
+		$field_description = WC_Admin_Settings::get_field_description( $settings );
+		extract( $field_description );
+		?>
+
+		
+		<tr valign="top">
+			<th scope="row" class="titledesc">
+				<label for="<?php echo esc_attr( $settings['id'] ); ?>"><?php echo esc_html( $settings['title'] ); ?></label>
+				<?php echo $tooltip_html; ?>
+			</th>
+		    <td class="forminp forminp-<?php echo sanitize_title( $settings['nrtype'] ) ?>">
+				<?php 
+					$counters = $this->helper->getAllCounters($settings['nrtype']);
+					echo $this->helper->counter_modification_create_table($settings['nrtype'], $counters);
+				?>
+			</td>
+		</tr> 
+		<?php
+	}
+	
+	/** 
+	 * Hook to add the order numer post meta field to the searchable field 
+	 * (so the admin can search for the order number in the backend)
+	 */
+	public function order_search_fields($fields) {
+		$fields[] = $this->ordernumber_meta;
+		return $fields;
+	}
+	
+	/**
+	 * Sort the order list's "Order" column by our post meta rather than by ID
+	 */
+	public function modify_order_column_sortkey($columns) {
+		$columns['order_title'] = $this->ordernumber_meta;
+		return $columns;
+	}
+	
+	/**
+	 * Add the "create new order number" action to the edit order page in the Backend
+	 */
+	public function add_order_action_new_number($actions) {
+		$actions['assign_new_ordernumber'] = $this->helper->__('Assign a new order number');
+		return $actions;
+	}
+	/**
+	 * Handle the "Assign a new order number" action from the edit order page in the backend
+	 */
+	public function order_action_assign_new_ordernumber( $order ) {
+		$number = $this->generateNumber($order->id, $order, 'ordernumber');
+	}
+	
+	
+	/** 
+	 * Handle new posts created in the frontend. This action will be called for all posts, 
+	 * not only for orders, so we need to check explicitly. Also, this function will be called
+	 * for order updates, so we need to check the update argument, too.
+	 */
+	public function check_assignNumber($post_id, $post, $update) {
+		// Is the post really an order?
+		// Order numbers are only assigned to orders on creation, not when updating!
+		if ($post->post_type != 'shop_order') {
+			return;
+		} else {
+			// Handle new admin-created orders, where the address is entered later on!
+			// Assign an order number:
+			$number = $this->assign_new_ordernumber($post_id, $post, $update);
+		}
+	}
+
+	/**
+	 * AJAX Counter handling (simple loading/storing counters), storing them as options
+	 */
+	
+	public function counter_delete_callback() {
+		$json = array('action' => 'delete_counter', 'success' => 0);
+		$json['success'] = $this->helper->deleteCounter($_POST['nrtype'], $_POST['counter']);
+		wp_send_json($json);
+	}
+
+	public function counter_add_callback () {
+		$type = $_POST['nrtype'];
+		$format = $_POST['counter'];
+		$value = isset($_POST['value'])?$_POST['value']:"0";
+		$json = array('action' => 'add_counter', 'success' => 0);
+		if ($this->helper->getCounter($type, $format, -1) != -1) {
+			// Counter already exists => error message
+			$json['error'] = sprintf($this->helper->__('Counter "%s" already exists, cannot create again.'), $format);
+		} else {
+			$json['success'] = $this->helper->setCounter($type, $format, $value);
+			$json['row']  = $this->helper->counter_modification_create_row($type, $format, $value);
+		}
+		wp_send_json($json);
+	}
+	
+	public function counter_set_callback () {
+		$json = array('action' => 'set_counter', 'success' => 0);
+		$json['success'] = $this->helper->setCounter($_POST['nrtype'], $_POST['counter'], $_POST['value']);
+		$json['row']  = $this->helper->counter_modification_create_row($_POST['nrtype'], $_POST['counter'], $_POST['value']);
+		wp_send_json($json);
+	}
+	
+	
+	
+	/** ***********************************************************
+	 * 
+	 *  REPLACEMENT FUNCTIONS
+	 *
+	 **************************************************************/
+	
+	public function setupAddressReplacements(&$reps, $prefix, $address, $nrtype) {
+		$reps["[email]"]     = $address->billing_email;
+		$reps["[firstname]"] = $address->billing_first_name;
+		$reps["[lastname]"]  = $address->billing_last_name;
+
+		$reps["[company]"]   = $address->billing_company;
+		$reps["[zip]"]       = $address->billing_postcode;
+		$reps["[postcode]"]  = $address->billing_postcode;
+		$reps["[city]"]      = $address->billing_city;
+    
+		$country = $address->billing_country;
+		$state = $address->billing_state;
+		$allcountries = WC()->countries->get_countries();
+		$states = WC()->countries->get_states($country);
+		$reps["[country]"]     = $country;
+		$reps["[countryname]"] = ( isset( $allcountries[ $country ] ) ) ? $allcountries[ $country ] : $country;
+
+		$reps["[state]"]       = $state;
+		$reps["[statename]"]   = ( $country && $state && isset( $states[ $country ][ $state ] ) ) ? $states[ $country ][ $state ] : $state;
+	}
+	
+	public function setupStoreReplacements (&$reps, $order, $nrtype) {
+	}
+    
+	public function setupOrderReplacements (&$reps, $order, $nrtype) {
+		$reps["[orderid]"] = $order->id;
+		
+		if ($nrtype != 'ordernumber') {
+			$reps["[ordernumber]"] = $order->get_order_number();
+		}
+		$reps["[orderstatus]"] = $order->get_status();
+		$reps["[currency]"]    = $order->get_order_currency();
+
+		$this->setupAddressReplacements($reps, "", $order, $nrtype);
+	
+		$reps["[articles]"]    = $order->get_item_count();
+// 		$reps["[downloadpermitted]"] = $order->is_download_permitted();
+// 		$reps["[hasdownloads]"] = $order->has_downloadable_item();
+// 		$reps["[coupons]"] = $order->get_used_coupons();
+		$reps["[ordertotal]"]      = $order->get_total();
+		$reps["[amount]"]      = $order->get_total();
+		$reps["[ordersubtotal]"]      = $order->get_subtotal();
+		$reps["[totaltax]"]      = $order->get_total_tax();
+		$reps["[totalshipping]"]      = $order->get_total_shipping();
+		
+		// List-valued properties for custom variable checks:
+		// TODO: Also implement variable for:
+		//  - Shipping needed
+		//  - Downloads available
+		$lineitems = $order->get_items();
+		$skus = array();
+		$categories = array();
+		$tags = array();
+		$shippingclasses = array();
+		foreach ($lineitems as $l) {
+			$p = $order->get_product_from_item($l);
+			$skus[$p->get_sku()] = 1;
+			foreach (wc_get_product_terms( $p->id, 'product_cat') as $c) {
+				$categories[$c->slug] = 1;
+			}
+			foreach (wc_get_product_terms( $p->id, 'product_tag') as $c) {
+				$tags[$c->slug] = 1;
+			}
+			$shippingclasses[$p->get_shipping_class()] = 1;
+		}
+		$reps["[skus]"] = array_keys($skus);
+		$reps["[categories]"] = array_keys($categories);
+		$reps["[tags]"] = array_keys($tags);
+		$reps["[shippingclasses]"] = array_keys($shippingclasses);
+	}
+
+	public function setupUserReplacements (&$reps, $details, $nrtype) {
+		$reps["[ipaddress]"]   = $details->customer_ip_address;
+		$reps["[userid]"]      = $details->get_user_id();
+	}
+
+	public function setupShippingReplacements(&$reps, $order, $nrtype) {
+// 		$reps["[shippingmethod]"] = $order->getShippingMethod();
+	}
+	
+	/*public function setupInvoiceReplacements (&$reps, $invoice, $order, $nrtype) {
+		$reps["[invoiceid]"] = $invoice->getId();
+	}*/
+
+	public function setupThirdPartyReplacements (&$reps, $details, $nrtype) {
+		$reps = apply_filters( 'opentools_ordernumber_replacements', $reps, $details, $nrtype);
+	}
+
+
+	function generateNumber($orderid, $order, $type='ordernumber') {
+		if (get_option('customize_'.$type, 'no')!='no') {
+			$fmt     = get_option ($type.'_format',  "#");
+			$ctrsettings = array(
+				"${type}_format"  => '',
+				"${type}_counter" => '',
+				"${type}_global"  => get_option ($type.'_global',  'no'),
+				"${type}_padding" => 1,
+				"${type}_step"    => 1,
+				"${type}_start"   => 1,
+			);
+			$customvars = get_option ('ordernumber_variables',   array());
+
+			$number = $this->helper->createNumber ($fmt, $type, $order, $customvars, $ctrsettings);
+			update_post_meta( $orderid, $this->ordernumber_meta.$type, $number );
+			return $number;
+		} else {
+			return $orderid;
+		}
+	}
+	
+	/** 
+	 * The hook to assign a customized order number (unless the order already has one assigned)
+	 */
+	function assign_new_ordernumber($orderid, $order, $update=true) {
+		if ((!$update) /*&& ($order->post_status == 'auto-draft')*/) {
+			// New order => assign placeholder, which will later be overwritten the real order number
+			update_post_meta( $orderid, $this->ordernumber_meta.'ordernumber', $this->ordernumber_new_placeholder );
+		}
+		// If we do not have an order (yet), we cannot proceed. But we probably have created the 
+		// ordernumber placeholder for that post, so this function has done its job and we can return
+		if (!$order instanceof WC_Order) {
+			return;
+		}
+		$number = get_post_meta( $orderid, $this->ordernumber_meta.'ordernumber', 'true');
+		if ($number == $this->ordernumber_new_placeholder && $order->post_status != 'auto-draft') {
+			$number = $this->generateNumber($orderid, $order, 'ordernumber');
+			// Assign a new number
+		}
+		return $number;
+	}
+
+	/** 
+	 * The generic function to retrieve a particular number
+	 */
+	function get_number($orderid, $order, $type = 'ordernumber') {
+		$stored_number = get_post_meta( $orderid, $this->ordernumber_meta.$type, 'true');
+		if ($stored_number == $this->ordernumber_new_placeholder) {
+			// Check whether the order was now really created => create order number now
+			return $this->assign_new_ordernumber($orderid, $order, $type);
+		} elseif (!empty($stored_number)) {
+			// Order number already exists => simply return it
+			return $stored_number;
+		} else {
+			// No order number was created for this order, so simply use the orderid as default.
+			return $orderid;
+		}
+	}
+	
+	/**
+	 * Callback function for Woocommerce to retrieve the ordernumber for an order
+	 */
+	function get_ordernumber($orderid, $order) {
+		return $this->get_number($orderid, $order);
+	}
+ 
+}
diff --git a/woocommerce-advanced-ordernumbers.php b/woocommerce-advanced-ordernumbers.php
index af39a82465d6ea65345f5348cd7f327ce7dbe458..942f529d83efe052cc1108419cccbe44857a723a 100644
--- a/woocommerce-advanced-ordernumbers.php
+++ b/woocommerce-advanced-ordernumbers.php
@@ -13,13 +13,6 @@
 WC tested up to: 2.3
 */
 
-/**
- * The structure of this plugin originally followed this tutorial, although much of the plugin has been rewritten since then:
- * http://www.yaconiello.com/blog/how-to-write-wordpress-plugin/
- * A lot of coding ideas were also taken directly from the way things are implemented in WooCommerce itself.
- * The order number handling code itself comes straight from our general library, which is also used for VirtueMart and Magento.
- */
- 
 if ( ! defined( 'ABSPATH' ) ) { 
 	exit; // Exit if accessed directly
 }
@@ -28,923 +21,12 @@ if ( ! defined( 'ABSPATH' ) ) {
  **/
 if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {
 
+	require_once( plugin_dir_path( __FILE__ ) . '/ordernumber_helper_woocommerce.php');
 	if (!class_exists("OpenToolsOrdernumbers")) {
-		class OpenToolsOrdernumbers {
-			public $ordernumber_meta = "oton_number_";
-			public $ordernumber_new_placeholder = "[New Order]";
-			public $ordernumber_counter_prefix = 'ordernumber-counter-';
-			
-			
-			/**
-			 * STATIC HELPER FUNCTIONS, WooCommerce-specific
-			 */
-			public static function __($string) {
-				return __($string, 'opentools-advanced-ordernumbers');
-			}
-			public static function rel_url($file) {
-				return plugins_url($file, __FILE__);
-			}
-			public static function img_url($img) {
-				return self::rel_url('assets/images/'.$img );
-			}
-			public static function css_url($css) {
-				return self::rel_url('assets/css/'.$css );
-			}
-			public static function js_url($js) {
-				return self::rel_url('assets/js/'.$js );
-			}
-			
-			
-			
-			
-			/**
-			 * Construct the plugin object
-			 */
-			public function __construct()
-			{
-				$plugin = plugin_basename(__FILE__); 
-				load_plugin_textdomain('opentools-ordernumbers', false, basename( dirname( __FILE__ ) ) . '/languages' );
-
-				// Init settings
-				$this->settings = array(
-					array(
-						'name' 		=> self::__( 'Advanced Order Numbers'),
-						'desc'		=> self::__( 'Configure the format and the counters of the order numbers in WooCommerce.'),
-						'type' 		=> 'title',
-						'id' 		=> 'ordernumber_options'
-					),
-
-					array(
-						'name' 		=> self::__( 'Customize Order Numbers'),
-						'desc' 		=> self::__( 'Check to use custom order numbers rather than the default wordpress post ID.'),
-						'id' 		=> 'customize_ordernumber',
-						'type' 		=> 'checkbox',
-						'default'	=> 'no'
-					),
-					array(
-						'title'		=> self::__( 'Order number format'),
-						'desc' 		=> self::__( 'The format for the order numbers (variables can be entered as [...], the counter is indicated by the #). To use a different counter name than displayed, put the custom counter name after a |, e.g. "[year]-[month]/#|[year]" to use the month in the order number, but reset the counter only yearly. Advanced settings for the counter can be added as [#####:start/step], e.g. [#:100] to start new counters at 100, or [#/5] to increment the counter by 5. The number of # in the format determine how many digits are shown at least, e.g. [########] will always show at least 8 digits for the counter, e.g. 00000014.'),
-						'desc_tip'	=> true,
-						'id' 		=> 'ordernumber_format',
-						'default'	=> '#',
-						'type' 		=> 'text',
-						'css'		=> 'width: 100%',
-					),
-					array(
-						'title'		=> self::__( 'Use global counter'),
-						'desc' 		=> self::__( 'A global counter is independent of the number format and will never reset. A non-global counter runs within the number format and will start from the inital value whenever any of the variables used in the format changes (to be precise: a new counter will be used, so it is possible to have multiple counters running in parallel).'),
-						'desc_tip'	=> true,
-						'id' 		=> 'ordernumber_global',
-						'type' 		=> 'checkbox',
-						'default'	=> 'no',
-					),
-					array( 'type' => 'sectionend', 'id' => 'ordernumber_options' ),
-					
-					// TODO: customize order password, and other numbers!
-
-					array(
-						'name' 		=> self::__( 'Custom Variables'),
-						'desc'		=> self::__( 'Define your own (conditional) variables for use in the number formats'),
-						'type' 		=> 'title',
-						'id' 		=> 'ordernumber_variables'
-					),
-					array(
-						'id' 		=> 'ordernumber_variables',
-						'type' 		=> 'ordernumber_variables',
-					),
-					array( 'type' => 'sectionend', 'id' => 'ordernumber_variables' ),
-
-					array(
-						'name' 		=> self::__( 'Current Counters'),
-						'type' 		=> 'title',
-						'id' 		=> 'ordernumber_counters'
-					),
-					array(
-						'name' 		=> self::__( 'All Ordernumber Counters'),
-						'desc'		=> self::__( 'View and modify the current counter values. The counter value is the value used for the previous number. All changes are immediately applied!'),
-						'desc_tip'	=> true,
-						'id' 		=> 'ordernumber_counters',
-						'type' 		=> 'ordernumber_counters',
-					),
-					array( 'type' => 'sectionend', 'id' => 'ordernumber_counters' ),
-
-				);
-				// Default options
-
-				add_option ('customize_ordernumber', 'no');
-				add_option ('ordernumber_format',    "#");
-				add_option ('ordernumber_global',    'no');
-				
- 				add_option ('ordernumber_variables',  array());
-
-				// register filters and actions
-				
-				// CONFIGURATION SCREENS
-				add_filter( 'woocommerce_get_sections_checkout',                array($this, 'add_admin_section'));
-				// The checkout settings page assumes all subpages are payment gateways, so we have to override this and manually pass our settings:
-				add_action( 'woocommerce_settings_checkout',                    array( $this, 'settings_output' ) );
-				add_action( 'woocommerce_settings_save_checkout',               array( $this, 'settings_save' ) );
-				add_action( 'woocommerce_admin_field_ordernumber_counters',     array( $this, 'admin_field_counters' ) );
-				add_action( 'woocommerce_admin_field_ordernumber_variables',    array( $this, 'admin_field_variables' ) );
-				add_action( 'pre_update_option_ordernumber_variables',          array( $this, 'update_option_variables'));
-				
-				add_action( 'woocommerce_order_actions',                        array( $this, 'add_order_action_new_number' ) );
-				add_action( 'woocommerce_order_action_assign_new_ordernumber',  array( $this, 'order_action_assign_new_ordernumber' ) );
-
-				add_action( 'admin_print_styles-woocommerce_page_wc-settings',  array($this, 'print_admin_styles'));
-				add_action( 'admin_print_scripts-woocommerce_page_wc-settings', array($this, 'print_admin_scripts'));
-				
-				// AJAX counter modifications
-				add_action( 'wp_ajax_set_counter',    array($this, 'counter_set_callback') );
-				add_action( 'wp_ajax_add_counter',    array($this, 'counter_add_callback') );
-				add_action( 'wp_ajax_delete_counter', array($this, 'counter_delete_callback') );
-
-				// Add the ordernumber post meta to the search in the backend
-				add_filter( 'woocommerce_shop_order_search_fields', array($this, 'order_search_fields'));
-				// Sort the order list in the backend by order number rather than ID, make sure this is called LAST so we modify the defaults passed as arguments
-				add_filter( 'manage_edit-shop_order_sortable_columns', array( $this, 'modify_order_column_sortkey' ), 9999 );
-
-				// When a new order is created, we immediately assign the order number:
-				add_action( 'wp_insert_post', array(&$this, 'check_assignNumber'), 10, 3);
-// 				add_action( 'save_post', array(&$this, 'check_assignNumber'), 10, 3);
-				// The filter to actually return the order number for the given order
-				add_filter ('woocommerce_order_number', array(&$this, 'get_ordernumber'), 10, 2/*<= Also get the order object! */);
-			}
-    
-			// Activate the plugin
-			public static function activate() {}
-    
-			// Deactivate the plugin
-			public static function deactivate() {}
-
-			/**
-			 * Insert our own section in the checkout setting page. Rearrange the sections array to make sure our settings
-			 * come second place, directly after the default page with the '' key and before all the payment gateways
-			 */
-			function add_admin_section($sections) {
-				$newsections = array();
-				foreach ($sections as $sec => $name ) {
-					$newsections[$sec] = $name;
-					if ($sec == '') {
-						$newsections['ordernumber'] = self::__('Order Numbers');
-					}
-				}
-				return $newsections;
-			}
-			
-			public function settings_output() {
-				global $current_section;
-				if ($current_section == 'ordernumber') {
-					$settings = $this->settings;
-					WC_Admin_Settings::output_fields( $settings );
-				}
-			}
-
-			public function settings_save() {
-				global $current_section;
-				if ($current_section == 'ordernumber') {
-					$settings = $this->settings;
-					WC_Admin_Settings::save_fields( $settings );
-				}
-			}
-			
-			/** 
-			 * Print the CSS for the counter values and counter variables tables to the page header in the WC backend admin setting page
-			 */
-			public function print_admin_styles () {
-				wp_register_style ( 'ordernumber-counter-style',  self::css_url('ordernumber-counter.css') );
-				wp_enqueue_style('ordernumber-counter-style');
-
-				wp_register_style ( 'ordernumber-variables-style',  self::css_url('ordernumber-variables.css') );
-				wp_enqueue_style('ordernumber-variables-style');
-			}
-			/** 
-			 * Print the JS for the counter values and counter variables tables to the page header in the WC backend admin setting page
-			 */
-			public function print_admin_scripts() {
-			
-				wp_register_script( 'ordernumber-counter-script', self::js_url( 'ordernumber-counter.js',  __FILE__), array('jquery') );
-				wp_enqueue_script( 'ordernumber-counter-script');
-				
-				// Handle the translations:
-				$localizations = array( 'ajax_url' => admin_url( 'admin-ajax.php' ) );
-				
-				$localizations['ORDERNUMBER_JS_JSONERROR'] = self::__("Error reading response from server:");
-				$localizations['ORDERNUMBER_JS_NOT_AUTHORIZED'] = self::__("You are not authorized to modify order number counters.");
-				$localizations['ORDERNUMBER_JS_NEWCOUNTER'] = self::__("Please enter the format/name of the new counter:");
-				$localizations['ORDERNUMBER_JS_ADD_FAILED'] = self::__("Failed adding counter {0}");
-				$localizations['ORDERNUMBER_JS_INVALID_COUNTERVALUE'] = self::__("You entered an invalid value for the counter.\n\n");
-				
-				$localizations['ORDERNUMBER_JS_EDITCOUNTER'] = self::__("{0}Please enter the new value for the counter '{1}' (current value: {2}):");
-				$localizations['ORDERNUMBER_JS_MODIFY_FAILED'] = self::__("Failed modifying counter {0}");
-				$localizations['ORDERNUMBER_JS_DELETECOUNTER'] = self::__("Really delete counter '{0}' with value '{1}'?");
-				$localizations['ORDERNUMBER_JS_DELETE_FAILED'] = self::__("Failed deleting counter {0}");
-
-				// in JavaScript, object properties are accessed as ajax_object.ajax_url, ajax_object.we_value
-				wp_localize_script( 'ordernumber-counter-script', 'ajax_ordernumber', $localizations );
-
-
-
-				wp_register_script( 'ordernumber-variables-script', self::js_url( 'ordernumber-variables.js'), array('jquery') );
-				wp_enqueue_script( 'ordernumber-variables-script');
-				
-				wp_register_script( 'ordernumber-admin', self::js_url( 'ordernumber-config.js'), array('jquery'));
-				wp_enqueue_script( 'ordernumber-admin');
-			}
-
-			/**
-			 * Render the Custom Variables configuration table
-			 */
-			public function admin_field_variables($settings) {
-				$variables = get_option( $settings['id'], array() );
-				if (!is_array($variables)) {
-					$variables = array();
-				} ?>
-		<tr valign="top">
-		    <td class="forminp forminp-<?php echo sanitize_title( $settings['type'] ) ?>" colspan="2">
-				<?php
-					print $this->customvar_admin_table($settings['id'], $variables);
-				?>
-			</td>
-		</tr> 
-		<?php
-			}
-			
-			protected function customvar_admin_table($id, $variables) { ?>
-			
-				<table id="ordernumber_variables_template" style="display:none">
-					<?php echo $this->create_replacements_row_html($id, array(), 'disabled'); ?>
-				</table>
-
-				<table id="ordernumber_variables" class="ordernumber_variables widefat wc_input_table sortable" cellspacing="0">
-					<?php
-						$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' => '',
-						);
-					?>
-					<thead>
-						<tr>
-							<?php
-								foreach ( $columns as $key => $column ) {
-									echo '<th class="' . esc_attr( $key ) . '">' . esc_html( $column ) . '</th>';
-								}
-							?>
-						</tr>
-						<tr id = "ordernumber-replacements-empty-row" class="oton-empty-row-notice <?php echo (empty($variables))?'':'rowhidden'; ?>">
-							<td class="oton-empty-row-notice" colspan="8">
-								<em> <?php echo self::__('No custom variables have been defined.'); ?></em>
-								<input type="hidden" name="<?php echo $id; ?>" value="" <?php echo (empty($variables))?'':'disabled'; ?>>
-							</td>
-						</tr>
-					</thead>
-					<colgroup>
-						<?php 
-							foreach ($columns as $key => $column) {
-								echo '<col class="' . esc_attr($key) . '" />';
-							}
-						?>
-					</colgroup>
-
-					<tbody>
-						<?php
-						foreach ($variables as $var) {
-							echo $this->create_replacements_row_html($id, $var);
-						} ?>
-					</tbody>
-					<tfoot>
-						<tr class='addreplacement_row'>
-							<td colspan=8 class='variable_add'>
-								<div class='ordernumber-variables-addbtn ordernumber-btn' onClick="ordernumberVariablesAddRow('ordernumber_variables_template', 'ordernumber_variables')">
-									<div class='ordernumber-ajax-loading'><img src='<?php echo self::img_url( 'icon-16-new.png' ); ?>' class='ordernumber-counter-addbtn' /></div>
-									<?php _e('Add new custom variable'); ?>
-								</div>
-							</td>
-						</tr>
-					</tfoot>
-				</table>
-				<?php
-			}
-
-			protected function create_replacements_row_html($name, $values = array(), $disabled = '') {
-				$escname = esc_attr($name);
-				$operator = (isset($values['conditionop'])?$values['conditionop']:'');
-				$operators = array(
-					'equals'       => '=', 
-					'contains'     => self::__('contains'), 
-					'smaller'      => '<',
-					'smallerequal' => '<=',
-					'larger'       => '>',
-					'largerequal'  => '>=', 
-					'startswith'   => self::__('starts with'),
-					'endswith'     => self::__('ends with'),
-				);
-				// Wrap all output of the values with esc_html to prevent script injection attacks
-				$html  = '
-				<tr>
-					<td class="variables_ifvar"><input name="' . $escname . '[conditionvar][]" value="' . (isset($values['conditionvar'])?esc_html($values['conditionvar']):'') . '" ' . esc_html($disabled) . '/></td>
-					<td class="variables_ifop"      ><select name="' . $escname . '[conditionop][]" ' . $disabled . '>';
-				foreach ($operators as $op => $opname) {
-					$html .= '		<option value="' . esc_attr($op) . '" ' . (($op === $operator)?'selected':'') . '>' . esc_html($opname) . '</option>';
-				}
-				$html .= '</select></td>
-					<td class="variables_ifval"   ><input name="' . $escname . '[conditionval][]" value="' . (isset($values['conditionval'])?esc_html($values['conditionval']):'') . '" ' . esc_html($disabled) . '/></td>
-					<td class="variables_then">=></td>
-					<td class="variables_thenvar"><input name="' . $escname . '[newvar][]"       value="' . (isset($values['newvar'])?esc_html($values['newvar']):'') .       '" ' . esc_html($disabled) . '/></td>
-					<td class="variables_thenval"><input name="' . $escname . '[newval][]"       value="' . (isset($values['newval'])?esc_html($values['newval']):'') .       '" ' . esc_html($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;
-			}
-
-			/** 
-			 * Store the variable replacements array into the options. Need to transpose the array before we can store it into the options...
-			 * This filter is called directly before the option is saved.
-			 */
-			public function update_option_variables ($value) {
-				if (!is_array($value)) 
-					return array();
-				$keys = array_keys($value);
-				$vallist = array();
-				
-				foreach (array_keys($value[$keys[0]]) as $i) {
-					$entry = array();
-					foreach ($keys as $k) {
-						$entry[$k] = $value[$k][$i];
-					}
-					$vallist[] = $entry;
-				}
-				return $vallist;
-			}
-
-			
-			/**
-			 * Render the Counter Values modification table
-			 */
-			public function admin_field_counters ($settings) {
-				// Description handling
-				$field_description = WC_Admin_Settings::get_field_description( $settings );
-				extract( $field_description );
-
-				// First, get all counter names:
-				$counters = array();
-				$pfxlen = strlen($this->ordernumber_counter_prefix );
-				foreach (wp_load_alloptions() as $name => $value) {
-					if (substr($name, 0, $pfxlen) == $this->ordernumber_counter_prefix) {
-						$parts = explode('-', substr($name, $pfxlen), 2);
-						if (sizeof($parts)==1) {
-							array_unshift($parts, 'ordernumber');
-						}
-						$counters[] = array(
-							'type' => $parts[0],
-							'name' => $parts[1],
-							'value' => $value,
-						);
-					}
-				} 
-				?>
-
-				
-		<tr valign="top">
-			<th scope="row" class="titledesc">
-				<label for="<?php echo esc_attr( $settings['id'] ); ?>"><?php echo esc_html( $settings['title'] ); ?></label>
-				<?php echo $tooltip_html; ?>
-			</th>
-		    <td class="forminp forminp-<?php echo sanitize_title( $settings['type'] ) ?>">
-				<img src='<?php echo self::img_url( 'loading.gif' ); ?>' class='wc-ordernumber-loading' style="display: none; position: absolute; top: 2px; left: 0px; z-index: 9999;"/>
-				<table class="wc_ordernumber_counters widefat" cellspacing="0">
-					<?php
-						$columns = apply_filters( 'woocommerce_ordernumber_counters_columns', array(
-							'type'     => self::__( ''),
-							'name'     => self::__( 'Counter name'),
-							'value'    => self::__( 'Counter value'),
-							'settings' => ''
-						) );
-					?>
-					<thead>
-						<tr>
-							<?php
-								foreach ( $columns as $key => $column ) {
-									echo '<th class="counter_' . esc_attr( $key ) . '">' . esc_html( $column ) . '</th>';
-								}
-							?>
-						</tr>
-					</thead>
-					<colgroup>
-						<?php 
-							foreach ($columns as $key => $column) {
-								echo '<col class="counter_' . esc_attr($key) . '" />';
-							}
-						?>
-					</colgroup>
-
-					<tbody>
-						<?php
-						foreach ($counters as $counter) {
-							echo $this->create_admin_counter_row($counter['type'], $counter['name'], $counter['value']);
-						} 
-						?>
-					</tbody>
-					<tfoot>
-						<tr class='addcounter_row'>
-							<td class="counter_type"></td>
-							<td colspan=3 class='counter_add'>
-								<div class='ordernumber-counter-addbtn ordernumber-btn' onClick="ajaxAddCounter(this, 'ordernumber')">
-									<div class='ordernumber-ajax-loading'><img src='<?php echo self::img_url( 'icon-16-new.png' ); ?>' class='ordernumber-counter-addbtn' /></div>
-									<?php _e('Add new counter'); ?>
-								</div>
-							</td>
-						</tr>
-					</tfoot>
-				</table>
-			</td>
-		</tr> 
-		<?php
-			}
-			
-			public function create_admin_counter_row ($type, $format, $value=0) {
-				$html =	"
-					<tr class='counter_row counter_type_" . $type . "'>
-						<td class='counter_type'>" . self::__($type, 'wooocommerce-advanced-ordernumbers' ) . "</td>
-						<td class='counter_format'>" . (empty($format)?("<i>".self::__("[Global]")."</i>"):esc_html($format)) . "</td>
-						<td class='counter_value'>" . esc_html($value) . "</td>
-						<td class='counter_buttons'>
-							<div class='ordernumber-ajax-loading'>
-							<img src='" . self::img_url( 'icon-16-edit.png' ) . "' 
-										class='ordernumber-counter-editbtn ordernumber-btn' 
-										onClick='ajaxEditCounter(this, " . json_encode($type) . ", " . json_encode($format) . ", " . json_encode($value) . ")' />
-							</div>
-							<div class='ordernumber-ajax-loading'>
-								<img src='" . self::img_url( 'icon-16-delete.png' ) . "' 
-										class='ordernumber-counter-deletebtn ordernumber-btn' 
-										onClick='ajaxDeleteCounter(this, " . json_encode($type) . ", " . json_encode($format) . ", " . json_encode($value) . ")' />
-							</div>
-						</td>
-					</tr>";
-				return $html;
-			}
-			
-			
-			/** 
-			 * Hook to add the order numer post meta field to the searchable field 
-			 * (so the admin can search for the order number in the backend)
-			 */
-			public function order_search_fields($fields) {
-				$fields[] = $this->ordernumber_meta;
-				return $fields;
-			}
-			
-			/**
-			 * Sort the order list's "Order" column by our post meta rather than by ID
-			 */
-			public function modify_order_column_sortkey($columns) {
-				$custom = array(
-					'order_title' => $this->ordernumber_meta,
-				);
-				// Use the passed columns as "default", so effectively, only the order_title will be changed:
-				return wp_parse_args ($custom, $columns);
-			}
-			
-			/**
-			 * Add the "create new order number" action to the edit order page in the Backend
-			 */
-			public function add_order_action_new_number($actions) {
-				$actions['assign_new_ordernumber'] = self::__('Assign a new order number');
-				return $actions;
-			}
-			/**
-			 * Handle the "Assign a new order number" action from the edit order page in the backend
-			 */
-			public function order_action_assign_new_ordernumber( $order ) {
-				$number = $this->assignNumber($order->id, $order, 'ordernumber');
-			}
-			
-			
-			/** 
-			 * Handle new posts created in the frontend. This action will be called for all posts, 
-			 * not only for orders, so we need to check explicitly. Also, this function will be called
-			 * for order updates, so we need to check the update argument, too.
-			 */
-			public function check_assignNumber($post_id, $post, $update) {
-				// Is the post really an order?
-				// Order numbers are only assigned to orders on creation, not when updating!
-				if ($post->post_type != 'shop_order') {
-					return;
-				} else {
-					// Handle new admin-created orders, where the address is entered later on!
-					// Assign an order number:
-					$number = $this->assign_new_ordernumber($post_id, $post, $update);
-				}
-			}
-
-			/**
-			 * Counter handling (simple loading/storing counters), storing them as options
-			 */
-			function _getCounter($type, $format, $start=0) {
-				$count = get_option ($this->ordernumber_counter_prefix.$type.'-'.$format, $start);
-				return $count;
-			}
-			// Insert new counter value into the db or update existing one
-			function _setCounter($type, $format, $value) {
-				return update_option($this->ordernumber_counter_prefix.$type.'-'.$format, $value);
-			}
-			function _deleteCounter($type, $format) {
-				return delete_option($this->ordernumber_counter_prefix.$type.'-'.$format);
-			}
-			
-			public function counter_delete_callback() {
-				$json = array('action' => 'delete_counter', 'success' => 0);
-				$json['success'] = $this->_deleteCounter($_POST['nrtype'], $_POST['counter']);
-				wp_send_json($json);
-			}
-
-			public function counter_add_callback () {
-				$type = $_POST['nrtype'];
-				$format = $_POST['counter'];
-				$value = isset($_POST['value'])?$_POST['value']:"0";
-				$json = array('action' => 'add_counter', 'success' => 0);
-				if ($this->_getCounter($type, $format, -1) != -1) {
-					// Counter already exists => error message
-					$json['error'] = sprintf(self::__('Counter "%s" already exists, cannot create again.'), $format);
-				} else {
-					$json['success'] = $this->_setCounter($type, $format, $value);
-					$json['newrow']  = $this->create_admin_counter_row($type, $format, $value);
-				}
-				wp_send_json($json);
-			}
-			
-			public function counter_set_callback () {
-				$json = array('action' => 'set_counter', 'success' => 0);
-				$json['success'] = $this->_setCounter($_POST['nrtype'], $_POST['counter'], $_POST['value']);
-				wp_send_json($json);
-			}
-			
-			
-			
-			/** ***********************************************************
-			 * 
-			 *  REPLACEMENT FUNCTIONS
-			 *
-			 **************************************************************/
-			
-			/* 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);
-			}
-	
-			protected function setupDateTimeReplacements (&$reps, $order, $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 setupAddressReplacements(&$reps, $prefix, $address, $nrtype) {
-				$reps["[email]"]     = $address->billing_email;
-				$reps["[firstname]"] = $address->billing_first_name;
-				$reps["[lastname]"]  = $address->billing_last_name;
-
-				$reps["[company]"]   = $address->billing_company;
-				$reps["[zip]"]       = $address->billing_postcode;
-				$reps["[postcode]"]  = $address->billing_postcode;
-				$reps["[city]"]      = $address->billing_city;
-        
-				$country = $address->billing_country;
-				$state = $address->billing_state;
-				$allcountries = WC()->countries->get_countries();
-				$states = WC()->countries->get_states($country);
-				$reps["[country]"]     = $country;
-				$reps["[countryname]"] = ( isset( $allcountries[ $country ] ) ) ? $allcountries[ $country ] : $country;
-
-				$reps["[state]"]       = $state;
-				$reps["[statename]"]   = ( $country && $state && isset( $states[ $country ][ $state ] ) ) ? $states[ $country ][ $state ] : $state;
-			}
-			
-			protected function setupStoreReplacements (&$reps, $order, $nrtype) {
-			}
-    
-			protected function setupOrderReplacements (&$reps, $order, $nrtype) {
-				$reps["[orderid]"] = $order->id;
-				
-				if ($nrtype != 'ordernumber') {
-					$reps["[ordernumber]"] = $order->get_order_number();
-				}
-				$reps["[orderstatus]"] = $order->get_status();
-				$reps["[currency]"]    = $order->get_order_currency();
-
-				$this->setupAddressReplacements($reps, "", $order, $nrtype);
-	
-				$reps["[articles]"]    = $order->get_item_count();
-// 				$reps["[downloadpermitted]"] = $order->is_download_permitted();
-// 				$reps["[hasdownloads]"] = $order->has_downloadable_item();
-// 				$reps["[coupons]"] = $order->get_used_coupons();
-				$reps["[ordertotal]"]      = $order->get_total();
-				$reps["[amount]"]      = $order->get_total();
-				$reps["[ordersubtotal]"]      = $order->get_subtotal();
-				$reps["[totaltax]"]      = $order->get_total_tax();
-				$reps["[totalshipping]"]      = $order->get_total_shipping();
-				
-				// List-valued properties for custom variable checks:
-				// TODO: Also implement variable for:
-				//  - Shipping needed
-				//  - Downloads available
-				$lineitems = $order->get_items();
-				$skus = array();
-				$categories = array();
-				$tags = array();
-				$shippingclasses = array();
-				foreach ($lineitems as $l) {
-					$p = $order->get_product_from_item($l);
-					$skus[$p->get_sku()] = 1;
-					foreach (wc_get_product_terms( $p->id, 'product_cat') as $c) {
-						$categories[$c->slug] = 1;
-					}
-					foreach (wc_get_product_terms( $p->id, 'product_tag') as $c) {
-						$tags[$c->slug] = 1;
-					}
-					$shippingclasses[$p->get_shipping_class()] = 1;
-				}
-				$reps["[skus]"] = array_keys($skus);
-				$reps["[categories]"] = array_keys($categories);
-				$reps["[tags]"] = array_keys($tags);
-				$reps["[shippingclasses]"] = array_keys($shippingclasses);
-			}
-
-			protected function setupUserReplacements (&$reps, $details, $nrtype) {
-				$reps["[ipaddress]"]   = $details->customer_ip_address;
-				$reps["[userid]"]      = $details->get_user_id();
-			}
-
-			protected function setupShippingReplacements(&$reps, $order, $nrtype) {
-// 				$reps["[shippingmethod]"] = $order->getShippingMethod();
-			}
-			
-			/*protected function setupInvoiceReplacements (&$reps, $invoice, $order, $nrtype) {
-				$reps["[invoiceid]"] = $invoice->getId();
-			}*/
-
-			protected function setupThirdPartyReplacements (&$reps, $details, $nrtype) {
-				$reps = apply_filters( 'opentools_ordernumber_replacements', $reps, $details, $nrtype);
-			}
-			
-			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]+))?\]%';
-// 				$counters = array();
-				
-				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]; 
-					}
-					
-// print("<pre>Counters regexp matches: ".print_r($counters,1)."</pre>");
-					$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];
-				
-// print("<pre>Counter settings are: ".print_r($ctrsettings,1)."</pre>");
-				return $ctrsettings;
-			}
-
-			/* replace the variables in the given format. $type indicates the type of number, currently only 'ordernumber', because WooCommerce does not support invoices or customer numbers. We might allow the shop owner to customize the order password, though. */
-			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);
-
-				$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"]);
-				return $number;
-			}
-			
-			function assignNumber($orderid, $order, $type='ordernumber') {
-				if (get_option('customize_'.$type, 'no')!='no') {
-					$fmt     = get_option ($type.'_format',  "#");
-					$ctrsettings = array(
-						"${type}_format"  => '',
-						"${type}_counter" => '',
-						"${type}_global"  => get_option ($type.'_global',  'no'),
-						"${type}_padding" => 1,
-						"${type}_step"    => 1,
-						"${type}_start"   => 1,
-					);
-					$customvars = get_option ('ordernumber_variables',   array());
-
-					$number = $this->createNumber ($fmt, $type, $order, $customvars, $ctrsettings);
-					update_post_meta( $orderid, $this->ordernumber_meta.$type, $number );
-					return $number;
-				} else {
-					return $orderid;
-				}
-			}
-			
-			/** 
-			 * The hook to assign a customized order number (unless the order already has one assigned)
-			 */
-			function assign_new_ordernumber($orderid, $order, $update=true) {
-				if ((!$update) /*&& ($order->post_status == 'auto-draft')*/) {
-					// New order => assign placeholder, which will later be overwritten the real order number
-					update_post_meta( $orderid, $this->ordernumber_meta.'ordernumber', $this->ordernumber_new_placeholder );
-				}
-				// If we do not have an order (yet), we cannot proceed. But we probably have created the 
-				// ordernumber placeholder for that post, so this function has done its job and we can return
-				if (!$order instanceof WC_Order) {
-					return;
-				}
-				$number = get_post_meta( $orderid, $this->ordernumber_meta.'ordernumber', 'true');
-				if ($number == $this->ordernumber_new_placeholder && $order->post_status != 'auto-draft') {
-					$number = $this->assignNumber($orderid, $order, 'ordernumber');
-					// Assign a new number
-				}
-				return $number;
-			}
-
-			/** 
-			 * The generic function to retrieve a particular number
-			 */
-			function get_number($orderid, $order, $type = 'ordernumber') {
-				$stored_number = get_post_meta( $orderid, $this->ordernumber_meta.$type, 'true');
-				if ($stored_number == $this->ordernumber_new_placeholder) {
-					// Check whether the order was now really created => create order number now
-					return $this->assign_new_ordernumber($orderid, $order, $type);
-				} elseif (!empty($stored_number)) {
-					// Order number already exists => simply return it
-					return $stored_number;
-				} else {
-					// No order number was created for this order, so simply use the orderid as default.
-					return $orderid;
-				}
-			}
-
-		}
+		require_once( plugin_dir_path( __FILE__ ) . '/ordernumbers_woocommerce.php');
 	}
 
 	if (class_exists("OpenToolsOrdernumbers")) {
-		// Installation and uninstallation hooks
-		register_activation_hook(__FILE__, array('OpenToolsOrdernumbers', 'activate'));
-		register_deactivation_hook(__FILE__, array('OpenToolsOrdernumbers', 'deactivate'));
-
 		// instantiate the plugin class
 		$ordernumber_plugin = new OpenToolsOrdernumbers();
 	}