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.'), 'desc_tip' => true, 'id' => 'ordernumber_format', 'default' => '#', 'type' => 'text', ), 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( 'title' => self::__( 'Counter Digits'), 'desc' => self::__( 'Minimum number of digits for the number'), 'desc_tip' => true, 'id' => 'ordernumber_padding', 'type' => 'number', 'default' => '0' ), array( 'title' => self::__( 'Counter Start'), 'desc' => self::__( 'Start value for each new counter'), 'desc_tip' => true, 'id' => 'ordernumber_start', 'type' => 'number', 'default' => '1' ), array( 'title' => self::__( 'Counter step'), 'desc' => self::__( 'By how much the counter will be increased after each order. Typically 1.'), 'desc_tip' => true, 'id' => 'ordernumber_step', 'type' => 'number', 'default' => '1' ), 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', '0'); add_option ('ordernumber_format', "#"); add_option ('ordernumber_global', '0'); add_option ('ordernumber_padding', '1'); add_option ('ordernumber_start', '1'); add_option ('ordernumber_step', '1'); 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( '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_create_ordernumber'), 10, 3); add_action( 'save_post', array(&$this, 'check_create_ordernumber'), 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'); } /** * Render the Custom Variables configuration table */ public function admin_field_variables($settings) { $variables = get_option( $settings['id'], array() ); if (!is_array($variables)) { $variables = array(); } ?> customvar_admin_table($settings['id'], $variables); ?> create_replacements_row_html($id, array(), 'disabled'); ?> 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' => '', ); ?> $column ) { echo ''; } ?> $column) { echo ''; } ?> create_replacements_row_html($id, $var); } ?>
' . esc_html( $column ) . '
>
' class='ordernumber-counter-addbtn' />
'=', 'contains' => self::__('contains'), 'smaller' => '<', 'smallerequal' => '<=', 'larger' => '>', 'largerequal' => '>=', 'startswith' => self::__('starts with'), 'endswith' => self::__('ends with'), ); $html = ' =>   '; 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, ); } } ?> ' class='wc-ordernumber-loading' style="display: none; position: absolute; top: 2px; left: 0px; z-index: 9999;"/> self::__( ''), 'name' => self::__( 'Counter name'), 'value' => self::__( 'Counter value'), 'settings' => '' ) ); ?> $column ) { echo ''; } ?> $column) { echo ''; } ?> create_admin_counter_row($counter['type'], $counter['name'], $counter['value']); } ?>
' . esc_html( $column ) . '
' class='ordernumber-counter-addbtn' />
" . self::__($type, 'wooocommerce-advanced-ordernumbers' ) . " " . (empty($format)?("".self::__("[Global]").""):esc_html($format)) . " " . esc_html($value) . "
"; 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); } /** * 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_create_ordernumber($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; $reps["[ipaddress]"] = $order->customer_ip_address; if ($nrtype != 'ordernumber') { $reps["[ordernumber]"] = $order->get_order_number(); } $reps["[orderstatus]"] = $order->get_status(); $reps["[currency]"] = $order->get_order_currency(); $reps["[userid]"] = $order->get_user_id(); $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(); } protected function setupShippingReplacements(&$reps, $order, $nrtype) { // $reps["[shippingmethod]"] = $order->getShippingMethod(); } /*protected function setupInvoiceReplacements (&$reps, $invoice, $order, $nrtype) { $reps["[invoiceid]"] = $invoice->getId(); }*/ protected function setupReplacements($nrtype, $order) { $reps = array(); $this->setupDateTimeReplacements($reps, $order, $nrtype); $this->setupStoreReplacements($reps, $order, $nrtype); $this->setupOrderReplacements($reps, $order, $nrtype); $this->setupShippingReplacements($reps, $order, $nrtype); $reps = apply_filters( 'opentools_ordernumber_replacements', $reps, $order, $nrtype); return $reps; } protected function applyCustomVariables ($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 '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; } 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); return str_ireplace (array_keys($reps), array_values($reps), $fmt); } // Allow the user to override the format like any other custom variable: protected function determineNumberFormatString($fmt, $type, $order, $reps) { if (isset($reps['['.$type.'_format]'])) { return $reps['['.$type.'_format]']; } else { return $fmt; } } /* 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 replace_fields ($fmt, $type, $order, $customvars) { $reps = $this->setupReplacements ($type, $order); $reps = $this->applyCustomVariables ($type, $order, $reps, $customvars); $format = $this->determineNumberFormatString($fmt, $type, $order, $reps); return $this->doReplacements($format, $reps); } function create_ordernumber($orderid, $order, $type='ordernumber') { if (get_option('customize_'.$type, 'false')) { $fmt = get_option ($type.'_format', "#"); $global = get_option ($type.'_global', 1); $padding = get_option ($type.'_padding', 1); $step = get_option ($type.'_step', 1); $start = get_option ($type.'_start', 1)-$step; // The counter contains the PREVIOUS number! $customvars = get_option ('ordernumber_variables', array()); $nr = $this->replace_fields ($fmt, $type, $order, $customvars); // Split at a | to get the number format and a possibly different counter increment format // If a separate counter format is given after the |, use it, otherwise reuse the number format itself as counter format $parts = explode ("|", $nr); $format = $parts[0]; $counterfmt = ($global==1)?"":$parts[(count($parts)>1)?1:0]; // Look up the current counter $count = $this->_getCounter($type, $counterfmt, $start) + $step; $this->_setCounter($type, $counterfmt, $count); // return the format with the counter inserted $number = str_replace ("#", sprintf('%0' . $padding . 's', $count), $format); update_post_meta( $orderid, $this->ordernumber_meta, $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, $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, 'true'); if ($number == $this->ordernumber_new_placeholder && $order->post_status != 'auto-draft') { $number = $this->create_ordernumber($orderid, $order, 'ordernumber'); // Assign a new number } return $number; } /** * The hook to customize order numbers (requests the order number from the database; creates a new ordernumber if no entry exists in the database) */ function get_ordernumber($orderid, $order) { $stored_number = get_post_meta( $orderid, $this->ordernumber_meta, '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); } 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; } } } } 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(); } }