woocommerce-advanced-ordernumbers.php 15.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<?php
/**
 * Plugin Name: Advanced Ordernumbers
 * Plugin URI: http://open-tools.net/WooCommerce/Advanced-Ordernumbers.php
 * Description: Lets the user freely configure the order numbers in WooCommerce.
 * Version: 0.1.0
 * Author: Open Tools, Reinhold Kainhofer
 * Author URI: http://open-tools.net
 * Text Domain: woocommerce-advanced-ordernumbers
 * Domain Path: 
 * License: GPL2+
 WC requires at least: 2.2
WC tested up to: 2.3
*/

/**
17
 * The structure of this plugin originally followed the tutorial, although much of the plugin has been rewritten since then:
18
 * http://www.yaconiello.com/blog/how-to-write-wordpress-plugin/
19
 * A lot of coding ideas were also taken directly from the way things are implemented in WooCommerce itself.
20 21 22 23 24 25 26 27 28 29 30
 */
 
if ( ! defined( 'ABSPATH' ) ) { 
	exit; // Exit if accessed directly
}
/**
 * Check if WooCommerce is active
 **/
if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {
    // Put your plugin code here
    // Load the language files
31
	load_plugin_textdomain('opentools-ordernumbers', false, basename( dirname( __FILE__ ) ) . '/languages' );
32 33 34

	if (!class_exists("OpenToolsOrdernumbers")) {
		class OpenToolsOrdernumbers {
35
			public $ordernumber_meta = "_order_number";
36 37
			public $ordernumber_new_placeholder = "[New Order]";
			public $ordernumber_counter_prefix = 'ordernumber-counter-';
38
			
39 40 41 42 43
			/**
			 * Construct the plugin object
			 */
			public function __construct()
			{
44
				$plugin = plugin_basename(__FILE__); 
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114


				// Init settings
				$this->settings = array(
					array(
						'name' 		=> __( 'Advanced Order Numbers', 'woocommerce-advanced-ordernumbers' ),
						'desc'		=> __( 'Configure the format and the counters of the order numbers in WooCommerce.', 'woocommerce-advanced-ordernumbers' ),
						'type' 		=> 'title',
						'id' 		=> 'ordernumber_options'
					),

					array(
						'name' 		=> __( 'Customize Order Numbers', 'woocommerce-advanced-ordernumbers' ),
						'desc' 		=> __( 'Check to use custom order numbers rather than the default wordpress post ID.', 'woocommerce-advanced-ordernumbers' ),
						'id' 		=> 'customize_ordernumber',
						'type' 		=> 'checkbox',
						'default'	=> 'no'
					),
					array(
						'title'		=> __( 'Order number format', 'woocommerce-advanced-ordernumbers' ),
						'desc' 		=> __( '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.', 'woocommerce-advanced-ordernumbers' ),
						'desc_tip'	=> true,
						'id' 		=> 'ordernumber_format',
						'default'	=> '#',
						'type' 		=> 'text',
					),
					array(
						'title'		=> __( 'Global', 'woocommerce-advanced-ordernumbers' ),
						'desc' 		=> __( 'Counter Scope', 'woocommerce-advanced-ordernumbers' ),
						'id' 		=> 'ordernumber_global',
						'type' 		=> 'checkbox',
						'default'	=> 'no'
					),
					array(
						'title' 		=> __( 'Counter Digits', 'woocommerce-advanced-ordernumbers' ),
						'desc' 		=> __( 'Minimum number of digits for the number', 'woocommerce-advanced-ordernumbers' ),
						'desc_tip'	=> true,
						'id' 		=> 'ordernumber_padding',
						'type' 		=> 'number',
						'default'	=> '0'
					),
					array(
						'title' 	=> __( 'Counter Start', 'woocommerce-advanced-ordernumbers' ),
						'desc' 		=> __( 'Start value for each new counter', 'woocommerce-advanced-ordernumbers' ),
						'desc_tip'	=> true,
						'id' 		=> 'ordernumber_start',
						'type' 		=> 'number',
						'default'	=> '1'
					),
					array(
						'title'	=> __( 'Counter step', 'woocommerce-advanced-ordernumbers' ),
						'desc' 		=> __( 'By how much the counter will be increased after each order. Typically 1.', 'woocommerce-advanced-ordernumbers' ),
						'desc_tip'	=> true,
						'id' 		=> 'ordernumber_step',
						'type' 		=> 'number',
						'default'	=> '1'
					),
					array( 'type' => 'sectionend', 'id' => 'ordernumber_options' ),
					
					// TODO: customize order password, and other numbers!
				);
				// 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');

115
				// register actions
116
				add_filter( 'woocommerce_get_sections_checkout', array($this, 'add_admin_section'));
117 118 119
				// 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' ) );
120 121
				// Add the ordernumber post meta to the search in the backend
				add_filter( 'woocommerce_shop_order_search_fields', array($this, 'order_search_fields'));
122 123 124
				// 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 );

125 126 127 128
				// 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
129
				add_filter ('woocommerce_order_number', array(&$this, 'get_ordernumber'), 10, 2/*<= Also get the order object! */);
130 131
			}
    
132 133
			// Activate the plugin
			public static function activate() {}
134
    
135 136 137
			// Deactivate the plugin
			public static function deactivate() {}

138 139 140 141
			/**
			 * 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
			 */
142
			function add_admin_section($sections) {
143 144 145 146 147 148 149 150
				$newsections = array();
				foreach ($sections as $sec => $name ) {
					$newsections[$sec] = $name;
					if ($sec == '') {
						$newsections['ordernumber'] = __( 'Order Numbers', 'woocommerce-advanced-ordernumbers');
					}
				}
				return $newsections;
151 152
			}
			
153
			public function settings_output() {
154 155 156 157 158
				global $current_section;
				if ($current_section == 'ordernumber') {
					$settings = $this->settings;
					WC_Admin_Settings::output_fields( $settings );
				}
159 160
			}

161
			public function settings_save() {
162 163 164 165 166
				global $current_section;
				if ($current_section == 'ordernumber') {
					$settings = $this->settings;
					WC_Admin_Settings::save_fields( $settings );
				}
167
			}
168 169 170 171 172 173 174 175 176
			
			/** 
			 * 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;
			}
177 178 179 180 181 182 183 184 185 186 187
			
			/**
			 * 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);
			}
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
			
			/** 
			 * 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);
				}
				
			}
206

207
			/**
208
			 * Counter handling (simple loading/storing counters), storing them as options
209 210
			 */
			function _getCounter($type, $format, $start=1) {
211
				$count = get_option ($this->ordernumber_counter_prefix.$type.'-'.$format, $start);
212 213 214 215
				return $count;
			}
			// Insert new counter value into the db or update existing one
			function _setCounter($type, $format, $value) {
216
				return update_option($this->ordernumber_counter_prefix.$type.'-'.$format, $value);
217 218
			}
			
219
			
220
			/**
221 222 223 224 225 226 227 228 229 230 231 232 233 234
			 * Variable replacements:
			 *   - Random strings/digits
			 *   - Order properties
			 *   - User properties
			 */
			
			/* 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;
235 236
			}

237 238 239 240 241 242 243 244 245 246 247 248 249
			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;
250
				}
251 252 253
				return self::randomString ($alphabet, $len);
			}
    
254

255
			/* 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. */
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
			function replace_fields ($fmt, $type, $order) {
				// First, replace all randomXXX[n] fields. This needs to be done with a regexp and a callback:
				$fmt = preg_replace_callback ('/\[(random)(.*?)([0-9]*?)\]/', array($this, 'replaceRandom'), $fmt);
        
				$reps = array (
					"[year]" => date ("Y"),
					"[year2]" => date ("y"),
					"[month]" => date("m"),
					"[day]" => date("d"),
					"[hour]" => date("H"),
					"[hour12]" => date("h"),
					"[ampm]" => date("a"),
					"[minute]" => date("i"),
					"[second]" => date("s"),
					"[orderid]" => $order->id,
				);
272

273
				$reps["[userid]"]      = $order->get_user_id();
274 275 276 277 278 279
				$reps["[ipaddress]"]   = $order->customer_ip_address;
				$reps["[orderstatus]"] = $order->get_status();
				$reps["[email]"]       = $order->billing_email;
        
				
				$reps["[firstname]"] = $order->billing_first_name;
280
				$reps["[lastname]"]  = $order->billing_last_name;
281

282 283 284 285
				$reps["[company]"]   = $order->billing_company;
				$reps["[zip]"]       = $order->billing_postcode;
				$reps["[postcode]"]  = $order->billing_postcode;
				$reps["[city]"]      = $order->billing_city;
286 287 288 289 290
        
				$country = $order->billing_country;
				$state = $order->billing_state;
				$allcountries = WC()->countries->get_countries();
				$states = WC()->countries->get_states($country);
291
				$reps["[country]"]     = $country;
292 293
				$reps["[countryname]"] = ( isset( $allcountries[ $country ] ) ) ? $allcountries[ $country ] : $country;

294 295
				$reps["[state]"]       = $state;
				$reps["[statename]"]   = ( $country && $state && isset( $states[ $country ][ $state ] ) ) ? $states[ $country ][ $state ] : $state;
296
				
297 298 299 300
				$reps["[articles]"]    = $order->get_item_count();
				$reps["[currency]"]    = $order->get_order_currency();
// 				$reps["[downloadpermitted]"] = $order->is_download_permitted();
// 				$reps["[hasdownloads]"] = $order->has_downloadable_item();
301 302 303 304 305 306 307 308 309 310 311
// 				$reps["[coupons]"] = $order->get_used_coupons();
				
				if ($type != 'ordernumber') {
					$reps["[ordernumber]"] = $order->get_order_number();
				}
				$user = $order->get_user();
				if ($user) {
//					TODO: Shall we supply a variable for the user's login / display name?
// 					$reps["[user]"] = print_r($user,1);
// 					if (isset($order->username)) $reps["[username]"] = $order->username;
				}
312

313 314 315
				// Allow customization via plugins: filter function($reps, $order, $type, $fmt)
				$reps = apply_filters( 'opentools_ordernumber_replacements', $reps, $order, $type, $fmt);
				return str_ireplace (array_keys($reps), array_values($reps), $fmt);
316
			}
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339

			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!

					$nr = $this->replace_fields ($fmt, $type, $order);

					// 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);
340
					update_post_meta( $orderid, $this->ordernumber_meta, $number );
341 342 343 344 345
					return $number;
				} else {
					return $orderid;
				}
			}
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
			
			/** 
			 * 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;
			}

368 369 370 371
			/** 
			 * 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) {
372
				$stored_number = get_post_meta( $orderid, $this->ordernumber_meta, 'true');
373 374 375 376 377
				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
378
					return $stored_number;
379
				} else {
380
					// No order number was created for this order, so simply use the orderid as default.
381
					return $orderid;
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397
				}
			}

		}
	}

	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();
	}
 
}