From ca6a3a1a6d3ae3ca98a894c51faf801861be136c Mon Sep 17 00:00:00 2001 From: Woo Date: Thu, 25 Apr 2024 10:09:25 +0000 Subject: [PATCH] Updates to 3.1.9 --- assets/css/admin.css | 1 + assets/js/frontend-checkout.js | 33 + assets/js/frontend-checkout.min.js | 1 + assets/js/table-rate-rows.js | 199 +++ assets/js/table-rate-rows.min.js | 1 + changelog.txt | 397 +++++ includes/class-helpers.php | 260 +++ .../class-wc-shipping-table-rate-privacy.php | 32 + includes/class-wc-shipping-table-rate.php | 1396 +++++++++++++++++ includes/class-wc-table-rate-shipping.php | 278 ++++ includes/functions-admin.php | 297 ++++ includes/functions-ajax.php | 40 + installer.php | 47 + languages/woocommerce-table-rate-shipping.pot | 449 ++++++ uninstall.php | 15 + woocommerce-table-rate-shipping.php | 62 + 16 files changed, 3508 insertions(+) create mode 100644 assets/css/admin.css create mode 100644 assets/js/frontend-checkout.js create mode 100644 assets/js/frontend-checkout.min.js create mode 100644 assets/js/table-rate-rows.js create mode 100644 assets/js/table-rate-rows.min.js create mode 100644 changelog.txt create mode 100644 includes/class-helpers.php create mode 100644 includes/class-wc-shipping-table-rate-privacy.php create mode 100644 includes/class-wc-shipping-table-rate.php create mode 100644 includes/class-wc-table-rate-shipping.php create mode 100644 includes/functions-admin.php create mode 100644 includes/functions-ajax.php create mode 100644 installer.php create mode 100644 languages/woocommerce-table-rate-shipping.pot create mode 100644 uninstall.php create mode 100644 woocommerce-table-rate-shipping.php diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 0000000..d8e7e02 --- /dev/null +++ b/assets/css/admin.css @@ -0,0 +1 @@ +#shipping_class_priorities table input,#shipping_class_priorities table select,table#shipping_rates input,table#shipping_rates select{width:100%}#shipping_class_priorities table input[type=checkbox],table#shipping_rates input[type=checkbox]{width:auto}#shipping_class_priorities table td,table#shipping_rates td{cursor:move}#shipping_class_priorities table a.dupe,#shipping_class_priorities table a.remove,table#shipping_rates a.dupe,table#shipping_rates a.remove{float:right;margin-left:4px}#shipping_class_priorities table td,#shipping_class_priorities table th,table#shipping_rates td,table#shipping_rates th{padding:15px 10px;vertical-align:middle}#shipping_class_priorities table tr:nth-child(even) td,#shipping_class_priorities table tr:nth-child(even) th,table#shipping_rates tr:nth-child(even) td,table#shipping_rates tr:nth-child(even) th{background:#fafafa}#shipping_class_priorities table td.checkbox,#shipping_class_priorities table th.checkbox,table#shipping_rates td.checkbox,table#shipping_rates th.checkbox{text-align:center}#shipping_class_priorities table td .disabled,table#shipping_rates td .disabled{opacity:.5}#shipping_class_priorities table .check-column,table#shipping_rates .check-column{width:24px;min-width:24px!important;text-align:center;padding:15px 10px}#shipping_class_priorities table .check-column input,table#shipping_rates .check-column input{width:auto;vertical-align:middle;margin:0 0 0 8px}table#shipping_rates td.minmax input.error{background:#e9e6ed}table#shipping_rates td.minmax .tips{width:20px;height:20px;background:#ffba00;color:#fff;border-radius:100%;line-height:1.3;font-weight:700;text-align:center;display:inline-block;margin-top:4px} \ No newline at end of file diff --git a/assets/js/frontend-checkout.js b/assets/js/frontend-checkout.js new file mode 100644 index 0000000..ebbe1a1 --- /dev/null +++ b/assets/js/frontend-checkout.js @@ -0,0 +1,33 @@ +function wc_trs_display_abort_text( datas ) { + var wc_info = jQuery( '.woocommerce-info' ); + var wc_chk_info = jQuery( 'form.woocommerce-checkout .woocommerce-info' ); + var has_trs = false; + + wc_chk_info.each( function ( idx, elem ) { + var current_elem = jQuery( elem ); + + // Remove the element if abort text has been found previously. + if ( 'yes' === current_elem.data('wc_trs') ) { + if ( true === has_trs ) { + current_elem.remove(); + } + + has_trs = true; + } + } ); + + if ( false === has_trs ) { + return; + } + + wc_info.each( function ( idx, elem ) { + var current_elem = jQuery( elem ); + if ( 'yes' === current_elem.data('wc_trs') && 1 > current_elem.parents( 'form.woocommerce-checkout' ).length ) { + current_elem.remove(); + } + } ); +} + +jQuery( document.body ).on( 'updated_checkout checkout_error', function( datas ) { + wc_trs_display_abort_text(); +} ); diff --git a/assets/js/frontend-checkout.min.js b/assets/js/frontend-checkout.min.js new file mode 100644 index 0000000..d01bf0c --- /dev/null +++ b/assets/js/frontend-checkout.min.js @@ -0,0 +1 @@ +function wc_trs_display_abort_text(e){var o=jQuery(".woocommerce-info"),c=jQuery("form.woocommerce-checkout .woocommerce-info"),r=!1;c.each(function(e,o){var c=jQuery(o);"yes"===c.data("wc_trs")&&(!0===r&&c.remove(),r=!0)}),!1!==r&&o.each(function(e,o){var c=jQuery(o);"yes"===c.data("wc_trs")&&c.parents("form.woocommerce-checkout").length<1&&c.remove()})}jQuery(document.body).on("updated_checkout checkout_error",function(e){wc_trs_display_abort_text()}); diff --git a/assets/js/table-rate-rows.js b/assets/js/table-rate-rows.js new file mode 100644 index 0000000..8b0ec02 --- /dev/null +++ b/assets/js/table-rate-rows.js @@ -0,0 +1,199 @@ +/*global $, woocommerce_shipping_table_rate_rows, ajaxurl */ +( function( $, data, wp, ajaxurl ) { + + var wc_table_rate_rows_row_template = wp.template( 'table-rate-shipping-row-template' ), + $settings_form = $( '#mainform' ), + $rates_table = $( '#shipping_rates' ), + $rates = $rates_table.find( 'tbody.table_rates' ); + + var wc_table_rate_rows = { + init: function() { + $settings_form + .on( 'change', '#woocommerce_table_rate_calculation_type', this.onCalculationTypeChange ); + $rates_table + .on( 'change', 'select[name^="shipping_condition"]', this.onShippingConditionChange ) + .on( 'change', 'input[name^="shipping_abort["]', this.onShippingAbortChange ) + .on( 'click', 'a.add-rate', this.onAddRate ) + .on( 'click', 'a.remove', this.onRemoveRate ) + .on( 'click', 'a.dupe', this.onDupeRate ); + + var rates_data = $rates.data( 'rates' ); + + $( rates_data ).each( function( i ) { + var size = $rates.find( '.table_rate' ).length; + $rates.append( wc_table_rate_rows_row_template( { + rate: rates_data[ i ], + index: size + } ) ); + } ); + + $( 'label[for="woocommerce_table_rate_handling_fee"], label[for="woocommerce_table_rate_max_cost"], label[for="woocommerce_table_rate_min_cost"]', $settings_form ).each( function( i, el ) { + $(el).data( 'o_label', $(el).text() ); + }); + + $( '#woocommerce_table_rate_calculation_type, select[name^="shipping_condition"], input[name^="shipping_abort["]', $settings_form ).change(); + + $rates.sortable( { + items: 'tr', + cursor: 'move', + axis: 'y', + handle: 'td', + scrollSensitivity: 40, + helper: function(e,ui){ + ui.children().each( function() { + $( this ).width( $(this).width() ); + }); + ui.css( 'left', '0' ); + return ui; + }, + start: function( event, ui ) { + ui.item.css('background-color','#f6f6f6'); + }, + stop: function( event, ui ) { + ui.item.removeAttr( 'style' ); + wc_table_rate_rows.reindexRows(); + } + } ); + }, + onCalculationTypeChange: function() { + var selected = $( this ).val(); + + if ( selected == 'item' ) { + $( 'td.cost_per_item input' ).attr( 'disabled', 'disabled' ).addClass('disabled'); + } else { + $( 'td.cost_per_item input' ).removeAttr( 'disabled' ).removeClass('disabled'); + } + + if ( selected ) { + $( '#shipping_class_priorities' ).hide(); + $( 'td.shipping_label, th.shipping_label' ).hide(); + } else { + $( '#shipping_class_priorities' ).show(); + $( 'td.shipping_label, th.shipping_label' ).show(); + } + + if ( ! selected ) { + $( '#shipping_class_priorities span.description.per_order' ).show(); + $( '#shipping_class_priorities span.description.per_class' ).hide(); + } + + var label_text = data.i18n.order; + + if ( selected == 'item' ) { + label_text = data.i18n.item; + } else if ( selected == 'line' ) { + label_text = data.i18n.line_item; + } else if ( selected == 'class' ) { + label_text = data.i18n.class; + } + + $('label[for="woocommerce_table_rate_handling_fee"], label[for="woocommerce_table_rate_max_cost"], label[for="woocommerce_table_rate_min_cost"]').each(function( i, el ) { + var text = $(el).data( 'o_label' ); + text = text.replace( '[item]', label_text ); + $(el).text( text ); + }); + }, + onShippingConditionChange: function() { + var selected = $( this ).val(); + var $row = $( this ).closest('tr'); + + if ( selected == '' ) { + $row.find('input[name^="shipping_min"], input[name^="shipping_max"]').val( '' ).prop( 'disabled', true ).addClass( 'disabled' ); + } else { + $row.find('input[name^="shipping_min"], input[name^="shipping_max"]').prop( 'disabled', false ).removeClass( 'disabled' ); + } + }, + onShippingAbortChange: function() { + var checked = $( this ).is( ':checked' ); + var $row = $( this ).closest( 'tr' ); + + if ( checked ) { + $row.find('td.cost').hide(); + $row.find('td.abort_reason').show(); + $row.find('input[name^="shipping_per_item"], input[name^="shipping_cost_per_weight"], input[name^="shipping_cost_percent"], input[name^="shipping_cost"], input[name^="shipping_label"]').prop( 'disabled', true ).addClass( 'disabled' ); + } else { + $row.find('td.cost').show(); + $row.find('td.abort_reason').hide(); + $row.find('input[name^="shipping_per_item"], input[name^="shipping_cost_per_weight"], input[name^="shipping_cost_percent"], input[name^="shipping_cost"], input[name^="shipping_label"]').prop( 'disabled', false ).removeClass( 'disabled' ); + } + + $( '#woocommerce_table_rate_calculation_type' ).change(); + }, + onAddRate: function( event ) { + event.preventDefault(); + var target = $rates; + var size = target.find( '.table_rate' ).length; + + target.append( wc_table_rate_rows_row_template( { + rate: { + rate_id: '', + rate_class: '', + rate_condition: '', + rate_min: '', + rate_max: '', + rate_priority: '', + rate_abort: '', + rate_abort_reason: '', + rate_cost: '', + rate_cost_per_item: '', + rate_cost_per_weight_unit: '', + rate_cost_percent: '', + rate_label: '' + }, + index: size + } ) ); + + $( '#woocommerce_table_rate_calculation_type, select[name^="shipping_condition"], input[name^="shipping_abort["]', $rates_table ).change(); + }, + onRemoveRate: function( event ) { + event.preventDefault(); + if ( confirm( data.i18n.delete_rates ) ) { + var rate_ids = []; + + $rates.find( 'tr td.check-column input:checked' ).each( function( i, el ) { + var rate_id = $(el).closest( 'tr.table_rate' ).find( '.rate_id' ).val(); + rate_ids.push( rate_id ); + $(el).closest( 'tr.table_rate' ).addClass( 'deleting' ); + }); + + var ajax_data = { + action: 'woocommerce_table_rate_delete', + rate_id: rate_ids, + security: data.delete_rates_nonce + }; + + $.post( ajaxurl, ajax_data, function(response) { + $( 'tr.deleting').fadeOut( '300', function() { + $( this ).remove(); + } ); + }); + } + }, + onDupeRate: function( event ) { + event.preventDefault(); + if ( confirm( data.i18n.dupe_rates ) ) { + + $rates.find( 'tr td.check-column input:checked' ).each( function( i, el ) { + var dupe = $(el).closest( 'tr' ).clone(); + dupe.find( '.rate_id' ).val( '0' ); + $rates.append( dupe ); + }); + + wc_table_rate_rows.reindexRows(); + } + }, + reindexRows: function() { + var loop = 0; + $rates.find( 'tr' ).each( function( index, row ) { + $('input.text, input.checkbox, select.select, input[type=hidden]', row ).each( function( i, el ) { + var t = $(el); + t.attr( 'name', t.attr('name').replace(/\[([^[]*)\]/, "[" + loop + "]" ) ); + }); + loop++; + }); + } + }; + + wc_table_rate_rows.init(); + +})( jQuery, woocommerce_shipping_table_rate_rows, wp, ajaxurl ); diff --git a/assets/js/table-rate-rows.min.js b/assets/js/table-rate-rows.min.js new file mode 100644 index 0000000..b7a5a37 --- /dev/null +++ b/assets/js/table-rate-rows.min.js @@ -0,0 +1 @@ +!function(a,i,e,o){var n=e.template("table-rate-shipping-row-template"),t=a("#mainform"),r=a("#shipping_rates"),s=r.find("tbody.table_rates"),p={init:function(){t.on("change","#woocommerce_table_rate_calculation_type",this.onCalculationTypeChange),r.on("change",'select[name^="shipping_condition"]',this.onShippingConditionChange).on("change",'input[name^="shipping_abort["]',this.onShippingAbortChange).on("click","a.add-rate",this.onAddRate).on("click","a.remove",this.onRemoveRate).on("click","a.dupe",this.onDupeRate);var i=s.data("rates");a(i).each(function(e){var t=s.find(".table_rate").length;s.append(n({rate:i[e],index:t}))}),a('label[for="woocommerce_table_rate_handling_fee"], label[for="woocommerce_table_rate_max_cost"], label[for="woocommerce_table_rate_min_cost"]',t).each(function(e,t){a(t).data("o_label",a(t).text())}),a('#woocommerce_table_rate_calculation_type, select[name^="shipping_condition"], input[name^="shipping_abort["]',t).change(),s.sortable({items:"tr",cursor:"move",axis:"y",handle:"td",scrollSensitivity:40,helper:function(e,t){return t.children().each(function(){a(this).width(a(this).width())}),t.css("left","0"),t},start:function(e,t){t.item.css("background-color","#f6f6f6")},stop:function(e,t){t.item.removeAttr("style"),p.reindexRows()}})},onCalculationTypeChange:function(){var e=a(this).val();"item"==e?a("td.cost_per_item input").attr("disabled","disabled").addClass("disabled"):a("td.cost_per_item input").removeAttr("disabled").removeClass("disabled"),e?(a("#shipping_class_priorities").hide(),a("td.shipping_label, th.shipping_label").hide()):(a("#shipping_class_priorities").show(),a("td.shipping_label, th.shipping_label").show()),e||(a("#shipping_class_priorities span.description.per_order").show(),a("#shipping_class_priorities span.description.per_class").hide());var n=i.i18n.order;"item"==e?n=i.i18n.item:"line"==e?n=i.i18n.line_item:"class"==e&&(n=i.i18n.class),a('label[for="woocommerce_table_rate_handling_fee"], label[for="woocommerce_table_rate_max_cost"], label[for="woocommerce_table_rate_min_cost"]').each(function(e,t){var i=a(t).data("o_label");i=i.replace("[item]",n),a(t).text(i)})},onShippingConditionChange:function(){var e=a(this).val(),t=a(this).closest("tr");""==e?t.find('input[name^="shipping_min"], input[name^="shipping_max"]').val("").prop("disabled",!0).addClass("disabled"):t.find('input[name^="shipping_min"], input[name^="shipping_max"]').prop("disabled",!1).removeClass("disabled")},onShippingAbortChange:function(){var e=a(this).is(":checked"),t=a(this).closest("tr");e?(t.find("td.cost").hide(),t.find("td.abort_reason").show(),t.find('input[name^="shipping_per_item"], input[name^="shipping_cost_per_weight"], input[name^="shipping_cost_percent"], input[name^="shipping_cost"], input[name^="shipping_label"]').prop("disabled",!0).addClass("disabled")):(t.find("td.cost").show(),t.find("td.abort_reason").hide(),t.find('input[name^="shipping_per_item"], input[name^="shipping_cost_per_weight"], input[name^="shipping_cost_percent"], input[name^="shipping_cost"], input[name^="shipping_label"]').prop("disabled",!1).removeClass("disabled")),a("#woocommerce_table_rate_calculation_type").change()},onAddRate:function(e){e.preventDefault();var t=s,i=t.find(".table_rate").length;t.append(n({rate:{rate_id:"",rate_class:"",rate_condition:"",rate_min:"",rate_max:"",rate_priority:"",rate_abort:"",rate_abort_reason:"",rate_cost:"",rate_cost_per_item:"",rate_cost_per_weight_unit:"",rate_cost_percent:"",rate_label:""},index:i})),a('#woocommerce_table_rate_calculation_type, select[name^="shipping_condition"], input[name^="shipping_abort["]',r).change()},onRemoveRate:function(e){if(e.preventDefault(),confirm(i.i18n.delete_rates)){var n=[];s.find("tr td.check-column input:checked").each(function(e,t){var i=a(t).closest("tr.table_rate").find(".rate_id").val();n.push(i),a(t).closest("tr.table_rate").addClass("deleting")});var t={action:"woocommerce_table_rate_delete",rate_id:n,security:i.delete_rates_nonce};a.post(o,t,function(e){a("tr.deleting").fadeOut("300",function(){a(this).remove()})})}},onDupeRate:function(e){e.preventDefault(),confirm(i.i18n.dupe_rates)&&(s.find("tr td.check-column input:checked").each(function(e,t){var i=a(t).closest("tr").clone();i.find(".rate_id").val("0"),s.append(i)}),p.reindexRows())},reindexRows:function(){var n=0;s.find("tr").each(function(e,t){a("input.text, input.checkbox, select.select, input[type=hidden]",t).each(function(e,t){var i=a(t);i.attr("name",i.attr("name").replace(/\[([^[]*)\]/,"["+n+"]"))}),n++})}};p.init()}(jQuery,woocommerce_shipping_table_rate_rows,wp,ajaxurl); diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..430224a --- /dev/null +++ b/changelog.txt @@ -0,0 +1,397 @@ +*** Table Rate Shipping Changelog *** + +2024-04-15 - version 3.1.9 +* Fix - Notice is displayed when rate minimum and rate maximum is equal. + +2024-03-22 - version 3.1.8 +* Fix - Adjust conflicting rate notices to be non-persistent and less ominous. +* Tweak - WordPress 6.5 compatibility. + +2024-03-19 - version 3.1.7 +* Add - Flag conflicting rates in the table. + +2023-10-06 - version 3.1.6 +* Fix - Shipping rates are not displayed when calculating per shipping class. + +2023-10-03 - version 3.1.5 +* Fix - The optional abort reason text is missing from the checkout page. +* Fix - WordPress Coding Standards. + +2023-09-07 - version 3.1.4 +* Update - Security updates. +* Fix - Method title always defaults to "Table Rate". +* Fix - Shipping tax is not calculated. + +2023-09-05 - version 3.1.3 +* Fix - Optional abort text will only be displayed if no other rates available. +* Tweak - PHP 8.2 compatibility. + +2023-08-08 - version 3.1.2 +* Fix - Security updates. + +2023-04-04 - version 3.1.1 +* Fix - Fatal error in installation process when plugin is activated while WooCommerce is not active. + +2022-11-03 - version 3.1.0 +* Add - Declared High-Performance Order Storage (HPOS) compatibility. + +2022-09-06 - version 3.0.41 +* Fix - Remove unnecessary files from plugin zip file. +* Tweak - WC 6.8 compatibility. + +2022-06-21 - version 3.0.40 +* Fix - Make sure the woocommerce_table_rate_package_row_base_price filter is always called. + +2022-06-09 - version 3.0.39 +* Add - Option to set how Min-Max Price conditions are compared: with or without taxes. +* Fix - Tax rate included in calculations when "Min-Max Price Calculation" is enabled. + +2022-05-03 - version 3.0.38 +* Add - Option to set how Min-Max Price conditions are compared: before discounts or after discounts. + +2022-03-28 - version 3.0.37 +* Fix - "Abort Reason" blocks user to proceed to checkout eventhough another shipping method available. +* Fix - "Per Order" label is still used after the calculation type is changed to "Class" type. +* Tweak - WC 6.3 and WP 5.9 compatibility. + +2021-12-13 - version 3.0.36 +* Fix - Excluding the /languages folder in composer.json file. +* Tweak - WC 5.9 compatibility. + +2021-11-17 - version 3.0.35 +* Fix - Min & max with zero value is not equal with empty. +* Fix - Translations are not being loaded. +* Fix - Fatal error on PHP 8.0 by replacing "%" with "%%". + +2021-09-29 - version 3.0.34 +* Fix - Incorrect shipping fee calculation with measurement price calculator product. +* Fix - "Abort Reason" warnings persist after shipping information is updated. + +2021-08-31 - version 3.0.33 +* Fix - "No class" shipping class being ignored where multiple shipping classes are declared. + +2021-08-19 - version 3.0.32 +* Fix - Fatal error on PHP 8.0 when using "%" for non formatting text. + +2021-07-21 - version 3.0.31 +* Fix - Rates does not display when table rate has multiple classes being set. +* Fix - Remove "including tax" in tooltips. +* Fix - Handling Fee field stepper incompatible with comma decimal separator. + +2020-10-06 - version 3.0.30 +* Tweak - WC 4.5 compatibility. + +2020-08-19 - version 3.0.29 +* Tweak - WordPress 5.5 compatibility. + +2020-06-05 - version 3.0.28 +* Tweak - WC 4.2 compatibility. + +2020-05-12 - version 3.0.27 +* Fix - Use tax rate based on cart items when table rate is set to including taxes. +* Fix - Deduct taxes when user is VAT exempt and table rate is set to including taxes. + +2020-04-30 - version 3.0.26 +* Tweak - WC 4.1 compatibility. + +2020-04-14 - version 3.0.25 +* Fix - Save the abort notice in the session (to display when shipping methods are loaded from cache). + +2020-04-08 - version 3.0.24 +* Fix - Adjust conditions for abort notices to show in cart/checkout pages. +* Tweak - WP 5.4 compatibility. + +2020-04-01 - version 3.0.23 +* Tweak - Add filter to compare price restrictions after discounts and coupons have been applied. +* Tweak - Remove legacy code. + +2020-03-11 - version 3.0.22 +* Fix - Change columns in table based on the chosen calculation type. + +2020-02-26 - version 3.0.21 +* Tweak - WC 4.0 compatibility. +* Tweak - Improve layout for min max fields. + +2020-02-05 - version 3.0.20 +* Fix - Use proper escape for attributes. + +2020-01-15 - version 3.0.19 +* Tweak - Only show abort notices in the cart/checkout page. + +2019-11-05 - version 3.0.18 +* Tweak - WC 3.8 compatibility. + +2019-08-08 - version 3.0.17 +* Tweak - WC 3.7 compatibility. + +2019-07-02 - version 3.0.16 +* Fix - PHP notices. + +2019-04-14 - version 3.0.15 +* Update - Add filter that allows per shipping class intergration with other plugins. +* Tweak - WC 3.6 compatibility. + +2019-03-04 - version 3.0.14 +* Tweak - Order Handling Fee verbiage to not include percentages as not intended. + +2018-11-28 - version 3.0.13 +* Fix - Fatal error with inclusive taxes and calculating rates per item. + +2018-10-31 - version 3.0.12 +* Fix - Default to shipping costs exclusive of taxes for existing methods. + +2018-10-29 - version 3.0.11 +* Fix - Duplicate row would not save changes. +* Update - Allow more than 2 decimals of precision for rule constraints. +* Update - Deleting shipping class deletes related table rate shipping rules. +* Update - Allow table rate prices to be entered inclusive of taxes. +* Fix - Properly determine product's price when inclusive taxes are used and respect the 'woocommerce_adjust_non_base_location_prices' filter. +* Fix - Rounding errors of shipping prices before taxes are added. +* Fix - Multiple abort notices appearing. + +2018-09-25 - version 3.0.10 +* Update - WC 3.5 compatibility. + +2018-05-30 - version 3.0.9 +* Fix - Saving settings not working in WooCommerce 3.4.1 + +2018-05-23 - version 3.0.8 +* Update - Privacy policy notification. +* Update - WC 3.4 compatibility. +* Fix - Use correct plugin URL on plugins listview. +* Fix - Weight cost doesn't support different decimal separators. + +2018-01-26 - version 3.0.7 +* Add - Percentage support for Order total Handling Fee. +* Fix - Additional fixes for supporting decimal separator as comma. + +2018-01-12 - version 3.0.6 +* Fix - Decimal separator as comma isn't respected in table rates. + +2017-12-13 - version 3.0.5 +* Update - WC tested up to version. + +2017-06-20 - version 3.0.4 +* Fix - Additional PHP7.1 notice fixes. + +2017-04-27 - version 3.0.3 +* Fix - Additional WC 3.0 compatibility. +* Fix - PHP 7.1 notices. + +2016-09-19 - version 3.0.2 +* Fix - Class type shipping label was not showing. +* Update - Hide unnecessary class priorities depending on calculation type. + +2016-06-09 - version 3.0.1 +* Fix - Undefined method get_field_default which introduced in WC 2.6 + +2016-05-24 - version 3.0.0 +* Implemented WC 2.6.0 Support and new data structures. + +2015-11-20 - version 2.9.2 +* Fix - Escape postcodes passed to queries. + +2015-11-18 - version 2.9.1 +* Fix - No matching rates when table rate has 'No Class' rule. +* Fix - Coupons not taken into account when looping through shipping methods. + +2015-05-12 - version 2.9.0 +* Removed legacy notice code. +* Reorganised options +* Added new max cost option. +* Hide shipping classes when unused. + +2015-04-21 - version 2.8.3 +* Fix - Potential XSS with add_query_arg. + +2015-02-17 - version 2.8.2 +* Fix - Postcode save method. + +2015-02-11 - version 2.8.1 +* Fix - Fatal error in cart and checkout when trying to register the shipping methods. + +2015-01-29 - version 2.8.0 +* WC 2.3 Compatibility. +* Refactored shipping zone framework. + +2014-12-03 - version 2.7.2 +* Fixed order type abort. It should abort and offer no rates from the table. + +2014-10-14 - version 2.7.1 +* Fix JS error when abort is selected. + +2014-10-08 - version 2.7.0 +* Row cleanup. +* Additonal logic to 'abort' a table rate if a row matches. +* Show optional message on abort. +* Added option for order handling fee (base cost). +* Added option for max cost. +* Updated text domain. +* Fix display of disabled inputs. + +2014-01-28 - version 2.6.10 +* Only show debugging if set to display + +2014-01-06 - version 2.6.9 +* 2.1 compat + +2013-12-02 - version 2.6.8 +* Hooks for WPML + +2013-11-21 - version 2.6.7 +* Hook when getting product price during calculation + +2013-08-13 - version 2.6.6 +* Fix zone ordering + +2013-04-25 - version 2.6.5 +* sanitize_text_field on state names + +2013-04-22 - version 2.6.4 +* Removed uninstall scripts + +2013-04-19 - version 2.6.3 +* Round weights to 2dp + +2013-03-15 - version 2.6.2 +* Fix numeric ranges + +2013-03-13 - version 2.6.1 +* Localisation for zones + +2013-01-29 - version 2.6.0 +* Shipping Zone interface update + +2013-01-29 - version 2.5.2 +* Correctly cast the shipping class id + +2013-01-21 - version 2.5.1 +* esc_js on class name + +2013-01-11 - version 2.5.0 +* WC 2.0 Compat + +2012-12-13 - version 2.4.1 +* Fix prepare +* Fix class != check + +2012-11-26 - version 2.4.0 +* Previous version class priorities has been removed in favour of running the rates in order of definition. +* Min cost option per table rate. +* New updater + +2012-11-26 - version 2.3.0 +* Fixed method enable/disable setting. +* Choose the order in which classes are evalulated for per-class rates. + +2012-11-06 - version 2.2.2 +* Fix matched rates when using the break option. + +2012-11-06 - version 2.2.1 +* Fix labels + +2012-11-05 - version 2.2 +* For stores with tax inc prices, calculate correct item price with local tax. +* Added debug mode - kicks in when WP_DEBUG is on. +* Fix shipping_condition none. +* Renamed 'priority' to 'break' to make more sense. +* Allow label translation. + +2012-10-23 - version 2.1.3 +* Calculated rate tweak - a row much match or 0 priced rates will be ignored +* Ensure transients are cleared on save + +2012-10-05 - version 2.1.2 +* Fix insert on some systems +* Fix default shipping_method_order in table + +2012-10-05 - version 2.1.1 +* Tweak some text descriptions + +2012-10-03 - version 2.1.0 +* Ability to sort methods within zones to control the order on the frontend + +2012-08-20 - version 2.0.6 +* Fix 'Any Shipping Class' + +2012-08-14 - version 2.0.5 +* Fix priority checkbox for per-class rates + +2012-07-26 - version 2.0.4 +* Set default title for instances - labels are required so this fixes things when title is not set +* Fix get_cart_shipping_class_id function + +2012-07-19 - version 2.0.3 +* First release + +2012-06-25 - version 2.0.2 Beta +* Fix state detection for zones +* Fix count items in class +* Fix no shipping class query +* Don't hide empty shipping classes +* 'None' condition + +2012-06-12 - version 2.0.1 Beta +* Fix zone dropdown for states + +2012-04-19 - version 2.0 Beta +* Re-write based on user feedback. Due to the massive restructure, and new zones functionality it isn't possible to upgrade your old rates - you will need to re-enter them (however, due to the zones and new features this process should be much easier!) +* Re-done the interface for efficiency +* Introduction of shipping zones to simplify data entry +* Allow costs to be defined with 4dp to prevent rounding issues +* items_in_class condition, if you only want to count items of the priority class +* Rates stored in a table rather than serialised for improved reliability +* Calculated rates (add matching rules together) +* Per item, per line, per class rules for calculated rates +* Multiple table rate instances per zone +* Define costs per item, per weight unit, and a percent of the total + +2012-02-09 - version 1.5.1 +* Weights/item count did not consider quantity + +2012-02-09 - version 1.5 +* Mixed carts - when using a shipping class, only count items in said class when using item # rules +* Weight and price and count only for items that need shipping + +2012-02-09 - version 1.4.4 +* Postcode - don't remove spaces + +2012-02-09 - version 1.4.3 +* Postcode case fix + +2012-02-02 - version 1.4.2 +* Empty label fix + +2012-02-01 - version 1.4.1 +* Logic bug with priority rates + +2012-01-26 - version 1.4 +* WC 1.4 Compatibility (shipping rate API) + +2011-12-15 - version 1.3 +* Support for the new 'Product Shipping Classes' in WC 1.3. This means you can have different table rates for different groups of products. +* Drag and drop rates to re-order by priority +* 'Priority' option if you want a rate to be the *only* one used if matched + +2011-12-01 - version 1.2 +* Woo Updater +* Made use of WC 1.3 Settings API +* 'Chosen' input to aid adding rates + +2011-11-15 - version 1.1.2 +* Changed textdomain + +2011-11-15 - version 1.1.1 +* Changed text domain + +2011-10-27 - version 1.1 +* Changed the way countries are defined to improve performance +* Shortcuts for EU countries/US States +* Postcodes can now be comma separated +* Ability to exclude postcodes + +2011-10-06 - version 1.0.1 +* Fixed rates when state is chosen/entered + +2011-09-27 - version 1.0 +* First Release diff --git a/includes/class-helpers.php b/includes/class-helpers.php new file mode 100644 index 0000000..277f447 --- /dev/null +++ b/includes/class-helpers.php @@ -0,0 +1,260 @@ +prefix . 'woocommerce_shipping_table_rates'; + } + + /** + * Get raw shipping rates from the DB. + * + * Optional filter helper for integration with other plugins. + * + * @param int $instance_id Shipping method instance ID. + * @param string $output Output format. + * + * @return mixed + */ + public static function get_shipping_rates( int $instance_id, string $output = OBJECT ) { + global $wpdb; + + $table_name = self::get_db_table_name(); + + $rates = $wpdb->get_results( + $wpdb->prepare( + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT * FROM $table_name WHERE shipping_method_id = %s ORDER BY rate_order;", + $instance_id + ), + $output + ); + + return apply_filters( 'woocommerce_table_rate_get_shipping_rates', $rates ); + } + + /** + * Loop through the shipping rates and add flags to indicate if any rate's conditions conflict with another rate's conditions. + * + * @param array $shipping_rates array of shipping rates. + * + * @return array. + */ + public static function flag_conflicting_shipping_rates( array $shipping_rates ): array { + // Loop through each rate. + $number_of_rates = count( $shipping_rates ); + for ( $i = 0; $i < $number_of_rates; $i++ ) { + $rate1 = $shipping_rates[ $i ]; + + // Check if min rate is bigger than max rate. + if ( '' !== $rate1['rate_max'] && $rate1['rate_min'] > $rate1['rate_max'] ) { + $shipping_rates[ $i ]['is_conflicting_max'] = __( 'Max condition can\'t be less than Min.' ); + continue; + } + + // Compare the current rate with other rates. + for ( $j = $i + 1; $j < $number_of_rates; $j++ ) { + $rate2 = $shipping_rates[ $j ]; + + // Check if the rates not the same class and condition. + if ( + $rate1['rate_class'] !== $rate2['rate_class'] || + $rate1['rate_condition'] !== $rate2['rate_condition'] + ) { + continue; + } + + // Rate min if it's empty should represent 0. + // Using `floatval()` to change empty string to be 0. + $rate1['rate_min'] = floatval( $rate1['rate_min'] ); + $rate2['rate_min'] = floatval( $rate2['rate_min'] ); + + // If the max is empty, it represent an infinite. + if ( '' === $rate1['rate_max'] ) { + $rate1['rate_max'] = PHP_FLOAT_MAX; + } + + // If the max is empty, it represent an infinite. + if ( '' === $rate2['rate_max'] ) { + $rate2['rate_max'] = PHP_FLOAT_MAX; + } + + if ( + ( $rate1['rate_min'] >= $rate2['rate_min'] && $rate1['rate_min'] <= $rate2['rate_max'] ) || + ( $rate1['rate_max'] >= $rate2['rate_min'] && $rate1['rate_max'] <= $rate2['rate_max'] ) || + ( $rate2['rate_min'] >= $rate1['rate_min'] && $rate2['rate_min'] <= $rate1['rate_max'] ) || + ( $rate2['rate_max'] >= $rate1['rate_min'] && $rate2['rate_max'] <= $rate1['rate_max'] ) + ) { + // translators: %d is row ID. + $shipping_rates[ $i ]['is_conflicting_max'] = sprintf( esc_attr__( 'Max value is overlapping with min value from row %d.' ), esc_attr( $j + 1 ) ); + // translators: %d is row ID. + $shipping_rates[ $j ]['is_conflicting_min'] = sprintf( esc_attr__( 'Min value is overlapping with max value from row %d.' ), esc_attr( $i + 1 ) ); + } + } + } + + return $shipping_rates; + } + + /** + * Get conflicting shipping rates. + * + * @param array $flagged_shipping_rates array of flagged shipping rates. + * + * @return array. + */ + public static function get_conflicting_shipping_rates( array $flagged_shipping_rates ): array { + + if ( empty( $flagged_shipping_rates ) ) { + return array(); + } + + $conflicting_shipping_rates = array(); + + foreach ( $flagged_shipping_rates as $rate ) { + if ( ( isset( $rate['is_conflicting_min'] ) && $rate['is_conflicting_min'] ) || ( isset( $rate['is_conflicting_max'] ) && $rate['is_conflicting_max'] ) ) { + $conflicting_shipping_rates[] = $rate; + } + } + + return $conflicting_shipping_rates; + } + + /** + * Get conflicting shipping rates by instance ID. + * + * @param int $instance_id Shipping method instance ID. + * + * @return array. + */ + public static function get_conflicting_shipping_rates_by_instance_id( int $instance_id ): array { + $shipping_rates = self::get_shipping_rates( $instance_id, ARRAY_A ); + $flagged_shipping_rates = self::flag_conflicting_shipping_rates( $shipping_rates ); + + return self::get_conflicting_shipping_rates( $flagged_shipping_rates ); + } + + /** + * Get an array of formatted rate values from the $_POST data. + * Returns false if $_POST is empty. + * + * @return array|false + */ + public static function get_formatted_table_rate_row_values_from_postdata() { + + // We are not checking nonce here because we aren't saving anything. + // phpcs:disable WordPress.Security.NonceVerification.Missing + if ( empty( $_POST ) ) { + return false; + } + + $formatted_rate_values = array(); + + $precision = function_exists( 'wc_get_rounding_precision' ) ? wc_get_rounding_precision() : 4; + + // Save rates. + $rate_ids = isset( $_POST['rate_id'] ) ? array_map( 'intval', wp_unslash( $_POST['rate_id'] ) ) : array(); + $shipping_class = isset( $_POST['shipping_class'] ) ? wc_clean( wp_unslash( $_POST['shipping_class'] ) ) : array(); + $shipping_condition = isset( $_POST['shipping_condition'] ) ? wc_clean( wp_unslash( $_POST['shipping_condition'] ) ) : array(); + $shipping_min = isset( $_POST['shipping_min'] ) ? wc_clean( wp_unslash( $_POST['shipping_min'] ) ) : array(); + $shipping_max = isset( $_POST['shipping_max'] ) ? wc_clean( wp_unslash( $_POST['shipping_max'] ) ) : array(); + $shipping_cost = isset( $_POST['shipping_cost'] ) ? wc_clean( wp_unslash( $_POST['shipping_cost'] ) ) : array(); + $shipping_per_item = isset( $_POST['shipping_per_item'] ) ? wc_clean( wp_unslash( $_POST['shipping_per_item'] ) ) : array(); + $shipping_cost_per_weight = isset( $_POST['shipping_cost_per_weight'] ) ? wc_clean( wp_unslash( $_POST['shipping_cost_per_weight'] ) ) : array(); + $cost_percent = isset( $_POST['shipping_cost_percent'] ) ? wc_clean( wp_unslash( $_POST['shipping_cost_percent'] ) ) : array(); + $shipping_label = isset( $_POST['shipping_label'] ) ? wc_clean( wp_unslash( $_POST['shipping_label'] ) ) : array(); + $shipping_priority = isset( $_POST['shipping_priority'] ) ? wc_clean( wp_unslash( $_POST['shipping_priority'] ) ) : array(); + $shipping_abort = isset( $_POST['shipping_abort'] ) ? wc_clean( wp_unslash( $_POST['shipping_abort'] ) ) : array(); + $shipping_abort_reason = isset( $_POST['shipping_abort_reason'] ) ? wc_clean( wp_unslash( $_POST['shipping_abort_reason'] ) ) : array(); + // phpcs:enable WordPress.Security.NonceVerification.Missing + + // Get max key. + $max_key = ( $rate_ids ) ? max( array_keys( $rate_ids ) ) : 0; + + for ( $i = 0; $i <= $max_key; $i++ ) { + + if ( ! isset( $rate_ids[ $i ] ) ) { + continue; + } + + $rate_id = $rate_ids[ $i ]; + $rate_class = isset( $shipping_class[ $i ] ) ? $shipping_class[ $i ] : ''; + $rate_condition = $shipping_condition[ $i ]; + $rate_min = isset( $shipping_min[ $i ] ) ? $shipping_min[ $i ] : ''; + $rate_max = isset( $shipping_max[ $i ] ) ? $shipping_max[ $i ] : ''; + $rate_cost = isset( $shipping_cost[ $i ] ) ? wc_format_decimal( $shipping_cost[ $i ], $precision, true ) : ''; + $rate_cost_per_item = isset( $shipping_per_item[ $i ] ) ? wc_format_decimal( $shipping_per_item[ $i ], $precision, true ) : ''; + $rate_cost_per_weight_unit = isset( $shipping_cost_per_weight[ $i ] ) ? wc_format_decimal( $shipping_cost_per_weight[ $i ], $precision, true ) : ''; + $rate_cost_percent = isset( $cost_percent[ $i ] ) ? wc_format_decimal( str_replace( '%', '', $cost_percent[ $i ] ), $precision, true ) : ''; + $rate_label = isset( $shipping_label[ $i ] ) ? $shipping_label[ $i ] : ''; + $rate_priority = isset( $shipping_priority[ $i ] ) ? 1 : 0; + $rate_abort = isset( $shipping_abort[ $i ] ) ? 1 : 0; + $rate_abort_reason = isset( $shipping_abort_reason[ $i ] ) ? $shipping_abort_reason[ $i ] : ''; + + // Format min and max. + switch ( $rate_condition ) { + case 'weight': + case 'price': + if ( $rate_min ) { + $rate_min = wc_format_decimal( $rate_min, $precision, true ); + } + if ( $rate_max ) { + $rate_max = wc_format_decimal( $rate_max, $precision, true ); + } + break; + case 'items': + case 'items_in_class': + if ( $rate_min ) { + $rate_min = round( $rate_min ); + } + if ( $rate_max ) { + $rate_max = round( $rate_max ); + } + break; + default: + $rate_min = ''; + $rate_max = ''; + break; + } + + $formatted_rate_values[ $i ] = array( + 'rate_id' => $rate_id, + 'rate_class' => $rate_class, + 'rate_condition' => $rate_condition, + 'rate_min' => $rate_min, + 'rate_max' => $rate_max, + 'rate_cost' => $rate_cost, + 'rate_cost_per_item' => $rate_cost_per_item, + 'rate_cost_per_weight_unit' => $rate_cost_per_weight_unit, + 'rate_cost_percent' => $rate_cost_percent, + 'rate_label' => $rate_label, + 'rate_priority' => $rate_priority, + 'rate_abort' => $rate_abort, + 'rate_abort_reason' => $rate_abort_reason, + ); + } + + return $formatted_rate_values; + } +} diff --git a/includes/class-wc-shipping-table-rate-privacy.php b/includes/class-wc-shipping-table-rate-privacy.php new file mode 100644 index 0000000..58c9ba8 --- /dev/null +++ b/includes/class-wc-shipping-table-rate-privacy.php @@ -0,0 +1,32 @@ +Learn more about how this works, including what you may want to include in your privacy policy.', 'woocommerce-table-rate-shipping' ), 'https://docs.woocommerce.com/document/privacy-shipping/#woocommerce-table-rate-shipping' ) ); + } +} + +new WC_Shipping_Table_Rate_Privacy(); diff --git a/includes/class-wc-shipping-table-rate.php b/includes/class-wc-shipping-table-rate.php new file mode 100644 index 0000000..2e9b211 --- /dev/null +++ b/includes/class-wc-shipping-table-rate.php @@ -0,0 +1,1396 @@ +id = 'table_rate'; + $this->instance_id = absint( $instance_id ); + $this->method_title = __( 'Table rates', 'woocommerce-table-rate-shipping' ); + $this->method_description = __( 'Table rates are dynamic rates based on a number of cart conditions.', 'woocommerce-table-rate-shipping' ); + $this->supports = array( 'zones', 'shipping-zones', 'instance-settings' ); + $this->enabled = 'yes'; + // Load the form fields. + $this->init_form_fields(); + + // Get settings. + $this->title = $this->get_option( 'title', $this->method_title ); + $this->fee = $this->get_option( 'handling_fee' ); + $this->tax_status = $this->get_option( 'tax_status' ); + $this->order_handling_fee = $this->get_option( 'order_handling_fee' ); + $this->calculation_type = $this->get_option( 'calculation_type' ); + $this->minmax_after_discount = $this->get_option( 'minmax_after_discount' ); + $this->minmax_with_tax = $this->get_option( 'minmax_with_tax' ); + $this->min_cost = $this->get_option( 'min_cost' ); + $this->max_cost = $this->get_option( 'max_cost' ); + $this->max_shipping_cost = $this->get_option( 'max_shipping_cost' ); + + // Table rate specific variables. + $this->rates_table = $wpdb->prefix . 'woocommerce_shipping_table_rates'; + $this->available_rates = array(); + + add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); + add_filter( 'woocommerce_package_rates', array( $this, 'maybe_display_notice' ), 10, 2 ); + + $this->maybe_add_error_notices_on_settings_saved(); + } + + /** + * Gets and option from the settings API, using defaults if necessary to prevent undefined notices. + * + * @param string $key Option key. + * @param mixed $empty_value Value to return if option is empty. + * @return mixed The value specified for the option or a default value for the option. + */ + public function get_option( $key, $empty_value = null ) { + // Instance options take priority over global options. + if ( in_array( $key, array_keys( $this->get_instance_form_fields() ), true ) ) { + return $this->get_instance_option( $key, $empty_value ); + } + + // Return global option. + return parent::get_option( $key, $empty_value ); + } + + /** + * Gets an option from the settings API, using defaults if necessary to prevent undefined notices. + * + * @param string $key Option key. + * @param mixed $empty_value Value to return if option is empty. + * @return mixed The value specified for the option or a default value for the option. + */ + public function get_instance_option( $key, $empty_value = null ) { + if ( empty( $this->instance_settings ) ) { + $this->init_instance_settings(); + } + + // Get option default if unset. + if ( ! isset( $this->instance_settings[ $key ] ) ) { + $form_fields = $this->get_instance_form_fields(); + + if ( is_callable( array( $this, 'get_field_default' ) ) ) { + $this->instance_settings[ $key ] = $this->get_field_default( $form_fields[ $key ] ); + } else { + $this->instance_settings[ $key ] = empty( $form_fields[ $key ]['default'] ) ? '' : $form_fields[ $key ]['default']; + } + } + + if ( ! is_null( $empty_value ) && '' === $this->instance_settings[ $key ] ) { + $this->instance_settings[ $key ] = $empty_value; + } + + return $this->instance_settings[ $key ]; + } + + /** + * Get settings fields for instances of this shipping method (within zones). + * Should be overridden by shipping methods to add options. + * + * @since 3.0.0 + * @return array + */ + public function get_instance_form_fields() { + /** + * Filter to let third party modify the form fields. + * + * @param array List of form fields. + * + * @since 3.0.0 + */ + return apply_filters( 'woocommerce_shipping_instance_form_fields_' . $this->id, $this->instance_form_fields ); + } + + /** + * Return the name of the option in the WP DB. + * + * @since 3.0.0 + * @return string + */ + public function get_instance_option_key() { + return $this->instance_id ? $this->plugin_id . $this->id . '_' . $this->instance_id . '_settings' : ''; + } + + /** + * Initialise Settings for instances. + * + * @since 3.0.0 + */ + public function init_instance_settings() { + // 2nd option is for BW compat. + $this->instance_settings = get_option( $this->get_instance_option_key(), get_option( $this->plugin_id . $this->id . '-' . $this->instance_id . '_settings', null ) ); + + if ( empty( $this->instance_settings ) ) { + $this->instance_settings = array(); + } + + /* + * Order handling fee does not handle percentages. So + * we need to remove previously saved % before initializing. + * + * @since 3.0.14 To fix https://github.com/woocommerce/woocommerce-table-rate-shipping/issues/91 + */ + $this->instance_settings['order_handling_fee'] = str_replace( + '%', + '', + empty( $this->instance_settings['order_handling_fee'] ) ? '' : $this->instance_settings['order_handling_fee'] + ); + + // If there are no settings defined, use defaults. + if ( ! is_array( $this->instance_settings ) ) { + $form_fields = $this->get_instance_form_fields(); + $this->instance_settings = array_merge( array_fill_keys( array_keys( $form_fields ), '' ), wp_list_pluck( $form_fields, 'default' ) ); + } + } + + /** + * Initialise Gateway Settings Form Fields + */ + public function init_form_fields() { + $this->form_fields = array(); // No global options for table rates. + $this->instance_form_fields = array( + 'title' => array( + 'title' => __( 'Method Title', 'woocommerce-table-rate-shipping' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-table-rate-shipping' ), + 'default' => __( 'Table Rate', 'woocommerce-table-rate-shipping' ), + ), + 'tax_status' => array( + 'title' => __( 'Tax Status', 'woocommerce-table-rate-shipping' ), + 'type' => 'select', + 'description' => '', + 'desc_tip' => true, + 'default' => 'taxable', + 'options' => array( + 'taxable' => __( 'Taxable', 'woocommerce-table-rate-shipping' ), + 'none' => __( 'None', 'woocommerce-table-rate-shipping' ), + ), + ), + 'prices_include_tax' => array( + 'title' => __( 'Tax included in shipping costs', 'woocommerce-table-rate-shipping' ), + 'type' => 'select', + 'description' => '', + 'desc_tip' => true, + 'default' => get_option( $this->get_instance_option_key() ) ? 'no' // Shipping method has previously been configured so we default to 'no' to maintain backwards compatibility. + : ( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ? 'yes' : 'no' ), // Otherwise default to the store setting. + 'options' => array( + 'yes' => __( 'Yes, I will enter costs below inclusive of tax', 'woocommerce-table-rate-shipping' ), + 'no' => __( 'No, I will enter costs below exclusive of tax', 'woocommerce-table-rate-shipping' ), + ), + ), + 'order_handling_fee' => array( + 'title' => __( 'Handling Fee', 'woocommerce-table-rate-shipping' ), + 'type' => 'price', + // translators: %s is amount example. + 'desc_tip' => sprintf( __( 'Enter an amount, e.g. %s. Leave blank to disable. This cost is applied once for the order as a whole.', 'woocommerce-table-rate-shipping' ), '2' . wc_get_price_decimal_separator() . '50' ), + 'default' => '', + 'placeholder' => __( 'n/a', 'woocommerce-table-rate-shipping' ), + ), + 'max_shipping_cost' => array( + 'title' => __( 'Maximum Shipping Cost', 'woocommerce-table-rate-shipping' ), + 'type' => 'price', + 'desc_tip' => __( 'Maximum cost that the customer will pay after all the shipping rules have been applied. If the shipping cost calculated is bigger than this value, this cost will be the one shown.', 'woocommerce-table-rate-shipping' ), + 'default' => '', + 'placeholder' => __( 'n/a', 'woocommerce-table-rate-shipping' ), + ), + 'rates' => array( + 'title' => __( 'Rates', 'woocommerce-table-rate-shipping' ), + 'type' => 'title', + 'description' => __( 'This is where you define your table rates which are applied to an order.', 'woocommerce-table-rate-shipping' ), + 'default' => '', + ), + 'calculation_type' => array( + 'title' => __( 'Calculation Type', 'woocommerce-table-rate-shipping' ), + 'type' => 'select', + 'description' => __( 'Per order rates will offer the customer all matching rates. Calculated rates will sum all matching rates and provide a single total.', 'woocommerce-table-rate-shipping' ), + 'default' => '', + 'desc_tip' => true, + 'options' => array( + '' => __( 'Per order', 'woocommerce-table-rate-shipping' ), + 'item' => __( 'Calculated rates per item', 'woocommerce-table-rate-shipping' ), + 'line' => __( 'Calculated rates per line item', 'woocommerce-table-rate-shipping' ), + 'class' => __( 'Calculated rates per shipping class', 'woocommerce-table-rate-shipping' ), + ), + ), + 'handling_fee' => array( + 'title' => __( 'Handling Fee Per [item]', 'woocommerce-table-rate-shipping' ), + 'type' => 'price', + 'desc_tip' => true, + // translators: %1$s is amount example. + 'description' => sprintf( __( 'Handling fee. Enter an amount, e.g. %1$s, or a percentage, e.g. 5%%. Leave blank to disable. Applied based on the "Calculation Type" chosen below.', 'woocommerce-table-rate-shipping' ), '2' . wc_get_price_decimal_separator() . '50' ), + 'default' => '', + 'placeholder' => __( 'n/a', 'woocommerce-table-rate-shipping' ), + ), + 'min_cost' => array( + 'title' => __( 'Minimum Cost Per [item]', 'woocommerce-table-rate-shipping' ), + 'type' => 'price', + 'desc_tip' => true, + 'description' => __( 'Minimum cost for this shipping method (optional). If the cost is lower, this minimum cost will be enforced.', 'woocommerce-table-rate-shipping' ), + 'default' => '', + 'placeholder' => __( 'n/a', 'woocommerce-table-rate-shipping' ), + ), + 'max_cost' => array( + 'title' => __( 'Maximum Cost Per [item]', 'woocommerce-table-rate-shipping' ), + 'type' => 'price', + 'desc_tip' => true, + 'description' => __( 'Maximum cost for this shipping method (optional). If the cost is higher, this maximum cost will be enforced.', 'woocommerce-table-rate-shipping' ), + 'default' => '', + 'placeholder' => __( 'n/a', 'woocommerce-table-rate-shipping' ), + ), + 'minmax_after_discount' => array( + 'title' => __( 'Discounts in Min-Max', 'woocommerce-table-rate-shipping' ), + 'label' => __( 'Use discounted price when comparing Min-Max Price Conditions.', 'woocommerce-table-rate-shipping' ), + 'type' => 'checkbox', + 'description' => __( 'When comparing Min-Max Price Condition for rate in Table Rates, set if discounted or non-discounted price should be used.', 'woocommerce-table-rate-shipping' ), + 'default' => '', + 'desc_tip' => true, + ), + 'minmax_with_tax' => array( + 'title' => __( 'Taxes in Min-Max', 'woocommerce-table-rate-shipping' ), + 'label' => __( 'Use price with tax when comparing Min-Max Price Conditions.', 'woocommerce-table-rate-shipping' ), + 'type' => 'checkbox', + 'description' => __( 'When comparing Min-Max Price Condition for rate in Table Rates, set if price with taxes or without taxes should be used.', 'woocommerce-table-rate-shipping' ), + 'default' => '', + 'desc_tip' => true, + ), + ); + } + + /** + * Admin options + */ + public function admin_options() { + $this->instance_options(); + } + + /** + * Return admin options as a html string. + * + * @return string + */ + public function get_admin_options_html() { + ob_start(); + $this->instance_options(); + return ob_get_clean(); + } + + /** + * Admin options. + */ + public function instance_options() { + ?> + + generate_settings_html( $this->get_instance_form_fields() ); + ?> + + + + + shipping->get_shipping_classes() ) ) : ?> + + + + + +
+ +
+ instance_id ); ?> +
+ decimal_options as $option ) { + $option = 'woocommerce_table_rate_' . $option; + + // phpcs:disable WordPress.Security.NonceVerification.Missing --- security is handled by WooCommerce + if ( ! isset( $_POST[ $option ] ) ) { + continue; + } + + $_POST[ $option ] = str_replace( $decimal_separator, '.', sanitize_text_field( wp_unslash( $_POST[ $option ] ) ) ); + // phpcs:enable WordPress.Security.NonceVerification.Missing + } + + parent::process_admin_options(); + wc_table_rate_admin_shipping_rows_process( $this->instance_id ); + } + + /** + * Check if the shipping method is available based on the package and cart. + * + * @param array $package Shipping package. + * @return bool + */ + public function is_available( $package ) { + $available = true; + + if ( ! $this->get_rates( $package ) ) { + $available = false; + } + + /** + * Filter to let third party modify the availability of the table rate. + * + * @param boolean table rate availability. + * @param array Cart package. + * @param WC_Shipping_Table_Rate Table rate object. + * + * @since 3.0.0 + */ + return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $available, $package, $this ); + } + + /** + * Get the items in class count. + * + * @param array $package Shipping package. + * @param int $class_id Class ID. + * @return int + */ + public function count_items_in_class( $package, $class_id ) { + $count = 0; + + // Find shipping classes for products in the package. + foreach ( $package['contents'] as $item_id => $values ) { + if ( $values['data']->needs_shipping() && $class_id === $values['data']->get_shipping_class_id() ) { + $count += $values['quantity']; + } + } + + return $count; + } + + /** + * Get cart shipping class id. + * + * @param array $package Shipping package. + * @return int + */ + public function get_cart_shipping_class_id( $package ) { + // Find shipping class for cart. + $found_shipping_classes = array(); + $shipping_class_id = 0; + $shipping_class_slug = ''; + + // Find shipping classes for products in the package. + if ( count( $package['contents'] ) > 0 ) { + foreach ( $package['contents'] as $item_id => $values ) { + if ( $values['data']->needs_shipping() ) { + $found_shipping_classes[ $values['data']->get_shipping_class_id() ] = $values['data']->get_shipping_class(); + } + } + } + + $found_shipping_classes = array_unique( $found_shipping_classes ); + + if ( 1 === count( $found_shipping_classes ) ) { + $shipping_class_slug = current( $found_shipping_classes ); + } elseif ( $found_shipping_classes > 1 ) { + + // Get class with highest priority. + $priority = get_option( 'woocommerce_table_rate_default_priority_' . $this->instance_id ); + $priorities = get_option( 'woocommerce_table_rate_priorities_' . $this->instance_id ); + + $rates_has_no_class = false; + + // search the shipping rates array if it has empty rate_class or zero rate_class. + foreach ( Helpers::get_shipping_rates( $this->instance_id, ARRAY_A ) as $idx => $shipping_rate ) { + if ( empty( $shipping_rate['rate_class'] ) ) { + $rates_has_no_class = true; + break; + } + } + + foreach ( $found_shipping_classes as $id => $class ) { + if ( isset( $priorities[ $class ] ) && ( $priorities[ $class ] < $priority || ( empty( $shipping_class_slug ) && false === $rates_has_no_class ) ) ) { + $priority = $priorities[ $class ]; + $shipping_class_slug = $class; + } + } + } + + $found_shipping_classes = array_flip( $found_shipping_classes ); + + if ( isset( $found_shipping_classes[ $shipping_class_slug ] ) ) { + $shipping_class_id = $found_shipping_classes[ $shipping_class_slug ]; + } + + return $shipping_class_id; + } + + /** + * Rates query. + * + * @param array $args Rates args. + * @return array + */ + public function query_rates( $args ) { + global $wpdb; + + $defaults = array( + 'price' => '', + 'weight' => '', + 'count' => 1, + 'count_in_class' => 1, + 'shipping_class_id' => '', + ); + + /** + * Filter to let third party modify the query rates arguments. + * + * @param array query arguments. + * + * @since 3.0.0 + */ + $args = apply_filters( 'woocommerce_table_rate_query_rates_args', wp_parse_args( $args, $defaults ) ); + + $shipping_class_id = '' === $args['shipping_class_id'] ? 0 : absint( $args['shipping_class_id'] ); + $rates = $wpdb->get_results( + // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $rates_table is hardcoded & Repeated arguments. + $wpdb->prepare( + " + SELECT rate_id, rate_cost, rate_cost_per_item, rate_cost_per_weight_unit, rate_cost_percent, rate_label, rate_priority, rate_abort, rate_abort_reason + FROM {$this->rates_table} + WHERE shipping_method_id IN ( %s ) + AND rate_class IN ( '', %d ) + AND + ( + rate_condition = '' + OR + ( + rate_condition = 'price' + AND + ( + ( ( rate_min ) = '' AND ( rate_max ) = '' ) + OR + ( ( rate_min + 0 ) >= 0 AND ( rate_max + 0 ) >=0 AND %f >= ( rate_min + 0 ) AND %f <= ( rate_max + 0 ) ) + OR + ( ( rate_min ) >= 0 AND ( rate_max ) = '' AND %f >= ( rate_min + 0 ) ) + OR + ( ( rate_min ) = '' AND ( rate_max ) >= 0 AND %f <= ( rate_max + 0 ) ) + ) + ) + OR + ( + rate_condition = 'weight' + AND + ( + ( ( rate_min ) = '' AND ( rate_max ) = '' ) + OR + ( ( rate_min + 0 ) >= 0 AND ( rate_max + 0 ) >=0 AND %f >= ( rate_min + 0 ) AND %f <= ( rate_max + 0 ) ) + OR + ( ( rate_min ) >= 0 AND ( rate_max ) = '' AND %f >= ( rate_min + 0 ) ) + OR + ( ( rate_min ) = '' AND ( rate_max ) >= 0 AND %f <= ( rate_max + 0 ) ) + ) + ) + OR + ( + rate_condition = 'items' + AND + ( + ( ( rate_min ) = '' AND ( rate_max ) = '' ) + OR + ( ( rate_min + 0 ) >= 0 AND ( rate_max + 0 ) >=0 AND %d >= ( rate_min + 0 ) AND %d <= ( rate_max + 0 ) ) + OR + ( ( rate_min ) >= 0 AND ( rate_max ) = '' AND %d >= ( rate_min + 0 ) ) + OR + ( ( rate_min ) = '' AND ( rate_max ) >= 0 AND %d <= ( rate_max + 0 ) ) + ) + ) + OR + ( + rate_condition = 'items_in_class' + AND + ( + ( ( rate_min ) = '' AND ( rate_max ) = '' ) + OR + ( ( rate_min + 0 ) >= 0 AND ( rate_max + 0 ) >= 0 AND %d >= ( rate_min + 0 ) AND %d <= ( rate_max + 0 ) ) + OR + ( ( rate_min ) >= 0 AND ( rate_max ) = '' AND %d >= ( rate_min + 0 ) ) + OR + ( ( rate_min ) = '' AND ( rate_max ) >= 0 AND %d <= ( rate_max + 0 ) ) + ) + ) + ) + ORDER BY rate_order ASC + ", + $this->instance_id, + $shipping_class_id, + ...array_fill( 0, 4, $args['price'] ), + ...array_fill( 0, 4, $args['weight'] ), + ...array_fill( 0, 4, $args['count'] ), + ...array_fill( 0, 4, $args['count_in_class'] ) + ) + // phpcs:enable + ); + + /** + * Filter to let third party modify the table rates. + * + * @param array Query rate results. + * + * @since 3.0.0 + */ + return apply_filters( 'woocommerce_table_rate_query_rates', $rates ); + } + + /** + * Get rates function. + * + * @param array $package Package to check. + * @return bool|void + */ + public function get_rates( $package ) { + global $wpdb; + + if ( ! $this->instance_id ) { + return false; + } + + $rates = array(); + $this->unset_abort_message( $package ); + + // Get rates, depending on type. + if ( 'item' === $this->calculation_type ) { + + // For each ITEM get matching rates. + $costs = array(); + + $matched = false; + + foreach ( $package['contents'] as $item_id => $values ) { + + $_product = $values['data']; + + if ( $values['quantity'] > 0 && $_product->needs_shipping() ) { + + $product_price = $this->get_product_price( $_product, 1, $values ); + + $matching_rates = $this->query_rates( + array( + 'price' => $product_price, + 'weight' => (float) $_product->get_weight(), + 'count' => 1, + 'count_in_class' => $this->count_items_in_class( $package, $_product->get_shipping_class_id() ), + 'shipping_class_id' => $_product->get_shipping_class_id(), + ) + ); + + $item_weight = round( (float) $_product->get_weight(), 2 ); + $item_fee = (float) $this->get_fee( $this->fee, $product_price ); + $item_cost = 0; + + foreach ( $matching_rates as $rate ) { + $item_cost += (float) $rate->rate_cost; + $item_cost += (float) $rate->rate_cost_per_weight_unit * $item_weight; + $item_cost += ( (float) $rate->rate_cost_percent / 100 ) * $product_price; + $matched = true; + if ( $rate->rate_abort ) { + if ( ! empty( $rate->rate_abort_reason ) && ! wc_has_notice( $rate->rate_abort_reason, 'notice' ) ) { + $this->add_notice( $rate->rate_abort_reason, $package ); + } + return; + } + if ( $rate->rate_priority ) { + break; + } + } + + $cost = ( $item_cost + $item_fee ) * $values['quantity']; + + if ( $this->min_cost && $cost < $this->min_cost ) { + $cost = $this->min_cost; + } + if ( $this->max_cost && $cost > $this->max_cost ) { + $cost = $this->max_cost; + } + + $costs[ $item_id ] = $cost; + + } + } + + if ( $matched ) { + if ( $this->order_handling_fee ) { + $costs['order'] = $this->order_handling_fee; + } else { + $costs['order'] = 0; + } + + if ( $this->max_shipping_cost && ( $costs['order'] + array_sum( $costs ) ) > $this->max_shipping_cost ) { + $rates[] = array( + 'id' => is_callable( array( $this, 'get_rate_id' ) ) ? $this->get_rate_id() : $this->instance_id, + 'label' => $this->title, + 'cost' => $this->max_shipping_cost, + ); + } else { + $rates[] = array( + 'id' => is_callable( array( $this, 'get_rate_id' ) ) ? $this->get_rate_id() : $this->instance_id, + 'label' => $this->title, + 'cost' => $costs, + 'calc_tax' => 'per_item', + 'package' => $package, + ); + } + } + } elseif ( 'line' === $this->calculation_type ) { + + // For each LINE get matching rates. + $costs = array(); + + $matched = false; + + foreach ( $package['contents'] as $item_id => $values ) { + + $_product = $values['data']; + + if ( $values['quantity'] > 0 && $_product->needs_shipping() ) { + + $product_price = $this->get_product_price( $_product, $values['quantity'], $values ); + + $matching_rates = $this->query_rates( + array( + 'price' => $product_price, + 'weight' => (float) $_product->get_weight() * $values['quantity'], + 'count' => $values['quantity'], + 'count_in_class' => $this->count_items_in_class( $package, $_product->get_shipping_class_id() ), + 'shipping_class_id' => $_product->get_shipping_class_id(), + ) + ); + + $item_weight = round( (float) $_product->get_weight() * $values['quantity'], 2 ); + $item_fee = (float) $this->get_fee( $this->fee, $product_price ); + $item_cost = 0; + + foreach ( $matching_rates as $rate ) { + $item_cost += (float) $rate->rate_cost; + $item_cost += (float) $rate->rate_cost_per_item * $values['quantity']; + $item_cost += (float) $rate->rate_cost_per_weight_unit * $item_weight; + $item_cost += ( (float) $rate->rate_cost_percent / 100 ) * $product_price; + $matched = true; + + if ( $rate->rate_abort ) { + if ( ! empty( $rate->rate_abort_reason ) && ! wc_has_notice( $rate->rate_abort_reason, 'notice' ) ) { + $this->add_notice( $rate->rate_abort_reason, $package ); + } + return; + } + if ( $rate->rate_priority ) { + break; + } + } + + $item_cost = $item_cost + $item_fee; + + if ( $this->min_cost && $item_cost < $this->min_cost ) { + $item_cost = $this->min_cost; + } + if ( $this->max_cost && $item_cost > $this->max_cost ) { + $item_cost = $this->max_cost; + } + + $costs[ $item_id ] = $item_cost; + } + } + + if ( $matched ) { + if ( $this->order_handling_fee ) { + $costs['order'] = $this->order_handling_fee; + } else { + $costs['order'] = 0; + } + + if ( $this->max_shipping_cost && ( $costs['order'] + array_sum( $costs ) ) > $this->max_shipping_cost ) { + $rates[] = array( + 'id' => is_callable( array( $this, 'get_rate_id' ) ) ? $this->get_rate_id() : $this->instance_id, + 'label' => $this->title, + 'cost' => $this->max_shipping_cost, + 'package' => $package, + ); + } else { + $rates[] = array( + 'id' => is_callable( array( $this, 'get_rate_id' ) ) ? $this->get_rate_id() : $this->instance_id, + 'label' => $this->title, + 'cost' => $costs, + 'calc_tax' => 'per_item', + 'package' => $package, + ); + } + } + } elseif ( 'class' === $this->calculation_type ) { + + // For each CLASS get matching rates. + $total_cost = 0; + + // First get all the rates in the table. + $all_rates = Helpers::get_shipping_rates( $this->instance_id ); + + // Now go through cart items and group items by class. + $classes = array(); + + foreach ( $package['contents'] as $item_id => $values ) { + + $_product = $values['data']; + + if ( $values['quantity'] > 0 && $_product->needs_shipping() ) { + + $shipping_class = $_product->get_shipping_class_id(); + + if ( ! isset( $classes[ $shipping_class ] ) ) { + $classes[ $shipping_class ] = new stdClass(); + $classes[ $shipping_class ]->price = 0; + $classes[ $shipping_class ]->weight = 0; + $classes[ $shipping_class ]->items = 0; + $classes[ $shipping_class ]->items_in_class = 0; + } + + $classes[ $shipping_class ]->price += $this->get_product_price( $_product, $values['quantity'], $values ); + $classes[ $shipping_class ]->weight += (float) $_product->get_weight() * $values['quantity']; + $classes[ $shipping_class ]->items += $values['quantity']; + $classes[ $shipping_class ]->items_in_class += $values['quantity']; + } + } + + $matched = false; + $total_cost = 0; + $stop = false; + + // Now we have groups, loop the rates and find matches in order. + foreach ( $all_rates as $rate ) { + + foreach ( $classes as $class_id => $class ) { + + if ( '' === $class_id ) { + if ( 0 !== (int) $rate->rate_class && '' !== $rate->rate_class ) { + continue; + } + } elseif ( $class_id !== (int) $rate->rate_class && '' !== $rate->rate_class ) { + continue; + } + + $rate_match = false; + + switch ( $rate->rate_condition ) { + case '': + $rate_match = true; + break; + case 'price': + case 'weight': + case 'items_in_class': + case 'items': + $condition = $rate->rate_condition; + $value = $class->$condition; + + if ( '' === $rate->rate_min && '' === $rate->rate_max ) { + $rate_match = true; + } + + if ( $value >= $rate->rate_min && $value <= $rate->rate_max ) { + $rate_match = true; + } + + if ( $value >= $rate->rate_min && ! $rate->rate_max ) { + $rate_match = true; + } + + if ( $value <= $rate->rate_max && ! $rate->rate_min ) { + $rate_match = true; + } + + break; + } + + // Rate matched class. + if ( $rate_match ) { + $rate_label = $this->title; + $class_cost = 0; + $class_cost += (float) $rate->rate_cost; + $class_cost += (float) $rate->rate_cost_per_item * $class->items_in_class; + $class_cost += (float) $rate->rate_cost_per_weight_unit * $class->weight; + $class_cost += ( (float) $rate->rate_cost_percent / 100 ) * $class->price; + + if ( $rate->rate_abort ) { + if ( ! empty( $rate->rate_abort_reason ) && ! wc_has_notice( $rate->rate_abort_reason, 'notice' ) ) { + $this->add_notice( $rate->rate_abort_reason, $package ); + } + return; + } + + if ( $rate->rate_priority ) { + $stop = true; + } + + $matched = true; + + $class_fee = (float) $this->get_fee( $this->fee, $class->price ); + $class_cost += $class_fee; + + if ( $this->min_cost && $class_cost < $this->min_cost ) { + $class_cost = $this->min_cost; + } + if ( $this->max_cost && $class_cost > $this->max_cost ) { + $class_cost = $this->max_cost; + } + + $total_cost += $class_cost; + } + } + + // Breakpoint. + if ( $stop ) { + break; + } + } + + if ( $this->order_handling_fee ) { + $total_cost += $this->get_fee( $this->order_handling_fee, $total_cost ); + } + + if ( $this->max_shipping_cost && $total_cost > $this->max_shipping_cost ) { + $total_cost = $this->max_shipping_cost; + } + + if ( $matched ) { + $rates[] = array( + 'id' => is_callable( array( $this, 'get_rate_id' ) ) ? $this->get_rate_id() : $this->instance_id, + 'label' => $rate_label, + 'cost' => $total_cost, + 'package' => $package, + ); + } + } else { + + // For the ORDER get matching rates. + $shipping_class = $this->get_cart_shipping_class_id( $package ); + $price = 0; + $weight = 0; + $count = 0; + $count_in_class = 0; + + foreach ( $package['contents'] as $item_id => $values ) { + + $_product = $values['data']; + + if ( $values['quantity'] > 0 && $_product->needs_shipping() ) { + $price += $this->get_product_price( $_product, $values['quantity'], $values ); + $weight += (float) $_product->get_weight() * (float) $values['quantity']; + $count += $values['quantity']; + + if ( $_product->get_shipping_class_id() === $shipping_class ) { + $count_in_class += $values['quantity']; + } + } + } + + $matching_rates = $this->query_rates( + array( + 'price' => $price, + 'weight' => $weight, + 'count' => $count, + 'count_in_class' => $count_in_class, + 'shipping_class_id' => $shipping_class, + ) + ); + + foreach ( $matching_rates as $rate ) { + $label = $rate->rate_label; + if ( ! $label ) { + $label = $this->title; + } + + if ( $rate->rate_abort ) { + if ( ! empty( $rate->rate_abort_reason ) && ! wc_has_notice( $rate->rate_abort_reason, 'notice' ) ) { + $this->add_notice( $rate->rate_abort_reason, $package ); + } + $rates = array(); // Clear rates. + break; + } + + if ( $rate->rate_priority ) { + $rates = array(); + } + + $cost = (float) $rate->rate_cost; + $cost += (float) $rate->rate_cost_per_item * $count; + $cost += (float) $this->get_fee( $this->fee, $price ); + $cost += (float) $rate->rate_cost_per_weight_unit * $weight; + $cost += ( (float) $rate->rate_cost_percent / 100 ) * $price; + + if ( $this->order_handling_fee ) { + $cost += $this->order_handling_fee; + } + + if ( $this->min_cost && $cost < $this->min_cost ) { + $cost = $this->min_cost; + } + + if ( $this->max_cost && $cost > $this->max_cost ) { + $cost = $this->max_cost; + } + + if ( $this->max_shipping_cost && $cost > $this->max_shipping_cost ) { + $cost = $this->max_shipping_cost; + } + + $rates[] = array( + 'id' => is_callable( array( $this, 'get_rate_id' ) ) ? $this->get_rate_id( $rate->rate_id ) : $this->instance_id . ' : ' . $rate->rate_id, + 'label' => $label, + 'cost' => $cost, + 'package' => $package, + ); + + if ( $rate->rate_priority ) { + break; + } + } + } + + $is_customer_vat_exempt = WC()->cart->get_customer()->get_is_vat_exempt(); + + if ( 'yes' === $this->get_instance_option( 'prices_include_tax' ) && ( $this->is_taxable() || $is_customer_vat_exempt ) ) { + // We allow the table rate to be entered inclusive of taxes just like product prices. + foreach ( $rates as $key => $rate ) { + + $tax_rates = WC_Tax::get_shipping_tax_rates(); + + // Temporarily override setting since our shipping rate will always include taxes here. + add_filter( 'woocommerce_prices_include_tax', array( $this, 'override_prices_include_tax_setting' ) ); + $base_tax_rates = WC_Tax::get_shipping_tax_rates( null, false ); + remove_filter( 'woocommerce_prices_include_tax', array( $this, 'override_prices_include_tax_setting' ) ); + + $total_cost = is_array( $rate['cost'] ) ? array_sum( $rate['cost'] ) : $rate['cost']; + + /** + * Filter to let third party properly determine product's price when inclusive taxes are used and respect the 'woocommerce_adjust_non_base_location_prices'. + * + * @param boolean. + * + * @since 3.0.10 + */ + if ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { + $taxes = WC_Tax::calc_tax( $total_cost, $base_tax_rates, true ); + } else { + $taxes = WC_Tax::calc_tax( $total_cost, $tax_rates, true ); + } + + $rates[ $key ]['cost'] = $total_cost - array_sum( $taxes ); + + $rates[ $key ]['taxes'] = $is_customer_vat_exempt ? array() : WC_Tax::calc_shipping_tax( $rates[ $key ]['cost'], $tax_rates ); + + $rates[ $key ]['price_decimals'] = '4'; // Prevent the cost from being rounded before the tax is added. + } + } + + // None found? + if ( 0 === count( $rates ) ) { + return false; + } + + // Set available. + $this->available_rates = $rates; + + return true; + } + + /** + * Unique function for overriding the prices including tax setting in WooCommerce. + * + * @since 3.0.27 + * + * @return bool + */ + public function override_prices_include_tax_setting() { + return true; + } + + /** + * Calculate shipping. + * + * @param array $package Shipping package. + */ + public function calculate_shipping( $package = array() ) { + if ( $this->available_rates ) { + foreach ( $this->available_rates as $rate ) { + $this->add_rate( $rate ); + } + } + } + + /** + * Get shipping rates with normalized values (respect decimal separator + * settings), for display. + * + * @return array + */ + public function get_normalized_shipping_rates() { + $shipping_rates = Helpers::flag_conflicting_shipping_rates( Helpers::get_shipping_rates( $this->instance_id, ARRAY_A ) ); + $decimal_separator = wc_get_price_decimal_separator(); + $normalize_keys = array( + 'rate_cost', + 'rate_cost_per_item', + 'rate_cost_per_weight_unit', + 'rate_cost_percent', + 'rate_max', + 'rate_min', + ); + + foreach ( $shipping_rates as $index => $shipping_rate ) { + foreach ( $normalize_keys as $key ) { + if ( ! isset( $shipping_rate[ $key ] ) ) { + continue; + } + + $shipping_rates[ $index ][ $key ] = str_replace( '.', $decimal_separator, $shipping_rates[ $index ][ $key ] ); + } + } + + return $shipping_rates; + } + + /** + * Retrieve the product price from a line item. + * + * @param object $_product Product object. + * @param int $qty Line item quantity. + * @param array $item Array of line item data. + * @return float + */ + public function get_product_price( $_product, $qty = 1, $item = array() ) { + + /** + * Filter to let third party use the product price with discounts or without discounts to calculate Min-Max conditions. + * + * @param boolean minmax_after_discount option value. + * @param array Cart item. + * + * @since 3.0.38 + */ + if ( apply_filters( 'woocommerce_table_rate_compare_price_limits_after_discounts', wc_string_to_bool( $this->minmax_after_discount ), $item ) && isset( $item['line_total'] ) ) { + $row_base_price = $item['line_total'] + ( wc_string_to_bool( $this->minmax_with_tax ) && isset( $item['line_tax'] ) ? $item['line_tax'] : 0 ); + + /** + * Filter to let third party modify the row base price. + * + * @param float + * @param WC_Product + * @param int + * + * @since 3.0.0 + */ + return apply_filters( 'woocommerce_table_rate_package_row_base_price', $row_base_price, $_product, $qty ); + } elseif ( isset( $item['line_subtotal'] ) ) { + $row_base_price = $item['line_subtotal'] + ( wc_string_to_bool( $this->minmax_with_tax ) && isset( $item['line_subtotal_tax'] ) ? $item['line_subtotal_tax'] : 0 ); + + /** + * Filter to let third party modify the row base price. + * + * @param float + * @param WC_Product + * @param int + * + * @since 3.0.0 + */ + return apply_filters( 'woocommerce_table_rate_package_row_base_price', $row_base_price, $_product, $qty ); + } + + $row_base_price = $_product->get_price() * $qty; + + // From Issue #134 : Adding a compatibility product price for Measurement Price Calculator plugin by SkyVerge. + if ( class_exists( 'WC_Measurement_Price_Calculator_Loader' ) && isset( $item['pricing_item_meta_data']['_price'] ) ) { + $row_base_price = $item['pricing_item_meta_data']['_price'] * $qty; + } + + /** + * Filter to let third party modify the row base price. + * + * @param float + * @param WC_Product + * @param int + * + * @since 3.0.0 + */ + $row_base_price = apply_filters( 'woocommerce_table_rate_package_row_base_price', $row_base_price, $_product, $qty ); + + if ( $_product->is_taxable() && wc_prices_include_tax() ) { + + $base_tax_rates = WC_Tax::get_base_tax_rates( $_product->get_tax_class() ); + + $tax_rates = WC_Tax::get_rates( $_product->get_tax_class() ); + + /** + * Filter to let third party properly determine product's price when inclusive taxes are used and respect the 'woocommerce_adjust_non_base_location_prices'. + * + * @param boolean. + * + * @since 3.0.10 + */ + if ( $tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { + $base_taxes = WC_Tax::calc_tax( $row_base_price, $base_tax_rates, true, true ); + $modded_taxes = WC_Tax::calc_tax( $row_base_price - array_sum( $base_taxes ), $tax_rates, false ); + $row_base_price = ( $row_base_price - array_sum( $base_taxes ) ) + array_sum( $modded_taxes ); + } + } + + return $row_base_price; + } + + /** + * Admin Panel Options Processing + * - Saves the options to the DB + * + * @since 1.0.0 + * @deprecated 3.0.0 + */ + public function process_instance_options() { + $this->validate_settings_fields( $this->get_instance_form_fields() ); + + if ( count( $this->errors ) > 0 ) { + $this->display_errors(); + return false; + } else { + wc_table_rate_admin_shipping_rows_process( $this->instance_id ); + update_option( $this->get_instance_option_key(), $this->sanitized_fields ); + return true; + } + } + + /** + * Save the abort notice in the session (to display when shipping methods are loaded from cache). + * + * @since 3.0.19 + * @param string $message Message to show. + * @param array $package Shipping package. + * @return void + */ + private function add_notice( $message, $package = null ) { + $abort = WC()->session->get( WC_Table_Rate_Shipping::$abort_key ); + + if ( ! is_array( $abort ) ) { + $abort = array(); + } + + $package_hash = WC_Table_Rate_Shipping::create_package_hash( $package ); + $abort[ $package_hash ] = $message; + + WC()->session->set( WC_Table_Rate_Shipping::$abort_key, $abort ); + } + + /** + * Print the abort text if no other available rates. + * + * @param array $rates Shipping rates. + * @param array $package Shipping package. + * + * @return array. + */ + public function maybe_display_notice( $rates, $package ) { + // Only display shipping notices in cart/checkout. + if ( ! is_cart() && ! is_checkout() ) { + return $rates; + } + + if ( ! empty( $rates ) ) { + $this->unset_abort_message( $package ); + return $rates; + } + + $abort = WC()->session->get( WC_Table_Rate_Shipping::$abort_key ); + + if ( ! is_array( $abort ) ) { + return $rates; + } + + $package_hash = WC_Table_Rate_Shipping::create_package_hash( $package ); + + if ( isset( $abort[ $package_hash ] ) && ! wc_has_notice( $abort[ $package_hash ], 'notice' ) ) { + wc_add_notice( $abort[ $package_hash ], 'notice', array( 'wc_trs' => 'yes' ) ); + } + + return $rates; + } + + /** + * Unset the abort notice in the session. + * + * @param array $package Shipping package. + * @since 3.0.25 + */ + private function unset_abort_message( $package = null ) { + $abort = WC()->session->get( WC_Table_Rate_Shipping::$abort_key ); + $package_hash = WC_Table_Rate_Shipping::create_package_hash( $package ); + + unset( $abort[ $package_hash ] ); + WC()->session->set( WC_Table_Rate_Shipping::$abort_key, $abort ); + } + + /** + * Maybe add error notices when the settings are saved. + * + * @return void + */ + public function maybe_add_error_notices_on_settings_saved() { + + // No need to check for nonce since we are only comparing values. + // phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( ! isset( $_GET['tab'], $_GET['instance_id'] ) || 'shipping' !== $_GET['tab'] ) { + return; + } + + $instance_id = absint( $_GET['instance_id'] ); + + if ( $instance_id !== $this->instance_id ) { + return; + } + + $formatted_rate_values = Helpers::get_formatted_table_rate_row_values_from_postdata(); + + if ( empty( $formatted_rate_values ) ) { + return; + } + + $conflicting_shipping_rates = Helpers::get_conflicting_shipping_rates( Helpers::flag_conflicting_shipping_rates( $formatted_rate_values ) ); + + if ( empty( $conflicting_shipping_rates ) ) { + return; + } + + add_action( 'woocommerce_settings_shipping', function () { + + $message = sprintf( + /* translators: %s: message */ + '

%s

', + esc_html__( + 'You have overlapping shipping rates defined, which may offer multiple options for the same criteria at checkout. If this is not your intention please review and adjust your rates and verify it on the cart/checkout pages.', + 'woocommerce-table-rate-shipping' + ) + ); + + echo wp_kses_post( $message ); + }, 0 ); + + // phpcs:enable WordPress.Security.NonceVerification.Recommended + } + +} diff --git a/includes/class-wc-table-rate-shipping.php b/includes/class-wc-table-rate-shipping.php new file mode 100644 index 0000000..8d0c5b7 --- /dev/null +++ b/includes/class-wc-table-rate-shipping.php @@ -0,0 +1,278 @@ +no_update[ $plugin_base ] ) ) { + unset( $value->no_update[ $plugin_base ] ); + } + + return $value; + } + + /** + * Register method for usage. + * + * @param array $shipping_methods List of shipping methods. + * @return array + */ + public function woocommerce_shipping_methods( $shipping_methods ) { + $shipping_methods['table_rate'] = 'WC_Shipping_Table_Rate'; + return $shipping_methods; + } + + /** + * Init TRS. + */ + public function init() { + if ( ! class_exists( 'WooCommerce' ) ) { + add_action( 'admin_notices', array( $this, 'woocommerce_deactivated' ) ); + return; + } + + include_once WC_TABLE_RATE_SHIPPING_MAIN_ABSPATH . 'includes/functions-ajax.php'; + include_once WC_TABLE_RATE_SHIPPING_MAIN_ABSPATH . 'includes/functions-admin.php'; + + /** + * Install check (for updates). + */ + if ( get_option( 'table_rate_shipping_version' ) < TABLE_RATE_SHIPPING_VERSION ) { + $this->install(); + } + + add_filter( 'woocommerce_shipping_methods', array( $this, 'woocommerce_shipping_methods' ) ); + + // Hooks. + add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); + add_action( 'wp_enqueue_scripts', array( $this, 'frontend_enqueue_scripts' ) ); + add_action( 'init', array( $this, 'load_plugin_textdomain' ) ); + add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 10, 2 ); + add_filter( 'woocommerce_translations_updates_for_woocommerce-table-rate-shipping', '__return_true' ); + add_action( 'woocommerce_shipping_init', array( $this, 'shipping_init' ) ); + add_action( 'delete_product_shipping_class', array( $this, 'update_deleted_shipping_class' ) ); + add_action( 'woocommerce_before_cart', array( $this, 'maybe_show_abort' ), 1 ); + add_action( 'woocommerce_before_checkout_form_cart_notices', array( $this, 'maybe_show_abort' ), 20 ); + } + + /** + * Declare High-Performance Order Storage (HPOS) compatibility + * + * @see https://github.com/woocommerce/woocommerce/wiki/High-Performance-Order-Storage-Upgrade-Recipe-Book#declaring-extension-incompatibility + * + * @return void + */ + public function declare_hpos_compatibility() { + if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) { + \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', 'woocommerce-table-rate-shipping/woocommerce-table-rate-shipping.php' ); + } + } + + /** + * Localisation. + */ + public function load_plugin_textdomain() { + load_plugin_textdomain( 'woocommerce-table-rate-shipping', false, dirname( plugin_basename( WC_TABLE_RATE_SHIPPING_MAIN_FILE ) ) . '/languages/' ); + } + + /** + * Row meta. + * + * @param array $links List of plugin links. + * @param string $file Current file. + * @return array + */ + public function plugin_row_meta( $links, $file ) { + if ( plugin_basename( WC_TABLE_RATE_SHIPPING_MAIN_FILE ) === $file ) { + $row_meta = array( + /** + * Filter to let third party modify the documentation URL. + * + * @param string documentation URL. + * + * @since 3.0.0 + */ + 'docs' => '' . __( 'Docs', 'woocommerce-table-rate-shipping' ) . '', + + /** + * Filter to let third party modify the Support URL. + * + * @param string Support URL. + * + * @since 2.9.1 + */ + 'support' => '' . __( 'Premium Support', 'woocommerce-table-rate-shipping' ) . '', + ); + return array_merge( $links, $row_meta ); + } + return (array) $links; + } + + /** + * Admin styles + scripts. + */ + public function admin_enqueue_scripts() { + $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; + + wp_enqueue_style( 'woocommerce_shipping_table_rate_styles', plugins_url( '/assets/css/admin.css', WC_TABLE_RATE_SHIPPING_MAIN_FILE ), array(), TABLE_RATE_SHIPPING_VERSION ); + wp_register_script( 'woocommerce_shipping_table_rate_rows', plugins_url( '/assets/js/table-rate-rows' . $suffix . '.js', WC_TABLE_RATE_SHIPPING_MAIN_FILE ), array( 'jquery', 'wp-util' ), TABLE_RATE_SHIPPING_VERSION, true ); + wp_localize_script( + 'woocommerce_shipping_table_rate_rows', + 'woocommerce_shipping_table_rate_rows', + array( + 'i18n' => array( + 'order' => __( 'Order', 'woocommerce-table-rate-shipping' ), + 'item' => __( 'Item', 'woocommerce-table-rate-shipping' ), + 'line_item' => __( 'Line Item', 'woocommerce-table-rate-shipping' ), + 'class' => __( 'Class', 'woocommerce-table-rate-shipping' ), + 'delete_rates' => __( 'Delete the selected rates?', 'woocommerce-table-rate-shipping' ), + 'dupe_rates' => __( 'Duplicate the selected rates?', 'woocommerce-table-rate-shipping' ), + ), + 'delete_rates_nonce' => wp_create_nonce( 'delete-rate' ), + ) + ); + } + + /** + * Enqueue front-end scripts. + * + * @return void + */ + public function frontend_enqueue_scripts() { + $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; + wp_enqueue_script( 'woocommerce_shipping_table_rate_checkout', plugins_url( '/assets/js/frontend-checkout' . $suffix . '.js', WC_TABLE_RATE_SHIPPING_MAIN_FILE ), array( 'jquery' ), TABLE_RATE_SHIPPING_VERSION, true ); + } + + /** + * Load shipping class. + */ + public function shipping_init() { + require_once WC_TABLE_RATE_SHIPPING_MAIN_ABSPATH . 'includes/class-wc-shipping-table-rate.php'; + require_once WC_TABLE_RATE_SHIPPING_MAIN_ABSPATH . 'includes/class-wc-shipping-table-rate-privacy.php'; + } + + /** + * Installer. + */ + public function install() { + include_once WC_TABLE_RATE_SHIPPING_MAIN_ABSPATH . 'installer.php'; + update_option( 'table_rate_shipping_version', TABLE_RATE_SHIPPING_VERSION ); + } + + /** + * Delete table rates when deleting shipping class. + * + * @param int $term_id Term ID. + */ + public function update_deleted_shipping_class( $term_id ) { + global $wpdb; + + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_table_rates', array( 'rate_class' => $term_id ) ); + } + + /** + * WooCommerce Deactivated Notice. + */ + public function woocommerce_deactivated() { + /* translators: %s: WooCommerce link */ + echo '

' . sprintf( esc_html__( 'WooCommerce Table Rate Shipping requires %s to be installed and active.', 'woocommerce-table-rate-shipping' ), 'WooCommerce' ) . '

'; + } + + /** + * Show abort message when shipping methods are loaded from cache. + * + * @since 3.0.25 + * @return void + */ + public function maybe_show_abort() { + $abort = WC()->session->get( self::$abort_key ); + + if ( ! is_array( $abort ) ) { + return; + } + + $packages = WC()->cart->get_shipping_packages(); + + if ( count( $packages ) ) { + foreach ( $packages as $package_id => $package ) { + $package_hash = self::create_package_hash( $package ); + + if ( isset( $abort[ $package_hash ] ) && ! wc_has_notice( $abort[ $package_hash ], 'notice' ) ) { + wc_add_notice( $abort[ $package_hash ], 'notice', array( 'wc_trs' => 'yes' ) ); + } + } + } + } + + /** + * Create hash string based on package. + * + * @since 3.0.26 + * @param array $package Shipping package. + * @return string + */ + public static function create_package_hash( $package ) { + // Remove data objects so hashes are consistent. + if ( isset( $package['contents'] ) ) { + foreach ( $package['contents'] as $item_id => $item ) { + if ( isset( $package['contents'][ $item_id ]['data'] ) ) { + unset( $package['contents'][ $item_id ]['data'] ); + } + } + } + + $package_to_hash = array_filter( + $package, + function ( $key ) { + return in_array( $key, array( 'contents', 'contents_cost', 'applied_coupons', 'user', 'destination' ), true ); + }, + ARRAY_FILTER_USE_KEY + ); + + // Calculate the hash for this package so we can tell if it's changed since last calculation. + return 'wc_table_rate_' . md5( wp_json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) ); + } +} diff --git a/includes/functions-admin.php b/includes/functions-admin.php new file mode 100644 index 0000000..97781ac --- /dev/null +++ b/includes/functions-admin.php @@ -0,0 +1,297 @@ + 'product_shipping_class', + 'hide_empty' => false, + ) + ); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + get_normalized_shipping_rates() ) ) : _wp_specialchars( wp_json_encode( $instance->get_normalized_shipping_rates() ), ENT_QUOTES, 'UTF-8', true ); + ?> + +
+ + [?] + + + [?] + + + [?] + + + [?] + + + [?] + + + [?] + + + [?] + + + [?] + + + [?] + + [?] +
+ + shipping->get_shipping_classes(); + if ( ! $classes ) : + echo '

' . esc_html__( 'No shipping classes exist - you can ignore this option :)', 'woocommerce-table-rate-shipping' ) . '

'; + else : + $priority = get_option( 'woocommerce_table_rate_default_priority_' . $shipping_method_id, 10 ); + ?> + + + + + + + + + + + + + + + + + + slug ] ) ) ? $woocommerce_table_rate_priorities[ $class->slug ] : 10; + + echo ''; + } + ?> + +
+ + searched for all shipping classes. If all product shipping classes are identical, the corresponding class will be used.
If there are a mix of classes then the class with the lowest number priority (defined above) will be used.', 'woocommerce-table-rate-shipping' ) ); + ?> +
+
' . esc_html( $class->name ) . '
+ query( "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE ('_transient_wc_ship_%')" ); + + // Save class priorities. + if ( empty( $_POST['woocommerce_table_rate_calculation_type'] ) ) { + + if ( isset( $_POST['woocommerce_table_rate_priorities'] ) ) { + $priorities = array_map( 'intval', (array) $_POST['woocommerce_table_rate_priorities'] ); + update_option( 'woocommerce_table_rate_priorities_' . $shipping_method_id, $priorities ); + } + + if ( isset( $_POST['woocommerce_table_rate_default_priority'] ) ) { + update_option( 'woocommerce_table_rate_default_priority_' . $shipping_method_id, intval( $_POST['woocommerce_table_rate_default_priority'] ) ); + } + } else { + delete_option( 'woocommerce_table_rate_priorities_' . $shipping_method_id ); + delete_option( 'woocommerce_table_rate_default_priority_' . $shipping_method_id ); + } + + $formatted_rate_values = Helpers::get_formatted_table_rate_row_values_from_postdata(); + + if ( empty( $formatted_rate_values ) ) { + return; + } + + $db_value_format = array( + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%d', + '%d', + '%d', + '%s', + ); + + foreach ( $formatted_rate_values as $idx => $rate_values ) { + $db_values = array( + 'rate_class' => $rate_values['rate_class'], + 'rate_condition' => sanitize_title( $rate_values['rate_condition'] ), + 'rate_min' => $rate_values['rate_min'], + 'rate_max' => $rate_values['rate_max'], + 'rate_cost' => $rate_values['rate_cost'], + 'rate_cost_per_item' => $rate_values['rate_cost_per_item'], + 'rate_cost_per_weight_unit' => $rate_values['rate_cost_per_weight_unit'], + 'rate_cost_percent' => $rate_values['rate_cost_percent'], + 'rate_label' => $rate_values['rate_label'], + 'rate_priority' => $rate_values['rate_priority'], + 'rate_order' => $idx, + 'shipping_method_id' => $shipping_method_id, + 'rate_abort' => $rate_values['rate_abort'], + 'rate_abort_reason' => $rate_values['rate_abort_reason'], + ); + + if ( $rate_values['rate_id'] > 0 ) { + + // Update row. + $wpdb->update( + $wpdb->prefix . 'woocommerce_shipping_table_rates', + $db_values, + array( + 'rate_id' => $rate_values['rate_id'], + ), + $db_value_format, + array( + '%d', + ) + ); + + } else { + + // Insert row. + $wpdb->insert( + $wpdb->prefix . 'woocommerce_shipping_table_rates', + $db_values, + $db_value_format + ); + } + } +} diff --git a/includes/functions-ajax.php b/includes/functions-ajax.php new file mode 100644 index 0000000..a87b0ec --- /dev/null +++ b/includes/functions-ajax.php @@ -0,0 +1,40 @@ +query( + // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Repeated arguments. + $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_shipping_table_rates WHERE rate_id IN ({$placeholders})", ...$rate_ids ) + // phpcs:enable + ); + } + + die(); +} diff --git a/installer.php b/installer.php new file mode 100644 index 0000000..63d7a26 --- /dev/null +++ b/installer.php @@ -0,0 +1,47 @@ +hide_errors(); + +$collate = ''; + +if ( $wpdb->has_cap( 'collation' ) ) { + $collate = $wpdb->get_charset_collate(); +} + +require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + +// Table for storing table rates themselves. shipping_method_id is an individual table of rates applied to a zone. +$sql = " +CREATE TABLE {$wpdb->prefix}woocommerce_shipping_table_rates ( +rate_id bigint(20) NOT NULL auto_increment, +rate_class varchar(200) NOT NULL, +rate_condition varchar(200) NOT NULL, +rate_min varchar(200) NOT NULL, +rate_max varchar(200) NOT NULL, +rate_cost varchar(200) NOT NULL, +rate_cost_per_item varchar(200) NOT NULL, +rate_cost_per_weight_unit varchar(200) NOT NULL, +rate_cost_percent varchar(200) NOT NULL, +rate_label longtext NULL, +rate_priority int(1) NOT NULL, +rate_order bigint(20) NOT NULL, +shipping_method_id bigint(20) NOT NULL, +rate_abort int(1) NOT NULL, +rate_abort_reason longtext NULL, +PRIMARY KEY (rate_id) +) $collate; +"; +dbDelta( $sql ); + +update_option( 'hide_table_rate_welcome_notice', '' ); diff --git a/languages/woocommerce-table-rate-shipping.pot b/languages/woocommerce-table-rate-shipping.pot new file mode 100644 index 0000000..af9aa5f --- /dev/null +++ b/languages/woocommerce-table-rate-shipping.pot @@ -0,0 +1,449 @@ +# Copyright (C) 2024 WooCommerce +# This file is distributed under the GNU General Public License v3.0. +msgid "" +msgstr "" +"Project-Id-Version: WooCommerce Table Rate Shipping 3.1.9\n" +"Report-Msgid-Bugs-To: " +"https://wordpress.org/support/plugin/woocommerce-table-rate-shipping\n" +"POT-Creation-Date: 2024-04-15 19:47:30+00:00\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2024-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"X-Generator: node-wp-i18n 1.2.3\n" + +#: includes/functions-admin.php:130 includes/functions-admin.php:133 +#: includes/functions-admin.php:136 includes/functions-admin.php:139 +msgid "0" +msgstr "" + +#: includes/class-helpers.php:72 +msgid "Max condition can't be less than Min." +msgstr "" + +#: includes/class-helpers.php:110 +#. translators: %d is row ID. +msgid "Max value is overlapping with min value from row %d." +msgstr "" + +#: includes/class-helpers.php:112 +#. translators: %d is row ID. +msgid "Min value is overlapping with max value from row %d." +msgstr "" + +#: includes/class-wc-shipping-table-rate-privacy.php:20 +#: includes/class-wc-shipping-table-rate.php:122 +msgid "Table rates" +msgstr "" + +#: includes/class-wc-shipping-table-rate-privacy.php:28 +#. translators: %s is privacy page link. +msgid "" +"By using this extension, you may be storing personal data or sharing data " +"with an external service. Learn more about " +"how this works, including what you may want to include in your privacy " +"policy." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:123 +msgid "Table rates are dynamic rates based on a number of cart conditions." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:265 +msgid "Method Title" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:268 +msgid "This controls the title which the user sees during checkout." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:269 +msgid "Table Rate" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:272 +msgid "Tax Status" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:278 +msgid "Taxable" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:279 +#: includes/functions-admin.php:109 +msgid "None" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:283 +msgid "Tax included in shipping costs" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:290 +msgid "Yes, I will enter costs below inclusive of tax" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:291 +msgid "No, I will enter costs below exclusive of tax" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:295 +msgid "Handling Fee" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:298 +#. translators: %s is amount example. +msgid "" +"Enter an amount, e.g. %s. Leave blank to disable. This cost is applied once " +"for the order as a whole." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:300 +#: includes/class-wc-shipping-table-rate.php:307 +#: includes/class-wc-shipping-table-rate.php:335 +#: includes/class-wc-shipping-table-rate.php:343 +#: includes/class-wc-shipping-table-rate.php:351 +#: includes/functions-admin.php:119 +msgid "n/a" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:303 +msgid "Maximum Shipping Cost" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:305 +msgid "" +"Maximum cost that the customer will pay after all the shipping rules have " +"been applied. If the shipping cost calculated is bigger than this value, " +"this cost will be the one shown." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:310 +msgid "Rates" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:312 +msgid "This is where you define your table rates which are applied to an order." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:316 +msgid "Calculation Type" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:318 +msgid "" +"Per order rates will offer the customer all matching rates. Calculated " +"rates will sum all matching rates and provide a single total." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:322 +msgid "Per order" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:323 +msgid "Calculated rates per item" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:324 +msgid "Calculated rates per line item" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:325 +msgid "Calculated rates per shipping class" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:329 +msgid "Handling Fee Per [item]" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:333 +#. translators: %1$s is amount example. +msgid "" +"Handling fee. Enter an amount, e.g. %1$s, or a percentage, e.g. 5%%. Leave " +"blank to disable. Applied based on the \"Calculation Type\" chosen below." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:338 +msgid "Minimum Cost Per [item]" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:341 +msgid "" +"Minimum cost for this shipping method (optional). If the cost is lower, " +"this minimum cost will be enforced." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:346 +msgid "Maximum Cost Per [item]" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:349 +msgid "" +"Maximum cost for this shipping method (optional). If the cost is higher, " +"this maximum cost will be enforced." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:354 +msgid "Discounts in Min-Max" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:355 +msgid "Use discounted price when comparing Min-Max Price Conditions." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:357 +msgid "" +"When comparing Min-Max Price Condition for rate in Table Rates, set if " +"discounted or non-discounted price should be used." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:362 +msgid "Taxes in Min-Max" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:363 +msgid "Use price with tax when comparing Min-Max Price Conditions." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:365 +msgid "" +"When comparing Min-Max Price Condition for rate in Table Rates, set if " +"price with taxes or without taxes should be used." +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:400 +msgid "Table Rates" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:407 +msgid "Class Priorities" +msgstr "" + +#: includes/class-wc-shipping-table-rate.php:1384 +#. translators: %s: message +msgid "" +"You have overlapping shipping rates defined, which may offer multiple " +"options for the same criteria at checkout. If this is not your intention " +"please review and adjust your rates and verify it on the cart/checkout " +"pages." +msgstr "" + +#: includes/class-wc-table-rate-shipping.php:138 +msgid "View Documentation" +msgstr "" + +#: includes/class-wc-table-rate-shipping.php:138 +msgid "Docs" +msgstr "" + +#: includes/class-wc-table-rate-shipping.php:147 +msgid "Visit Premium Customer Support Forum" +msgstr "" + +#: includes/class-wc-table-rate-shipping.php:147 +msgid "Premium Support" +msgstr "" + +#: includes/class-wc-table-rate-shipping.php:167 +msgid "Order" +msgstr "" + +#: includes/class-wc-table-rate-shipping.php:168 +msgid "Item" +msgstr "" + +#: includes/class-wc-table-rate-shipping.php:169 +msgid "Line Item" +msgstr "" + +#: includes/class-wc-table-rate-shipping.php:170 +#: includes/functions-admin.php:167 +msgid "Class" +msgstr "" + +#: includes/class-wc-table-rate-shipping.php:171 +msgid "Delete the selected rates?" +msgstr "" + +#: includes/class-wc-table-rate-shipping.php:172 +msgid "Duplicate the selected rates?" +msgstr "" + +#: includes/class-wc-table-rate-shipping.php:221 +#. translators: %s: WooCommerce link +msgid "WooCommerce Table Rate Shipping requires %s to be installed and active." +msgstr "" + +#: includes/functions-admin.php:38 +msgid "Shipping Class" +msgstr "" + +#: includes/functions-admin.php:39 +msgid "Shipping class this rate applies to." +msgstr "" + +#: includes/functions-admin.php:43 +msgid "Condition" +msgstr "" + +#: includes/functions-admin.php:44 +msgid "Condition vs. destination" +msgstr "" + +#: includes/functions-admin.php:47 +msgid "Min–Max" +msgstr "" + +#: includes/functions-admin.php:48 +msgid "Bottom and top range for the selected condition. " +msgstr "" + +#: includes/functions-admin.php:51 +msgid "Break" +msgstr "" + +#: includes/functions-admin.php:52 +msgid "" +"Break at this point. For per-order rates, no rates other than this will be " +"offered. For calculated rates, this will stop any further rates being " +"matched." +msgstr "" + +#: includes/functions-admin.php:55 +msgid "Abort" +msgstr "" + +#: includes/functions-admin.php:56 +msgid "" +"Enable this option to disable all rates/this shipping method if this row " +"matches any item/line/class being quoted." +msgstr "" + +#: includes/functions-admin.php:59 +msgid "Row cost" +msgstr "" + +#: includes/functions-admin.php:60 +msgid "Cost for shipping the order." +msgstr "" + +#: includes/functions-admin.php:63 +msgid "Item cost" +msgstr "" + +#: includes/functions-admin.php:64 +msgid "Cost per item." +msgstr "" + +#: includes/functions-admin.php:67 +msgid "cost" +msgstr "" + +#: includes/functions-admin.php:68 +msgid "Cost per weight unit." +msgstr "" + +#: includes/functions-admin.php:71 +msgid "% cost" +msgstr "" + +#: includes/functions-admin.php:72 +msgid "Percentage of total to charge." +msgstr "" + +#: includes/functions-admin.php:74 +msgid "Label" +msgstr "" + +#: includes/functions-admin.php:75 +msgid "Label for the shipping method which the user will be presented. " +msgstr "" + +#: includes/functions-admin.php:81 +msgid "Add Shipping Rate" +msgstr "" + +#: includes/functions-admin.php:82 +msgid "Define your table rates here in order of priority." +msgstr "" + +#: includes/functions-admin.php:82 +msgid "Duplicate selected rows" +msgstr "" + +#: includes/functions-admin.php:82 +msgid "Delete selected rows" +msgstr "" + +#: includes/functions-admin.php:99 +msgid "Any class" +msgstr "" + +#: includes/functions-admin.php:100 +msgid "No class" +msgstr "" + +#: includes/functions-admin.php:110 +msgid "Price" +msgstr "" + +#: includes/functions-admin.php:111 +msgid "Weight" +msgstr "" + +#: includes/functions-admin.php:112 +msgid "Item count" +msgstr "" + +#: includes/functions-admin.php:114 +msgid "Item count (same class)" +msgstr "" + +#: includes/functions-admin.php:127 +msgid "Optional abort reason text" +msgstr "" + +#: includes/functions-admin.php:160 +msgid "No shipping classes exist - you can ignore this option :)" +msgstr "" + +#: includes/functions-admin.php:168 +msgid "Priority" +msgstr "" + +#: includes/functions-admin.php:176 +msgid "" +"When calculating shipping, the cart contents will be searched for " +"all shipping classes. If all product shipping classes are " +"identical, the corresponding class will be " +"used.
If there are a mix of classes then the class " +"with the lowest number priority (defined above) will be " +"used." +msgstr "" + +#: includes/functions-admin.php:184 +msgid "Default" +msgstr "" + +#. Plugin Name of the plugin/theme +msgid "WooCommerce Table Rate Shipping" +msgstr "" + +#. Plugin URI of the plugin/theme +msgid "https://woocommerce.com/products/table-rate-shipping/" +msgstr "" + +#. Description of the plugin/theme +msgid "" +"Table rate shipping lets you define rates depending on location vs shipping " +"class, price, weight, or item count." +msgstr "" + +#. Author of the plugin/theme +msgid "WooCommerce" +msgstr "" + +#. Author URI of the plugin/theme +msgid "https://woocommerce.com/" +msgstr "" \ No newline at end of file diff --git a/uninstall.php b/uninstall.php new file mode 100644 index 0000000..c91264e --- /dev/null +++ b/uninstall.php @@ -0,0 +1,15 @@ +query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_shipping_table_rates" ); +} diff --git a/woocommerce-table-rate-shipping.php b/woocommerce-table-rate-shipping.php new file mode 100644 index 0000000..7e84f94 --- /dev/null +++ b/woocommerce-table-rate-shipping.php @@ -0,0 +1,62 @@ +