├── .editorconfig ├── .gitignore ├── exclude-patterns.txt ├── i18n └── languages │ └── woocommerce-sequential-order-numbers.pot ├── package.json ├── readme.txt ├── sake.config.js ├── tool.tartufo ├── woocommerce-sequential-order-numbers.php └── wp-assets ├── banner-772x250.png └── icon-256x256.png /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # http://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = tab 15 | 16 | [{.jshintrc,*.json,*.toml,*.yml}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [{*.txt}] 21 | end_of_line = crlf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | *.zip 3 | .DS_Store 4 | 5 | # Grunt.js SASS cache files 6 | /.sass-cache 7 | 8 | # NPM packages used by Grunt.js 9 | /node_modules 10 | package-lock.json 11 | 12 | # NPM debug log 13 | npm-debug.log 14 | 15 | # build directory 16 | /build 17 | -------------------------------------------------------------------------------- /exclude-patterns.txt: -------------------------------------------------------------------------------- 1 | resources/js/app.js 2 | resources/vendor 3 | (.*/)?autoload.php 4 | sample.env 5 | # Library Folders 6 | node_modules 7 | (.*/)?node_modules/ 8 | vendor 9 | (.*/)?vendor/ 10 | # Lock Files are not always in the root 11 | .*composer.lock 12 | .*package.json 13 | .*package-lock.json 14 | .pnp.js 15 | .*Pipfile.lock 16 | .*yarn.lock 17 | # Ignore inline images 18 | .*\.css$ 19 | .*\.scss$ 20 | .*\.ico$ 21 | .*\.jpg$ 22 | .*\.png$ 23 | .*\.svg$ 24 | # Ignore uploaded logs 25 | .*\.log$ 26 | # Frontend Build Files 27 | build 28 | data 29 | public/js/app.js 30 | -------------------------------------------------------------------------------- /i18n/languages/woocommerce-sequential-order-numbers.pot: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 SkyVerge 2 | # This file is distributed under the GNU General Public License v3.0. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: Sequential Order Numbers for WooCommerce 1.11.1\n" 6 | "Report-Msgid-Bugs-To: https://woocommerce.com/my-account/marketplace-ticket-form/\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: LANGUAGE \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "POT-Creation-Date: 2025-05-07T11:38:00+00:00\n" 13 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 | "X-Generator: WP-CLI 2.11.0\n" 15 | "X-Domain: woocommerce-sequential-order-numbers\n" 16 | 17 | #. Plugin Name of the plugin 18 | #: woocommerce-sequential-order-numbers.php 19 | msgid "Sequential Order Numbers for WooCommerce" 20 | msgstr "" 21 | 22 | #. Plugin URI of the plugin 23 | #: woocommerce-sequential-order-numbers.php 24 | msgid "http://www.skyverge.com/blog/woocommerce-sequential-order-numbers/" 25 | msgstr "" 26 | 27 | #. Description of the plugin 28 | #: woocommerce-sequential-order-numbers.php 29 | msgid "Provides sequential order numbers for WooCommerce orders" 30 | msgstr "" 31 | 32 | #. Author of the plugin 33 | #: woocommerce-sequential-order-numbers.php 34 | msgid "SkyVerge" 35 | msgstr "" 36 | 37 | #. Author URI of the plugin 38 | #: woocommerce-sequential-order-numbers.php 39 | msgid "http://www.skyverge.com" 40 | msgstr "" 41 | 42 | #. translators: Placeholders: %s - plugin name 43 | #: woocommerce-sequential-order-numbers.php:141 44 | msgid "You cannot clone instances of %s." 45 | msgstr "" 46 | 47 | #. translators: Placeholders: %s - plugin name 48 | #: woocommerce-sequential-order-numbers.php:153 49 | msgid "You cannot unserialize instances of %s." 50 | msgstr "" 51 | 52 | #: woocommerce-sequential-order-numbers.php:537 53 | msgid "Allows filtering of orders by custom order number. Example: /wp-json/wc/v3/orders/?number=240222-45" 54 | msgstr "" 55 | 56 | #. translators: Placeholders: %1$s - plugin name; %2$s - WooCommerce version; %3$s, %5$s - tags; %4$s - tag 57 | #: woocommerce-sequential-order-numbers.php:779 58 | msgid "%1$s is inactive because it requires WooCommerce %2$s or newer. Please %3$supdate WooCommerce%4$s or run the %5$sWooCommerce database upgrade%4$s." 59 | msgstr "" 60 | 61 | #: woocommerce-sequential-order-numbers.php:831 62 | msgid "Error activating and installing Sequential Order Numbers for WooCommerce: %s" 63 | msgstr "" 64 | 65 | #: woocommerce-sequential-order-numbers.php:833 66 | msgid "« Go Back" 67 | msgstr "" 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "woocommerce-sequential-order-numbers", 3 | "author": "SkyVerge", 4 | "homepage": "http://skyverge.com", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/skyverge/woocommerce-sequential-order-numbers.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/skyverge/woocommerce-sequential-order-numbers/issues" 11 | }, 12 | "devDependencies": { 13 | "sake": "github:skyverge/sake" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Sequential Order Numbers for WooCommerce === 2 | Contributors: SkyVerge, maxrice, tamarazuk, chasewiseman, nekojira, beka.rice 3 | Tags: woocommerce, order number, sequential order number, woocommerce orders 4 | Requires at least: 5.6 5 | Tested up to: 6.8.1 6 | Requires PHP: 7.4 7 | Stable tag: 1.11.1 8 | License: GPLv3 or later 9 | License URI: http://www.gnu.org/licenses/gpl-3.0.html 10 | 11 | This plugin extends WooCommerce by setting sequential order numbers for new orders. 12 | 13 | == Description == 14 | 15 | This plugin extends WooCommerce by automatically setting sequential order numbers for new orders. If there are existing orders at the time of installation, the sequential order numbers will start with the highest current order number. 16 | 17 | **This plugin requires WooCommerce 3.9.4 or newer.** 18 | 19 | > No configuration needed! The plugin is so easy to use, there aren't even any settings. Activate it, and orders will automatically become sequential. 20 | 21 | If you have no orders in your store, your orders will begin counting from order number 1. If you have existing orders, the count will pick up from your highest order number. 22 | 23 | If you've placed test orders, you must trash **and** permanently delete them to begin ordering at "1" (trashed orders have to be counted in case they're restored, so they need to be gone completely). 24 | 25 | = Support Details = 26 | 27 | We do support our free plugins and extensions, but please understand that support for premium products takes priority. We typically check the forums every few days (usually with a maximum delay of one week). 28 | 29 | = Sequential Order Numbers Pro = 30 | 31 | If you like this plugin, but are looking for the ability to set the starting number, or to add a custom prefix/suffix to your order numbers (ie, you'd prefer something like WT101UK, WT102UK, etc) please consider our premium Sequential Order Numbers Pro for WooCommerce plugin, which is available in the [WooCommerce Store](http://woocommerce.com/products/sequential-order-numbers-pro/). 32 | 33 | = More Details = 34 | - See the [product page](http://www.skyverge.com/product/woocommerce-sequential-order-numbers/) for full details. 35 | - Check out the [Pro Version](http://woocommerce.com/products/sequential-order-numbers-pro/). 36 | - View more of SkyVerge's [free WooCommerce extensions](http://profiles.wordpress.org/skyverge/) 37 | - View all [SkyVerge WooCommerce extensions](http://www.skyverge.com/shop/) 38 | 39 | Interested in contributing? You can [find the project on GitHub](https://github.com/skyverge/woocommerce-sequential-order-numbers) and contributions are welcome :) 40 | 41 | == Installation == 42 | 43 | You can install the plugin in a few ways: 44 | 45 | 1. Upload the entire 'woocommerce-sequential-order-numbers' folder to the '/wp-content/plugins/' directory 46 | 2. Upload the zip file you download via Plugins > Add New 47 | 3. Go to Plugins > Add New and search for "Sequential Order Numbers for WooCommerce", and install the one from SkyVerge. 48 | 49 | Once you've installed the plugin, to get started please: 50 | 51 | 1. Activate the plugin through the "Plugins" menu in WordPress. 52 | 2. No configuration needed! Order numbers will continue sequentially from the current highest order number, or from 1 if no orders have been placed yet. 53 | 54 | == Frequently Asked Questions == 55 | 56 | = Where are the settings? = 57 | 58 | The plugin doesn't require any :) When you activate it, it gets to work right away! Orders will automatically become sequential, starting from the most recent order number. 59 | 60 | = Why doesn't my payment gateway use this number? = 61 | 62 | For full compatibility with extensions which alter the order number, such as Sequential Order Numbers, WooCommerce extensions should use `$order->get_order_number();` rather than `$order->id` when referencing the order number. 63 | 64 | If your extension is not displaying the correct order number, you can try contacting the developers of your payment gateway to see if it's possible to make this tiny change. Using the order number instead is both compatible with WooCommerce core and our plugin, as without the order number being changed, it will be equal to the order ID. 65 | 66 | = Can I start the order numbers at a particular number? = 67 | 68 | This free version does not have that functionality, but the premium [Sequential Order Numbers Pro for WooCommerce](http://www.woothemes.com/products/sequential-order-numbers-pro/) will allow you to choose any starting number that's higher than your most current order number. 69 | 70 | = Can I start the order numbers at "1"? = 71 | 72 | If you want to begin numbering at "1", you must trash, then permanently delete all orders in your store so that there are no order numbers already being counted. 73 | 74 | = Can I set an order number prefix/suffix? = 75 | 76 | This free version does not have that functionality, but it's included in the premium [Sequential Order Numbers Pro for WooCommerce](http://www.woothemes.com/products/sequential-order-numbers-pro/). 77 | 78 | == Other Notes == 79 | 80 | If you'd like to make your payment gateway compatible with Sequential Order Numbers, or other plugins that filter the order number, please make one small change. Instead of referencing `$order->id` when storing order data, reference: `$order->get_order_number()` 81 | 82 | This is compatible with WooCommerce core by default, as the order number is typically equal to the order ID. However, this will also let you be compatible with plugins such as ours, as the order number can be filtered (which is what we do to make it sequential), so using order number is preferred. 83 | 84 | Some other notes to help developers: 85 | 86 | = Get an order from order number = 87 | 88 | If you want to access the order based on the sequential order number, you can do so with a helper method: 89 | 90 | ` 91 | $order_id = wc_sequential_order_numbers()->find_order_by_order_number( $order_number ); 92 | ` 93 | 94 | This will give you the order's ID (post ID), and you can get the order object from this. 95 | 96 | = Get the order number = 97 | 98 | If you have access to the order ID or order object, you can easily get the sequential order number based on WooCommerce core functions. 99 | 100 | ` 101 | $order = wc_get_order( $order_id ); 102 | $order_number = $order->get_order_number(); 103 | ` 104 | 105 | == Changelog == 106 | 107 | = 2025.05.07 - version 1.11.1 = 108 | * Fix - Searching by order number not working when Full Text Search is enabled 109 | 110 | = 2024.11.06 - version 1.11.0 = 111 | * Misc - Code clean up and optimization 112 | * Misc - Add compatibility for WooCommerce Checkout block 113 | * Misc - Register REST API custom order number filter 114 | 115 | = 2023.09.05 - version 1.10.1 = 116 | * Fix - Call save order method only in HPOS installs to avoid setting the same order number meta twice in CPT installations 117 | 118 | = 2023.08.02 - version 1.10.0 = 119 | * Tweak - Also set sequential order numbers for orders sent via the WooCommerce Checkout Block 120 | * Misc - Add compatibility for WooCommerce High Performance Order Storage (HPOS) 121 | * Misc - Require PHP 7.4 and WordPress 5.6 122 | 123 | = 2022.07.30 - version 1.9.7 = 124 | * Misc - Rename to Sequential Order Numbers for WooCommerce 125 | 126 | = 2022.03.01 - version 1.9.6 = 127 | * Misc - Require WooCommerce 3.9.4 or newer 128 | * Misc - Replace calls to deprecated `is_ajax()` with `wp_doing_ajax()` 129 | 130 | = 2020.05.07 - version 1.9.5 = 131 | * Misc - Add support for WooCommerce 4.1 132 | 133 | = 2020.03.10 - version 1.9.4 = 134 | * Misc - Add support for WooCommerce 4.0 135 | 136 | = 2020.02.05 - version 1.9.3 = 137 | * Misc - Add support for WooCommerce 3.9 138 | 139 | = 2019.11.05 - version 1.9.2 = 140 | * Misc - Add support for WooCommerce 3.8 141 | 142 | = 2019.10.03 - version 1.9.1 = 143 | * Fix - Fix order number filter in WooCommerce Admin Downloads Analytics 144 | 145 | = 2019.08.15 - version 1.9.0 = 146 | * Misc - Add support for WooCommerce 3.7 147 | * Misc - Remove support for WooCommerce 2.6 148 | 149 | = 2018.07.17 - version 1.8.3 = 150 | * Misc - Require WooCommerce 2.6.14+ and WordPress 4.4+ 151 | 152 | = 1.8.2 - 2017.08.22 = 153 | * Fix - PHP deprecation warning when Subscriptions is used 154 | * Misc - Removed support for WooCommerce Subscriptions older than v2.0 155 | 156 | = 1.8.1 - 2017.03.28 = 157 | * Fix - Removes errors on refund number display 158 | 159 | = 1.8.0 - 2017.03.23 = 160 | * Fix - Admin orderby was not properly scoped to orders, props [@brandondove](https://github.com/brandondove) 161 | * Misc - Added support for WooCommerce 3.0 162 | * Misc - Removed support for WooCommerce 2.4 163 | 164 | = 1.7.0 - 2016.05.24 = 165 | * Misc - Added support for WooCommerce 2.6 166 | * Misc - Removed support for WooCommerce 2.3 167 | 168 | = 1.6.1 - 2016.02.04 = 169 | * Misc - WooCommerce Subscriptions: Use new hook wcs_renewal_order_meta_query instead of deprecated woocommerce_subscriptions_renewal_order_meta_query 170 | 171 | = 1.6.0 - 2016.01.20 = 172 | * Misc - WooCommerce Subscriptions: Use new filter hook wcs_renewal_order_created instead of deprecated woocommerce_subscriptions_renewal_order_created 173 | * Misc - WooCommerce 2.5 compatibility 174 | * Misc - Dropped WooCommerce 2.2 support 175 | 176 | = 1.5.1 - 2015.11.26 = 177 | * Fix - Compatibility fix with WooCommerce Subscriptions 2.0 178 | 179 | = 1.5.0 - 2015.07.28 = 180 | * Misc - WooCommerce 2.4 Compatibility 181 | 182 | = 1.4.0 - 2015.02.10 = 183 | * Fix - Improved install routine for shops with a large number of orders 184 | * Misc - WooCommerce 2.3 compatibility 185 | 186 | = 1.3.4 - 2014.09.23 = 187 | * Fix - Compatibility fix with WooCommerce 2.1 188 | * Fix - Fix a deprecated notice in WooCommerce 2.2 189 | 190 | = 1.3.3 - 2014.09.05 = 191 | * Localization - Included a .pot file for localization 192 | 193 | = 1.3.2 - 2014.09.02 = 194 | * Misc - WooCommerce 2.2 compatibility 195 | 196 | = 1.3.1 - 2014.01.22 = 197 | * Misc - WooCommerce 2.1 compatibility 198 | 199 | = 1.3 - 2013.04.26 = 200 | * Feature - Improved WooCommerce Subscriptions compatibility 201 | * Feature - Improved WooCommerce Pre-Orders compatibility 202 | * General code cleanup and refactor 203 | 204 | = 1.2.4 - 2012.12.14 = 205 | * Fix - WordPress 3.5 compatibility fix 206 | * Fix - Order numbers not assigned to temporary auto-draft orders created from the admin 207 | 208 | = 1.2.3 - 2012.06.06 = 209 | * Fix - Removed WooCommerce functions, which caused a compatibility issue with other WooCommerce plugins 210 | 211 | = 1.2.2 - 2012.05.25 = 212 | * Tweak - Takes advantage of new action hooks/filters available in WooCommerce 1.5.6 213 | * Fix - Bug fix on installation to stores with more than 10 existing orders 214 | 215 | = 1.2.1 - 2012.05.13 = 216 | * Tweak - Minor updates due to WooCommerce 1.5.5 release 217 | 218 | = 1.2.0 - 2012.04.21 = 219 | * Feature - Added support for the order tracking page 220 | 221 | = 1.1.2 - 2012.04.18 = 222 | * Tweak - Minor updates due to WooCommerce 1.5.4 release 223 | 224 | = 1.1.1 - 2012.04.02 = 225 | * Fix - Order number in the subject line of the admin new order email is fixed 226 | 227 | = 1.1.0 - 2012.04.02 = 228 | * Feature - Search by order number 229 | 230 | = 1.0.1 - 2012.04.02 = 231 | * Fix - small bug fix 232 | 233 | = 1.0.0 - 2012.04.02 = 234 | * Initial Release 235 | 236 | == Upgrade Notice == 237 | 238 | = 1.2.2 - 2012.05.25 = 239 | This version requires WooCommerce 1.5.6 240 | 241 | = 1.2.1 - 2012.05.13 = 242 | This version requires WooCommerce 1.5.5 243 | -------------------------------------------------------------------------------- /sake.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | deploy: 'wp', 3 | framework: false 4 | } 5 | -------------------------------------------------------------------------------- /tool.tartufo: -------------------------------------------------------------------------------- 1 | [tool.tartufo] 2 | exclude-paths="./exclude-patterns.txt" 3 | repo-path = "." 4 | default-regexes = true 5 | json = false 6 | regex = true 7 | entropy = true 8 | exclude-signatures = [] 9 | -------------------------------------------------------------------------------- /woocommerce-sequential-order-numbers.php: -------------------------------------------------------------------------------- 1 | is_hpos_enabled(); 115 | 116 | if ( ! $using_hpos ) { 117 | return 'edit-shop_order' === $current_screen->id; 118 | } 119 | 120 | if ( is_callable( OrderUtil::class . '::get_order_admin_screen' ) ) { 121 | $orders_screen_id = OrderUtil::get_order_admin_screen(); 122 | } else { 123 | $orders_screen_id = function_exists( 'wc_get_page_screen_id' ) ? wc_get_page_screen_id( 'shop-order' ) : null; 124 | } 125 | 126 | return $orders_screen_id === $current_screen->id 127 | && isset( $_GET['page'] ) 128 | && $_GET['page'] === 'wc-orders' 129 | && ( ! isset( $_GET['action'] ) || ! in_array( $_GET['action'], [ 'new', 'edit' ], true ) ); 130 | } 131 | 132 | 133 | /** 134 | * Cloning instances is forbidden due to singleton pattern. 135 | * 136 | * @since 1.7.0 137 | */ 138 | public function __clone() { 139 | 140 | /* translators: Placeholders: %s - plugin name */ 141 | _doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'You cannot clone instances of %s.', 'woocommerce-sequential-order-numbers' ), 'Sequential Order Numbers for WooCommerce' ), '1.7.0' ); 142 | } 143 | 144 | 145 | /** 146 | * Unserializing instances is forbidden due to singleton pattern. 147 | * 148 | * @since 1.7.0 149 | */ 150 | public function __wakeup() { 151 | 152 | /* translators: Placeholders: %s - plugin name */ 153 | _doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'You cannot unserialize instances of %s.', 'woocommerce-sequential-order-numbers' ), 'Sequential Order Numbers for WooCommerce' ), '1.7.0' ); 154 | } 155 | 156 | 157 | /** 158 | * Initialize the plugin. 159 | * 160 | * Prevents loading if any required conditions are not met, including minimum WooCommerce version. 161 | * 162 | * @internal 163 | * 164 | * @since 1.3.2 165 | */ 166 | public function initialize() : void { 167 | 168 | if ( ! $this->minimum_php_version_met() || ! $this->minimum_wc_version_met() ) { 169 | // halt functionality 170 | return; 171 | } 172 | 173 | // set the custom order number on the new order 174 | if ( ! $this->is_hpos_enabled() ) { 175 | add_action( 'wp_insert_post', [ $this, 'set_sequential_order_number' ], 10, 2 ); 176 | } else { 177 | add_action( 'woocommerce_checkout_update_order_meta', [ $this, 'set_sequential_order_number' ], 10, 2 ); 178 | add_action( 'woocommerce_process_shop_order_meta', [ $this, 'set_sequential_order_number' ], 35, 2 ); 179 | add_action( 'woocommerce_before_resend_order_emails', [ $this, 'set_sequential_order_number' ] ); 180 | } 181 | 182 | // set the custom order number on WooCommerce Checkout Block submissions 183 | add_action( 'woocommerce_store_api_checkout_update_order_meta', [ $this, 'set_sequential_order_number' ], 10, 2 ); 184 | 185 | // return our custom order number for display 186 | add_filter( 'woocommerce_order_number', [ $this, 'get_order_number' ], 10, 2 ); 187 | 188 | // order tracking page search by order number 189 | add_filter( 'woocommerce_shortcode_order_tracking_order_id', [ $this, 'find_order_by_order_number' ] ); 190 | 191 | // WC Subscriptions support 192 | add_filter( 'wc_subscriptions_renewal_order_data', [ $this, 'subscriptions_remove_renewal_order_meta' ] ); 193 | add_filter( 'wcs_renewal_order_created', [ $this, 'subscriptions_set_sequential_order_number' ] ); 194 | 195 | // WooCommerce Admin support 196 | if ( class_exists( 'Automattic\WooCommerce\Admin\Install', false ) || class_exists( 'WC_Admin_Install', false ) ) { 197 | add_filter( 'woocommerce_rest_orders_prepare_object_query', [ $this, 'wc_admin_order_number_api_param' ], 10, 2 ); 198 | } 199 | 200 | if ( is_admin() ) { 201 | 202 | if ( $this->is_hpos_enabled() ) { 203 | /** @see \Automattic\WooCommerce\Internal\Admin\Orders\ListTable::prepare_items() */ 204 | add_filter( 'woocommerce_shop_order_list_table_request', [ $this, 'woocommerce_custom_shop_order_orderby' ], 20 ); 205 | } else { 206 | add_filter( 'request', [ $this, 'woocommerce_custom_shop_order_orderby' ], 20 ); 207 | } 208 | 209 | // ensure that admin order table search by order number works 210 | add_filter( 'woocommerce_shop_order_search_fields', [ $this, 'custom_search_fields' ] ); 211 | add_filter( 'woocommerce_order_table_search_query_meta_keys', [ $this, 'custom_search_fields' ] ); 212 | 213 | // admin order table search when using full text 214 | add_filter('woocommerce_hpos_generate_where_for_search_filter', [$this, 'fullTextSearchFilterWhereClause'], 10, 4); 215 | 216 | // sort by underlying _order_number on the Pre-Orders table 217 | add_filter( 'wc_pre_orders_edit_pre_orders_request', [ $this, 'custom_orderby' ] ); 218 | add_filter( 'wc_pre_orders_search_fields', [ $this, 'custom_search_fields' ] ); 219 | 220 | } 221 | 222 | // Installation 223 | if ( is_admin() && ! wp_doing_ajax() ) { 224 | add_action( 'admin_init', [ $this, 'install' ] ); 225 | } 226 | 227 | // Register REST API custom order number filter 228 | add_filter( 'woocommerce_rest_orders_collection_params', [ $this, 'add_rest_custom_order_number_query_param' ] ); 229 | } 230 | 231 | 232 | /** 233 | * Loads translations. 234 | * 235 | * @internal 236 | * 237 | * @since 1.3.3 238 | */ 239 | public function load_translation() : void { 240 | 241 | // localization 242 | load_plugin_textdomain( 'woocommerce-sequential-order-numbers', false, dirname( plugin_basename( __FILE__ ) ) . '/i18n/languages' ); 243 | } 244 | 245 | 246 | /** 247 | * Search for an order having a given order number. 248 | * 249 | * @since 1.0.0 250 | * 251 | * @param string|int $order_number order number to search for 252 | * @return int $order_id for the order identified by $order_number, or 0 253 | */ 254 | public function find_order_by_order_number( $order_number ) : int { 255 | 256 | // search for the order by custom order number 257 | if ( $this->is_hpos_enabled() ) { 258 | $orders = wc_get_orders( [ 259 | 'return' => 'ids', 260 | 'limit' => 1, 261 | 'meta_query' => [ 262 | [ 263 | 'key' => '_order_number', 264 | 'value' => $order_number, 265 | 'comparison' => '=', 266 | ], 267 | ], 268 | ] ); 269 | } else { 270 | $orders = get_posts( [ 271 | 'numberposts' => 1, 272 | 'meta_key' => '_order_number', 273 | 'meta_value' => $order_number, 274 | 'post_type' => 'shop_order', 275 | 'post_status' => 'any', 276 | 'fields' => 'ids', 277 | ] ); 278 | } 279 | 280 | $order_id = $orders ? current( $orders ) : null; 281 | 282 | // order was found 283 | if ( $order_id !== null ) { 284 | return (int) $order_id; 285 | } 286 | 287 | // if we didn't find the order, then it may be that this plugin was disabled and an order was placed in the interim 288 | $order = wc_get_order( $order_number ); 289 | 290 | if ( ! $order ) { 291 | return 0; 292 | } 293 | 294 | // _order_number was set, so this is not an old order, it's a new one that just happened to have an order ID that matched the searched-for order_number 295 | if ( $order->get_meta( '_order_number', true, 'edit' ) ) { 296 | return 0; 297 | } 298 | 299 | return $order->get_id(); 300 | } 301 | 302 | 303 | /** 304 | * Set the `_order_number` field for the newly created order according to HPOS usage. 305 | * 306 | * @internal 307 | * 308 | * @since 1.0.0 309 | * 310 | * @param int|WC_Order $order_id order identifier or order object 311 | * @param WP_Post|WC_Order|array|null $object $object order or post object or post data (depending on HPOS and hook in use) 312 | */ 313 | public function set_sequential_order_number( $order_id = null, $object = null ) : void { 314 | 315 | global $wpdb; 316 | 317 | $using_hpos = $this->is_hpos_enabled(); 318 | 319 | if ( $object instanceof WP_Post ) { 320 | 321 | $is_order = 'shop_order' === $object->post_type; 322 | $order = $is_order ? wc_get_order( $object->ID ) : null; 323 | $order_id = $object->ID; 324 | $order_status = $object->post_status; 325 | 326 | } else if ( $order_id instanceof WC_Order ) { 327 | 328 | $is_order = true; 329 | $order = $order_id; 330 | $order_id = $order->get_id(); 331 | $order_status = $order->get_status(); 332 | 333 | } else { 334 | 335 | $order = $object instanceof WC_Order ? $object : wc_get_order( (int) $order_id ); 336 | $is_order = $order instanceof WC_Order && 'shop_order' === $order->get_type(); 337 | $order_id = ! $order_id && $order ? $order->get_id() : (int) $order_id; 338 | $order_status = $order ? $order->get_status() : ''; 339 | 340 | if ( $is_order && $order_status !== 'auto-draft' && isset( $_GET['action'] ) && $_GET['action'] === 'new' ) { 341 | $order_status = 'auto-draft'; 342 | } 343 | } 344 | 345 | // when creating an order from the admin don't create order numbers for auto-draft orders, 346 | // because these are not linked to from the admin and so difficult to delete when CPT tables are used 347 | if ( $is_order && ( $using_hpos || 'auto-draft' !== $order_status ) ) { 348 | 349 | if ( $using_hpos ) { 350 | $order_number = $order ? $order->get_meta( '_order_number' ) : ''; 351 | } else { 352 | $order_number = get_post_meta( $order_id, '_order_number', true ); 353 | } 354 | 355 | // if no order number has been assigned, create one 356 | if ( empty( $order_number ) ) { 357 | 358 | // attempt the query up to 3 times for a much higher success rate if it fails (to avoid deadlocks) 359 | $success = false; 360 | $order_meta_table = $using_hpos ? $wpdb->prefix . 'wc_orders_meta' : $wpdb->postmeta; 361 | $order_id_column = $using_hpos ? 'order_id' : 'post_id'; 362 | 363 | for ( $i = 0; $i < 3 && ! $success; $i++ ) { 364 | 365 | $success = $wpdb->query( $wpdb->prepare( " 366 | INSERT INTO {$order_meta_table} ({$order_id_column}, meta_key, meta_value) 367 | SELECT %d, '_order_number', IF( MAX( CAST( meta_value as UNSIGNED ) ) IS NULL, 1, MAX( CAST( meta_value as UNSIGNED ) ) + 1 ) 368 | FROM {$order_meta_table} 369 | WHERE meta_key='_order_number' 370 | ", (int) $order_id ) ); 371 | } 372 | 373 | // with HPOS we need to trigger a save to update the order number, 374 | // or it won't persist by using the direct query above alone 375 | if ( $using_hpos ) { 376 | $order->save(); 377 | } 378 | } 379 | } 380 | } 381 | 382 | 383 | /** 384 | * Filters to return our _order_number field rather than the order ID, for display. 385 | * 386 | * @since 1.0.0 387 | * 388 | * @param string|int $order_number the order id with a leading hash 389 | * @param WC_Order|WC_Subscription $order the order object 390 | * @return string custom order number 391 | */ 392 | public function get_order_number( $order_number, $order ) { 393 | 394 | // don't display an order number for subscription objects 395 | if ( class_exists( WC_Subscription::class ) && $order instanceof WC_Subscription ) { 396 | return $order_number; 397 | } 398 | 399 | if ( $sequential_order_number = (string) $order->get_meta( '_order_number', true, 'edit' ) ) { 400 | $order_number = $sequential_order_number; 401 | } 402 | 403 | return $order_number; 404 | } 405 | 406 | 407 | /** Admin filters ******************************************************/ 408 | 409 | 410 | /** 411 | * Admin order table orderby ID operates on our meta `_order_number`. 412 | * 413 | * @internal 414 | * 415 | * @since 1.3 416 | * 417 | * @param array|mixed $vars associative array of orderby parameters 418 | * @return array|mixed associative array of orderby parameters 419 | */ 420 | public function woocommerce_custom_shop_order_orderby( $vars ) { 421 | 422 | global $typenow; 423 | 424 | if ( ! is_array( $vars ) ) { 425 | return $vars; 426 | } 427 | 428 | if ( ! $this->is_hpos_enabled() ) { 429 | 430 | if ( 'shop_order' !== $typenow ) { 431 | return $vars; 432 | } 433 | 434 | } elseif ( ! $this->is_orders_screen() ) { 435 | 436 | return $vars; 437 | } 438 | 439 | return $this->custom_orderby( $vars ); 440 | } 441 | 442 | 443 | /** 444 | * Modifies the given $args argument to sort on our` _order_number` meta. 445 | * 446 | * @internal 447 | * 448 | * @since 1.3 449 | * 450 | * @param array $args associative array of orderby parameters 451 | * @return array associative array of orderby parameters 452 | */ 453 | public function custom_orderby( array $args ) : array { 454 | 455 | // sorting 456 | if ( isset( $args['orderby'] ) && 'ID' === $args['orderby'] ) { 457 | 458 | $args = array_merge( $args, [ 459 | 'meta_key' => '_order_number', // sort on numerical portion for better results 460 | 'orderby' => 'meta_value_num', 461 | ] ); 462 | } 463 | 464 | return $args; 465 | } 466 | 467 | 468 | /** 469 | * Add our custom `_order_number` to the set of search fields so that the admin search functionality is maintained. 470 | * 471 | * @internal 472 | * 473 | * @since 1.0.0 474 | * 475 | * @param string[] $search_fields array of order meta fields to search by 476 | * @return string[] of order meta fields to search by 477 | */ 478 | public function custom_search_fields( array $search_fields ) : array { 479 | 480 | return array_merge( $search_fields, [ '_order_number' ] ); 481 | } 482 | 483 | /** 484 | * When Full Text Search is enabled, {@see \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableSearchQuery::generate_where_for_meta_table()} 485 | * doesn't run, which means our order ID meta field doesn't get searched. This method is responsible for reproducing that 486 | * method specifically when FTS is enabled. 487 | * 488 | * @param string|mixed $whereClause 489 | * @param string|mixed $searchTerm 490 | * @param string|mixed $searchFilter 491 | * @param OrdersTableQuery|mixed $query 492 | * @return string|mixed 493 | */ 494 | public function fullTextSearchFilterWhereClause($whereClause, $searchTerm, $searchFilter, $query) 495 | { 496 | try { 497 | $ftsIsEnabled = get_option(CustomOrdersTableController::HPOS_FTS_INDEX_OPTION) === 'yes' && get_option(CustomOrdersTableController::HPOS_FTS_ORDER_ITEM_INDEX_CREATED_OPTION) === 'yes'; 498 | if (! $ftsIsEnabled) { 499 | return $whereClause; 500 | } 501 | 502 | if ($searchFilter !== 'order_id') { 503 | return $whereClause; 504 | } 505 | 506 | global $wpdb; 507 | $order_table = $query->get_table_name('orders'); 508 | $meta_table = $query->get_table_name('meta'); 509 | 510 | $meta_sub_query = $wpdb->prepare( 511 | "SELECT search_query_meta.order_id 512 | FROM $meta_table as search_query_meta 513 | WHERE search_query_meta.meta_key = '_order_number' 514 | AND search_query_meta.meta_value LIKE %s 515 | GROUP BY search_query_meta.order_id 516 | ", 517 | '%' . $wpdb->esc_like($searchTerm) . '%' 518 | ); 519 | 520 | return "`$order_table`.id IN ( $meta_sub_query ) "; 521 | } catch(Exception $e) { 522 | return $whereClause; 523 | } 524 | } 525 | 526 | 527 | /** 528 | * Register a custom query parameter for WooCommerce orders REST API to filter by custom order number. 529 | */ 530 | public function add_rest_custom_order_number_query_param( array $args ) : array { 531 | 532 | if ( array_key_exists( 'number', $args ) ) { 533 | return $args; 534 | } 535 | 536 | $args['number'] = [ 537 | 'description' => __( 'Allows filtering of orders by custom order number. Example: /wp-json/wc/v3/orders/?number=240222-45', 'woocommerce-sequential-order-numbers' ), 538 | 'sanitize_callback' => 'rest_sanitize_request_arg', 539 | 'type' => 'string', 540 | 'validate_callback' => 'rest_validate_request_arg', 541 | ]; 542 | 543 | return $args; 544 | } 545 | 546 | /** 3rd Party Plugin Support ******************************************************/ 547 | 548 | 549 | /** 550 | * Sets an order number on a subscriptions-created order. 551 | * 552 | * @since 1.3 553 | * 554 | * @internal 555 | * 556 | * @param WC_Order|mixed $renewal_order the new renewal order object 557 | * @return WC_Order|mixed renewal order instance 558 | */ 559 | public function subscriptions_set_sequential_order_number( $renewal_order ) { 560 | 561 | if ( $renewal_order instanceof WC_Order ) { 562 | 563 | $order = wc_get_order( $renewal_order->get_id() ); 564 | 565 | if ( $order ) { 566 | $this->set_sequential_order_number( $order->get_id(), $order ); 567 | } 568 | } 569 | 570 | return $renewal_order; 571 | } 572 | 573 | 574 | /** 575 | * Don't copy over order number meta when creating a parent or child renewal order 576 | * 577 | * Prevents unnecessary order meta from polluting parent renewal orders, and set order number for subscription orders. 578 | * 579 | * @since 1.3 580 | * 581 | * @internal 582 | * 583 | * @param string[]|mixed $order_data 584 | * @return string[]|mixed 585 | */ 586 | public function subscriptions_remove_renewal_order_meta( $order_data ) { 587 | 588 | if ( ! is_array( $order_data ) ) { 589 | return $order_data; 590 | } 591 | 592 | unset( $order_data['_order_number'] ); 593 | 594 | return $order_data; 595 | } 596 | 597 | 598 | /** 599 | * Hook WooCommerce Admin order number search to the meta value. 600 | * 601 | * @since 1.3 602 | * 603 | * @internal 604 | * 605 | * @param array|mixed $args Arguments to be passed to WC_Order_Query. 606 | * @param WP_REST_Request|mixed $request REST API request being made. 607 | * @return array|mixed Arguments to be passed to WC_Order_Query. 608 | */ 609 | public function wc_admin_order_number_api_param( $args, $request ) { 610 | 611 | if ( ! is_array( $args ) || ! $request instanceof WP_REST_Request ) { 612 | return $args; 613 | } 614 | 615 | global $wpdb; 616 | 617 | if ( '/wc/v4/orders' === $request->get_route() && isset( $request['number'] ) ) { 618 | 619 | // Handles 'number' value here and modify $args. 620 | $number_search = trim( $request['number'] ); 621 | $order_sql = esc_sql( $args['order'] ); // Order defaults to DESC. 622 | $limit = (int) $args['posts_per_page']; // Posts per page defaults to 10. 623 | 624 | $using_hpos = $this->is_hpos_enabled(); 625 | $order_meta_table = $using_hpos ? $wpdb->prefix . 'wc_orders_meta' : $wpdb->postmeta; 626 | $order_id_column = $using_hpos ? 'order_id' : 'post_id'; 627 | 628 | // Search Order number meta value instead of Post ID. 629 | $order_ids = $wpdb->get_col( 630 | $wpdb->prepare( " 631 | SELECT {$order_id_column} 632 | FROM {$order_meta_table} 633 | WHERE meta_key = '_order_number' 634 | AND meta_value LIKE %s 635 | ORDER BY {$order_id_column} {$order_sql} 636 | LIMIT %d 637 | ", $wpdb->esc_like( $number_search ) . '%', $limit ) 638 | ); 639 | 640 | if ( $using_hpos ) { 641 | $args['order__in'] = empty( $order_ids ) ? [ 0 ] : $order_ids; 642 | } else { 643 | $args['post__in'] = empty( $order_ids ) ? [ 0 ] : $order_ids; 644 | } 645 | 646 | // Remove the 'number' parameter to short circuit WooCommerce Admin's handling. 647 | unset( $request['number'] ); 648 | } 649 | 650 | return $args; 651 | } 652 | 653 | /** Helper Methods ******************************************************/ 654 | 655 | 656 | /** 657 | * Main Sequential Order Numbers Instance, ensures only one instance is/can be loaded. 658 | * 659 | * @since 1.7.0 660 | * 661 | * @return WC_Seq_Order_Number 662 | * @see wc_sequential_order_numbers() 663 | * 664 | */ 665 | public static function instance() : WC_Seq_Order_Number { 666 | 667 | return self::$instance ??= new self(); 668 | } 669 | 670 | 671 | /** 672 | * Helper function to determine whether a plugin is active. 673 | * 674 | * @since 1.8.3 675 | * 676 | * @param string $plugin_name plugin name, as the plugin-filename.php 677 | * @return boolean true if the named plugin is installed and active 678 | */ 679 | public static function is_plugin_active( string $plugin_name ) : bool { 680 | 681 | $active_plugins = (array) get_option( 'active_plugins', [] ); 682 | 683 | if ( is_multisite() ) { 684 | $active_plugins = array_merge( $active_plugins, array_keys( get_site_option( 'active_sitewide_plugins', [] ) ) ); 685 | } 686 | 687 | $plugin_filenames = []; 688 | 689 | foreach ( $active_plugins as $plugin ) { 690 | 691 | if ( false !== strpos( $plugin, '/' ) ) { 692 | 693 | // normal plugin name (plugin-dir/plugin-filename.php) 694 | [ , $filename ] = explode( '/', $plugin ); 695 | 696 | } else { 697 | 698 | // no directory, just plugin file 699 | $filename = $plugin; 700 | } 701 | 702 | $plugin_filenames[] = $filename; 703 | } 704 | 705 | return in_array( $plugin_name, $plugin_filenames, true ); 706 | } 707 | 708 | 709 | /** Compatibility Methods ******************************************************/ 710 | 711 | 712 | /** 713 | * Helper method to get the version of the currently installed WooCommerce. 714 | * 715 | * @since 1.3.2 716 | * 717 | * @return string woocommerce version number or null 718 | */ 719 | private static function get_wc_version() : ?string { 720 | 721 | return defined( 'WC_VERSION' ) && WC_VERSION ? WC_VERSION : null; 722 | } 723 | 724 | 725 | /** 726 | * Performs a minimum WooCommerce version check. 727 | * 728 | * @since 1.3.2 729 | * 730 | * @return bool 731 | */ 732 | private function minimum_wc_version_met() : bool { 733 | 734 | $version_met = true; 735 | 736 | if ( ! $wc_version = self::get_wc_version() ) { 737 | return false; 738 | } 739 | 740 | // if a plugin defines a minimum WC version, render a notice and skip loading the plugin 741 | if ( version_compare( $wc_version, self::MINIMUM_WC_VERSION, '<' ) ) { 742 | 743 | if ( is_admin() && ! wp_doing_ajax() && ! has_action( 'admin_notices', [ $this, 'render_update_notices' ] ) ) { 744 | 745 | add_action( 'admin_notices', [ $this, 'render_update_notices' ] ); 746 | } 747 | 748 | $version_met = false; 749 | } 750 | 751 | return $version_met; 752 | } 753 | 754 | 755 | /** 756 | * Performs a minimum PHP version check. 757 | * 758 | * @since 1.11.0 759 | * 760 | * @return bool 761 | */ 762 | private function minimum_php_version_met() : bool { 763 | 764 | return PHP_VERSION_ID >= 70400; 765 | } 766 | 767 | 768 | /** 769 | * Renders a notice to update WooCommerce if needed 770 | * 771 | * @internal 772 | * 773 | * @since 1.3.2 774 | */ 775 | public function render_update_notices() : void { 776 | 777 | $message = sprintf( 778 | /* translators: Placeholders: %1$s - plugin name; %2$s - WooCommerce version; %3$s, %5$s - tags; %4$s - tag */ 779 | esc_html__( '%1$s is inactive because it requires WooCommerce %2$s or newer. Please %3$supdate WooCommerce%4$s or run the %5$sWooCommerce database upgrade%4$s.', 'woocommerce-sequential-order-numbers' ), 780 | 'Sequential Order Numbers', 781 | self::MINIMUM_WC_VERSION, 782 | '', 783 | '', 784 | '' 785 | ); 786 | 787 | printf( '

%s

', $message ); 788 | } 789 | 790 | 791 | /** Lifecycle methods ******************************************************/ 792 | 793 | 794 | /** 795 | * Run every time 796 | * 797 | * Used since the activation hook is not executed when updating a plugin 798 | * 799 | * @internal 800 | * 801 | * @since 1.0.0 802 | */ 803 | public function install() : void { 804 | 805 | $installed_version = get_option( self::VERSION_OPTION_NAME ); 806 | 807 | if ( ! $installed_version ) { 808 | 809 | // initial install, set the order number for all existing orders to the order id: 810 | // page through the "publish" orders in blocks to avoid out of memory errors 811 | $offset = (int) get_option( 'wc_sequential_order_numbers_install_offset', 0 ); 812 | $orders_par_page = 500; 813 | 814 | do { 815 | 816 | // initial install, set the order number for all existing orders to the order id 817 | $orders = wc_get_orders( [ 818 | 'type' => 'shop_order', 819 | 'offset' => $offset, 820 | 'limit' => $orders_par_page, 821 | ] ); 822 | 823 | // some sort of bad database error: deactivate the plugin and display an error 824 | if ( is_wp_error( $orders ) ) { 825 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; 826 | deactivate_plugins( 'woocommerce-sequential-order-numbers/woocommerce-sequential-order-numbers.php' ); // hardcode the plugin path so that we can use symlinks in development 827 | 828 | wp_die( 829 | sprintf( 830 | /** translators: Placeholder: %s - error message(s) */ 831 | __( 'Error activating and installing Sequential Order Numbers for WooCommerce: %s', 'woocommerce-sequential-order-numbers' ), 832 | '
  • ' . implode( '
  • ', $orders->get_error_messages() ) . '
' 833 | ) . '
' . __( '« Go Back', 'woocommerce-sequential-order-numbers' ) . '' 834 | ); 835 | 836 | } elseif ( is_array( $orders ) ) { 837 | 838 | foreach ( $orders as $order ) { 839 | 840 | if ( '' === $order->get_meta( '_order_number' ) ) { 841 | $order->add_meta_data( '_order_number', (string) $order->get_id() ); 842 | $order->save_meta_data(); 843 | } 844 | } 845 | } 846 | 847 | // increment offset 848 | $offset += $orders_par_page; 849 | // and keep track of how far we made it in case we hit a script timeout 850 | update_option( 'wc_sequential_order_numbers_install_offset', $offset ); 851 | 852 | } while ( count( $orders ) === $orders_par_page ); // while full set of results returned (meaning there may be more results still to retrieve) 853 | } 854 | 855 | if ( $installed_version !== self::VERSION ) { 856 | 857 | $this->upgrade( $installed_version ); 858 | 859 | // new version number 860 | update_option( self::VERSION_OPTION_NAME, self::VERSION ); 861 | } 862 | } 863 | 864 | 865 | /** 866 | * Runs when plugin version number changes. 867 | * 868 | * 1.0.0 869 | * 870 | * @param string $installed_version 871 | */ 872 | private function upgrade( string $installed_version ) : void { 873 | // upgrade code goes here 874 | } 875 | 876 | 877 | } 878 | 879 | 880 | /** 881 | * Returns the One True Instance of Sequential Order Numbers 882 | * 883 | * @since 1.7.0 884 | * 885 | * @return WC_Seq_Order_Number 886 | */ 887 | function wc_sequential_order_numbers() : WC_Seq_Order_Number { 888 | 889 | return WC_Seq_Order_Number::instance(); 890 | } 891 | 892 | 893 | // fire it up! 894 | wc_sequential_order_numbers(); 895 | -------------------------------------------------------------------------------- /wp-assets/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godaddy-wordpress/woocommerce-sequential-order-numbers/08f01ea18bc9574966ad7312734269a41fe613b1/wp-assets/banner-772x250.png -------------------------------------------------------------------------------- /wp-assets/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godaddy-wordpress/woocommerce-sequential-order-numbers/08f01ea18bc9574966ad7312734269a41fe613b1/wp-assets/icon-256x256.png --------------------------------------------------------------------------------