1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
if ( ! defined( 'ABSPATH' ) ) { |
4
|
|
|
exit; // Exit if accessed directly |
5
|
|
|
} |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Performs tax calculations and loads tax rates |
9
|
|
|
* |
10
|
|
|
* @class WC_Tax |
11
|
|
|
* @version 2.2.0 |
12
|
|
|
* @package WooCommerce/Classes |
13
|
|
|
* @category Class |
14
|
|
|
* @author WooThemes |
15
|
|
|
*/ |
16
|
|
|
class WC_Tax { |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* Precision. |
20
|
|
|
* |
21
|
|
|
* @var int |
22
|
|
|
*/ |
23
|
|
|
public static $precision; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Round at subtotal. |
27
|
|
|
* |
28
|
|
|
* @var bool |
29
|
|
|
*/ |
30
|
|
|
public static $round_at_subtotal; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Load options. |
34
|
|
|
* |
35
|
|
|
* @access public |
36
|
|
|
*/ |
37
|
|
|
public static function init() { |
38
|
|
|
self::$precision = WC_ROUNDING_PRECISION; |
39
|
|
|
self::$round_at_subtotal = 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ); |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Calculate tax for a line. |
44
|
|
|
* @param float $price Price to calc tax on |
45
|
|
|
* @param array $rates Rates to apply |
46
|
|
|
* @param boolean $price_includes_tax Whether the passed price has taxes included |
47
|
|
|
* @param boolean $suppress_rounding Whether to suppress any rounding from taking place |
48
|
|
|
* @return array Array of rates + prices after tax |
49
|
|
|
*/ |
50
|
|
|
public static function calc_tax( $price, $rates, $price_includes_tax = false, $suppress_rounding = false ) { |
51
|
|
|
// Work in pence to X precision |
52
|
|
|
$price = self::precision( $price ); |
53
|
|
|
|
54
|
|
|
if ( $price_includes_tax ) { |
55
|
|
|
$taxes = self::calc_inclusive_tax( $price, $rates ); |
56
|
|
|
} else { |
57
|
|
|
$taxes = self::calc_exclusive_tax( $price, $rates ); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
// Round to precision |
61
|
|
|
if ( ! self::$round_at_subtotal && ! $suppress_rounding ) { |
62
|
|
|
$taxes = array_map( 'round', $taxes ); // Round to precision |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
// Remove precision |
66
|
|
|
$price = self::remove_precision( $price ); |
67
|
|
|
$taxes = array_map( array( __CLASS__, 'remove_precision' ), $taxes ); |
68
|
|
|
|
69
|
|
|
return apply_filters( 'woocommerce_calc_tax', $taxes, $price, $rates, $price_includes_tax, $suppress_rounding ); |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Calculate the shipping tax using a passed array of rates. |
74
|
|
|
* |
75
|
|
|
* @param float Price |
76
|
|
|
* @param array Taxation Rate |
77
|
|
|
* @return array |
78
|
|
|
*/ |
79
|
|
|
public static function calc_shipping_tax( $price, $rates ) { |
80
|
|
|
return self::calc_exclusive_tax( $price, $rates ); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Multiply cost by pow precision. |
85
|
|
|
* @param float $price |
86
|
|
|
* @return float |
87
|
|
|
*/ |
88
|
|
|
private static function precision( $price ) { |
89
|
|
|
return $price * ( pow( 10, self::$precision ) ); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Divide cost by pow precision. |
94
|
|
|
* @param float $price |
95
|
|
|
* @return float |
96
|
|
|
*/ |
97
|
|
|
private static function remove_precision( $price ) { |
98
|
|
|
return $price / ( pow( 10, self::$precision ) ); |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* Round to precision. |
103
|
|
|
* |
104
|
|
|
* Filter example: to return rounding to .5 cents you'd use: |
105
|
|
|
* |
106
|
|
|
* function euro_5cent_rounding( $in ) { |
107
|
|
|
* return round( $in / 5, 2 ) * 5; |
108
|
|
|
* } |
109
|
|
|
* add_filter( 'woocommerce_tax_round', 'euro_5cent_rounding' ); |
110
|
|
|
* @return double |
111
|
|
|
*/ |
112
|
|
|
public static function round( $in ) { |
113
|
|
|
return apply_filters( 'woocommerce_tax_round', round( $in, self::$precision ), $in ); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* Calc tax from inclusive price. |
118
|
|
|
* |
119
|
|
|
* @param float $price |
120
|
|
|
* @param array $rates |
121
|
|
|
* @return array |
122
|
|
|
*/ |
123
|
|
|
public static function calc_inclusive_tax( $price, $rates ) { |
124
|
|
|
$taxes = array(); |
125
|
|
|
|
126
|
|
|
$regular_tax_rates = $compound_tax_rates = 0; |
127
|
|
|
|
128
|
|
|
foreach ( $rates as $key => $rate ) |
129
|
|
|
if ( $rate['compound'] == 'yes' ) |
130
|
|
|
$compound_tax_rates = $compound_tax_rates + $rate['rate']; |
131
|
|
|
else |
132
|
|
|
$regular_tax_rates = $regular_tax_rates + $rate['rate']; |
133
|
|
|
|
134
|
|
|
$regular_tax_rate = 1 + ( $regular_tax_rates / 100 ); |
135
|
|
|
$compound_tax_rate = 1 + ( $compound_tax_rates / 100 ); |
136
|
|
|
$non_compound_price = $price / $compound_tax_rate; |
137
|
|
|
|
138
|
|
|
foreach ( $rates as $key => $rate ) { |
139
|
|
|
if ( ! isset( $taxes[ $key ] ) ) |
140
|
|
|
$taxes[ $key ] = 0; |
141
|
|
|
|
142
|
|
|
$the_rate = $rate['rate'] / 100; |
143
|
|
|
|
144
|
|
|
if ( $rate['compound'] == 'yes' ) { |
145
|
|
|
$the_price = $price; |
146
|
|
|
$the_rate = $the_rate / $compound_tax_rate; |
147
|
|
|
} else { |
148
|
|
|
$the_price = $non_compound_price; |
149
|
|
|
$the_rate = $the_rate / $regular_tax_rate; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
$net_price = $price - ( $the_rate * $the_price ); |
153
|
|
|
$tax_amount = $price - $net_price; |
154
|
|
|
$taxes[ $key ] += apply_filters( 'woocommerce_price_inc_tax_amount', $tax_amount, $key, $rate, $price ); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
return $taxes; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Calc tax from exclusive price. |
162
|
|
|
* |
163
|
|
|
* @param float $price |
164
|
|
|
* @param array $rates |
165
|
|
|
* @return array |
166
|
|
|
*/ |
167
|
|
|
public static function calc_exclusive_tax( $price, $rates ) { |
168
|
|
|
$taxes = array(); |
169
|
|
|
|
170
|
|
|
if ( $rates ) { |
|
|
|
|
171
|
|
|
// Multiple taxes |
172
|
|
|
foreach ( $rates as $key => $rate ) { |
173
|
|
|
|
174
|
|
|
if ( $rate['compound'] == 'yes' ) |
175
|
|
|
continue; |
176
|
|
|
|
177
|
|
|
$tax_amount = $price * ( $rate['rate'] / 100 ); |
178
|
|
|
|
179
|
|
|
// ADVANCED: Allow third parties to modify this rate |
180
|
|
|
$tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price ); |
181
|
|
|
|
182
|
|
|
// Add rate |
183
|
|
View Code Duplication |
if ( ! isset( $taxes[ $key ] ) ) |
|
|
|
|
184
|
|
|
$taxes[ $key ] = $tax_amount; |
185
|
|
|
else |
186
|
|
|
$taxes[ $key ] += $tax_amount; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
$pre_compound_total = array_sum( $taxes ); |
190
|
|
|
|
191
|
|
|
// Compound taxes |
192
|
|
|
foreach ( $rates as $key => $rate ) { |
193
|
|
|
|
194
|
|
|
if ( $rate['compound'] == 'no' ) |
195
|
|
|
continue; |
196
|
|
|
|
197
|
|
|
$the_price_inc_tax = $price + ( $pre_compound_total ); |
198
|
|
|
|
199
|
|
|
$tax_amount = $the_price_inc_tax * ( $rate['rate'] / 100 ); |
200
|
|
|
|
201
|
|
|
// ADVANCED: Allow third parties to modify this rate |
202
|
|
|
$tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price, $the_price_inc_tax, $pre_compound_total ); |
203
|
|
|
|
204
|
|
|
// Add rate |
205
|
|
View Code Duplication |
if ( ! isset( $taxes[ $key ] ) ) |
|
|
|
|
206
|
|
|
$taxes[ $key ] = $tax_amount; |
207
|
|
|
else |
208
|
|
|
$taxes[ $key ] += $tax_amount; |
209
|
|
|
} |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
return $taxes; |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* Searches for all matching country/state/postcode tax rates. |
217
|
|
|
* |
218
|
|
|
* @param array $args |
219
|
|
|
* @return array |
220
|
|
|
*/ |
221
|
|
|
public static function find_rates( $args = array() ) { |
222
|
|
|
$args = wp_parse_args( $args, array( |
223
|
|
|
'country' => '', |
224
|
|
|
'state' => '', |
225
|
|
|
'city' => '', |
226
|
|
|
'postcode' => '', |
227
|
|
|
'tax_class' => '' |
228
|
|
|
) ); |
229
|
|
|
|
230
|
|
|
extract( $args, EXTR_SKIP ); |
231
|
|
|
|
232
|
|
|
if ( ! $country ) { |
233
|
|
|
return array(); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
$postcode = wc_normalize_postcode( wc_clean( $postcode ) ); |
237
|
|
|
$cache_key = WC_Cache_Helper::get_cache_prefix( 'taxes' ) . 'wc_tax_rates_' . md5( sprintf( '%s+%s+%s+%s+%s', $country, $state, $city, $postcode, $tax_class ) ); |
238
|
|
|
$matched_tax_rates = wp_cache_get( $cache_key, 'taxes' ); |
239
|
|
|
|
240
|
|
|
if ( false === $matched_tax_rates ) { |
241
|
|
|
$matched_tax_rates = self::get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ); |
242
|
|
|
wp_cache_set( $cache_key, $matched_tax_rates, 'taxes' ); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
return apply_filters( 'woocommerce_find_rates', $matched_tax_rates, $args ); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Searches for all matching country/state/postcode tax rates. |
250
|
|
|
* |
251
|
|
|
* @param array $args |
252
|
|
|
* @return array |
253
|
|
|
*/ |
254
|
|
|
public static function find_shipping_rates( $args = array() ) { |
255
|
|
|
$rates = self::find_rates( $args ); |
256
|
|
|
$shipping_rates = array(); |
257
|
|
|
|
258
|
|
|
if ( is_array( $rates ) ) { |
259
|
|
|
foreach ( $rates as $key => $rate ) { |
260
|
|
|
if ( 'yes' === $rate['shipping'] ) { |
261
|
|
|
$shipping_rates[ $key ] = $rate; |
262
|
|
|
} |
263
|
|
|
} |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
return $shipping_rates; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Loop through a set of tax rates and get the matching rates (1 per priority). |
271
|
|
|
* |
272
|
|
|
* @param string $country |
273
|
|
|
* @param string $state |
274
|
|
|
* @param string $postcode |
275
|
|
|
* @param string $city |
276
|
|
|
* @param string $tax_class |
277
|
|
|
* @return array |
278
|
|
|
*/ |
279
|
|
|
private static function get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ) { |
280
|
|
|
global $wpdb; |
281
|
|
|
|
282
|
|
|
// Query criteria - these will be ANDed |
283
|
|
|
$criteria = array(); |
284
|
|
|
$criteria[] = $wpdb->prepare( "tax_rate_country IN ( %s, '' )", strtoupper( $country ) ); |
285
|
|
|
$criteria[] = $wpdb->prepare( "tax_rate_state IN ( %s, '' )", strtoupper( $state ) ); |
286
|
|
|
$criteria[] = $wpdb->prepare( "tax_rate_class = %s", sanitize_title( $tax_class ) ); |
287
|
|
|
|
288
|
|
|
// Pre-query postcode ranges for PHP based matching. |
289
|
|
|
$postcode_search = wc_get_wildcard_postcodes( $postcode ); |
290
|
|
|
$postcode_ranges = $wpdb->get_results( "SELECT tax_rate_id, location_code FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE location_type = 'postcode' AND location_code LIKE '%...%';" ); |
291
|
|
|
|
292
|
|
|
if ( $postcode_ranges ) { |
293
|
|
|
$matches = wc_postcode_location_matcher( $postcode, $postcode_ranges, 'tax_rate_id', 'location_code' ); |
294
|
|
|
$postcode_search = array_unique( array_merge( $postcode_search, array_values( $matches ) ) ); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Location matching criteria - ORed |
299
|
|
|
* Needs to match: |
300
|
|
|
* - rates with no postcodes and cities |
301
|
|
|
* - rates with a matching postcode and city |
302
|
|
|
* - rates with matching postcode, no city |
303
|
|
|
* - rates with matching city, no postcode |
304
|
|
|
*/ |
305
|
|
|
$locations_criteria = array(); |
306
|
|
|
$locations_criteria[] = "locations.location_type IS NULL"; |
307
|
|
|
$locations_criteria[] = " |
308
|
|
|
locations.location_type = 'postcode' AND locations.location_code IN ('" . implode( "','", array_map( 'esc_sql', $postcode_search ) ) . "') |
309
|
|
|
AND ( |
310
|
|
|
( locations2.location_type = 'city' AND locations2.location_code = '" . esc_sql( strtoupper( $city ) ) . "' ) |
311
|
|
|
OR NOT EXISTS ( |
312
|
|
|
SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub |
313
|
|
|
WHERE sub.location_type = 'city' |
314
|
|
|
AND sub.tax_rate_id = tax_rates.tax_rate_id |
315
|
|
|
) |
316
|
|
|
) |
317
|
|
|
"; |
318
|
|
|
$locations_criteria[] = " |
319
|
|
|
locations.location_type = 'city' AND locations.location_code = '" . esc_sql( strtoupper( $city ) ) . "' |
320
|
|
|
AND NOT EXISTS ( |
321
|
|
|
SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub |
322
|
|
|
WHERE sub.location_type = 'postcode' |
323
|
|
|
AND sub.tax_rate_id = tax_rates.tax_rate_id |
324
|
|
|
) |
325
|
|
|
"; |
326
|
|
|
$criteria[] = '( ( ' . implode( ' ) OR ( ', $locations_criteria ) . ' ) )'; |
327
|
|
|
|
328
|
|
|
$found_rates = $wpdb->get_results( " |
329
|
|
|
SELECT tax_rates.* |
330
|
|
|
FROM {$wpdb->prefix}woocommerce_tax_rates as tax_rates |
331
|
|
|
LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations ON tax_rates.tax_rate_id = locations.tax_rate_id |
332
|
|
|
LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations2 ON tax_rates.tax_rate_id = locations2.tax_rate_id |
333
|
|
|
WHERE 1=1 AND " . implode( ' AND ', $criteria ) . " |
334
|
|
|
GROUP BY tax_rate_id |
335
|
|
|
ORDER BY tax_rate_priority, tax_rate_order |
336
|
|
|
" ); |
337
|
|
|
|
338
|
|
|
$matched_tax_rates = array(); |
339
|
|
|
$found_priority = array(); |
340
|
|
|
|
341
|
|
|
foreach ( $found_rates as $found_rate ) { |
342
|
|
|
if ( in_array( $found_rate->tax_rate_priority, $found_priority ) ) { |
343
|
|
|
continue; |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
$matched_tax_rates[ $found_rate->tax_rate_id ] = array( |
347
|
|
|
'rate' => $found_rate->tax_rate, |
348
|
|
|
'label' => $found_rate->tax_rate_name, |
349
|
|
|
'shipping' => $found_rate->tax_rate_shipping ? 'yes' : 'no', |
350
|
|
|
'compound' => $found_rate->tax_rate_compound ? 'yes' : 'no' |
351
|
|
|
); |
352
|
|
|
|
353
|
|
|
$found_priority[] = $found_rate->tax_rate_priority; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
return apply_filters( 'woocommerce_matched_tax_rates', $matched_tax_rates, $country, $state, $postcode, $city, $tax_class ); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
/** |
360
|
|
|
* Get the customer tax location based on their status and the current page. |
361
|
|
|
* |
362
|
|
|
* Used by get_rates(), get_shipping_rates(). |
363
|
|
|
* |
364
|
|
|
* @param $tax_class string Optional, passed to the filter for advanced tax setups. |
365
|
|
|
* @return array |
366
|
|
|
*/ |
367
|
|
|
public static function get_tax_location( $tax_class = '' ) { |
368
|
|
|
$location = array(); |
369
|
|
|
|
370
|
|
|
if ( ! empty( WC()->customer ) ) { |
371
|
|
|
$location = WC()->customer->get_taxable_address(); |
372
|
|
|
} elseif ( wc_prices_include_tax() || 'base' === get_option( 'woocommerce_default_customer_address' ) || 'base' === get_option( 'woocommerce_tax_based_on' ) ) { |
373
|
|
|
$location = array( |
374
|
|
|
WC()->countries->get_base_country(), |
375
|
|
|
WC()->countries->get_base_state(), |
376
|
|
|
WC()->countries->get_base_postcode(), |
377
|
|
|
WC()->countries->get_base_city() |
378
|
|
|
); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class ); |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* Get's an array of matching rates for a tax class. |
386
|
|
|
* @param string $tax_class |
387
|
|
|
* @return array |
388
|
|
|
*/ |
389
|
|
|
public static function get_rates( $tax_class = '' ) { |
390
|
|
|
$tax_class = sanitize_title( $tax_class ); |
391
|
|
|
$location = self::get_tax_location( $tax_class ); |
392
|
|
|
$matched_tax_rates = array(); |
393
|
|
|
|
394
|
|
|
if ( sizeof( $location ) === 4 ) { |
395
|
|
|
list( $country, $state, $postcode, $city ) = $location; |
396
|
|
|
|
397
|
|
|
$matched_tax_rates = self::find_rates( array( |
398
|
|
|
'country' => $country, |
399
|
|
|
'state' => $state, |
400
|
|
|
'postcode' => $postcode, |
401
|
|
|
'city' => $city, |
402
|
|
|
'tax_class' => $tax_class |
403
|
|
|
) ); |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
return apply_filters( 'woocommerce_matched_rates', $matched_tax_rates, $tax_class ); |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* Get's an array of matching rates for the shop's base country. |
411
|
|
|
* |
412
|
|
|
* @param string Tax Class |
413
|
|
|
* @return array |
414
|
|
|
*/ |
415
|
|
|
public static function get_base_tax_rates( $tax_class = '' ) { |
416
|
|
|
return apply_filters( 'woocommerce_base_tax_rates', self::find_rates( array( |
417
|
|
|
'country' => WC()->countries->get_base_country(), |
418
|
|
|
'state' => WC()->countries->get_base_state(), |
419
|
|
|
'postcode' => WC()->countries->get_base_postcode(), |
420
|
|
|
'city' => WC()->countries->get_base_city(), |
421
|
|
|
'tax_class' => $tax_class |
422
|
|
|
) ), $tax_class ); |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
/** |
426
|
|
|
* Alias for get_base_tax_rates(). |
427
|
|
|
* |
428
|
|
|
* @deprecated 2.3 |
429
|
|
|
* @param string Tax Class |
430
|
|
|
* @return array |
431
|
|
|
*/ |
432
|
|
|
public static function get_shop_base_rate( $tax_class = '' ) { |
433
|
|
|
return self::get_base_tax_rates( $tax_class ); |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
/** |
437
|
|
|
* Gets an array of matching shipping tax rates for a given class. |
438
|
|
|
* |
439
|
|
|
* @param string Tax Class |
440
|
|
|
* @return mixed |
441
|
|
|
*/ |
442
|
|
|
public static function get_shipping_tax_rates( $tax_class = null ) { |
443
|
|
|
// See if we have an explicitly set shipping tax class |
444
|
|
|
if ( $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ) ) { |
445
|
|
|
$tax_class = 'standard' === $shipping_tax_class ? '' : $shipping_tax_class; |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
$location = self::get_tax_location( $tax_class ); |
449
|
|
|
$matched_tax_rates = array(); |
450
|
|
|
|
451
|
|
|
if ( sizeof( $location ) === 4 ) { |
452
|
|
|
list( $country, $state, $postcode, $city ) = $location; |
453
|
|
|
|
454
|
|
|
if ( ! is_null( $tax_class ) ) { |
455
|
|
|
// This will be per item shipping |
456
|
|
|
$matched_tax_rates = self::find_shipping_rates( array( |
457
|
|
|
'country' => $country, |
458
|
|
|
'state' => $state, |
459
|
|
|
'postcode' => $postcode, |
460
|
|
|
'city' => $city, |
461
|
|
|
'tax_class' => $tax_class |
462
|
|
|
) ); |
463
|
|
|
|
464
|
|
View Code Duplication |
} else { |
|
|
|
|
465
|
|
|
|
466
|
|
|
// This will be per order shipping - loop through the order and find the highest tax class rate |
467
|
|
|
$cart_tax_classes = WC()->cart->get_cart_item_tax_classes(); |
468
|
|
|
|
469
|
|
|
// If multiple classes are found, use the first one. Don't bother with standard rate, we can get that later. |
470
|
|
|
if ( sizeof( $cart_tax_classes ) > 1 && ! in_array( '', $cart_tax_classes ) ) { |
471
|
|
|
$tax_classes = self::get_tax_classes(); |
472
|
|
|
|
473
|
|
|
foreach ( $tax_classes as $tax_class ) { |
474
|
|
|
$tax_class = sanitize_title( $tax_class ); |
475
|
|
|
if ( in_array( $tax_class, $cart_tax_classes ) ) { |
476
|
|
|
$matched_tax_rates = self::find_shipping_rates( array( |
477
|
|
|
'country' => $country, |
478
|
|
|
'state' => $state, |
479
|
|
|
'postcode' => $postcode, |
480
|
|
|
'city' => $city, |
481
|
|
|
'tax_class' => $tax_class |
482
|
|
|
) ); |
483
|
|
|
break; |
484
|
|
|
} |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
// If a single tax class is found, use it |
488
|
|
|
} elseif ( sizeof( $cart_tax_classes ) == 1 ) { |
489
|
|
|
$matched_tax_rates = self::find_shipping_rates( array( |
490
|
|
|
'country' => $country, |
491
|
|
|
'state' => $state, |
492
|
|
|
'postcode' => $postcode, |
493
|
|
|
'city' => $city, |
494
|
|
|
'tax_class' => $cart_tax_classes[0] |
495
|
|
|
) ); |
496
|
|
|
} |
497
|
|
|
} |
498
|
|
|
|
499
|
|
|
// Get standard rate if no taxes were found |
500
|
|
|
if ( ! sizeof( $matched_tax_rates ) ) { |
501
|
|
|
$matched_tax_rates = self::find_shipping_rates( array( |
502
|
|
|
'country' => $country, |
503
|
|
|
'state' => $state, |
504
|
|
|
'postcode' => $postcode, |
505
|
|
|
'city' => $city |
506
|
|
|
) ); |
507
|
|
|
} |
508
|
|
|
} |
509
|
|
|
|
510
|
|
|
return $matched_tax_rates; |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
/** |
514
|
|
|
* Return true/false depending on if a rate is a compound rate. |
515
|
|
|
* |
516
|
|
|
* @param int key |
517
|
|
|
* @return bool |
518
|
|
|
*/ |
519
|
|
|
public static function is_compound( $key ) { |
520
|
|
|
global $wpdb; |
521
|
|
|
return $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_compound FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ) ? true : false; |
522
|
|
|
} |
523
|
|
|
|
524
|
|
|
/** |
525
|
|
|
* Return a given rates label. |
526
|
|
|
* |
527
|
|
|
* @param mixed $key_or_rate Tax rate ID, or the db row itself in object format |
528
|
|
|
* @return string |
529
|
|
|
*/ |
530
|
|
|
public static function get_rate_label( $key_or_rate ) { |
531
|
|
|
global $wpdb; |
532
|
|
|
|
533
|
|
View Code Duplication |
if ( is_object( $key_or_rate ) ) { |
|
|
|
|
534
|
|
|
$key = $key_or_rate->tax_rate_id; |
535
|
|
|
$rate_name = $key_or_rate->tax_rate_name; |
536
|
|
|
} else { |
537
|
|
|
$key = $key_or_rate; |
538
|
|
|
$rate_name = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
if ( ! $rate_name ) { |
542
|
|
|
$rate_name = WC()->countries->tax_or_vat(); |
543
|
|
|
} |
544
|
|
|
|
545
|
|
|
return apply_filters( 'woocommerce_rate_label', $rate_name, $key ); |
546
|
|
|
} |
547
|
|
|
|
548
|
|
|
/** |
549
|
|
|
* Return a given rates percent. |
550
|
|
|
* |
551
|
|
|
* @param mixed $key_or_rate Tax rate ID, or the db row itself in object format |
552
|
|
|
* @return string |
553
|
|
|
*/ |
554
|
|
|
public static function get_rate_percent( $key_or_rate ) { |
555
|
|
|
global $wpdb; |
556
|
|
|
|
557
|
|
View Code Duplication |
if ( is_object( $key_or_rate ) ) { |
|
|
|
|
558
|
|
|
$key = $key_or_rate->tax_rate_id; |
559
|
|
|
$tax_rate = $key_or_rate->tax_rate; |
560
|
|
|
} else { |
561
|
|
|
$key = $key_or_rate; |
562
|
|
|
$tax_rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); |
563
|
|
|
} |
564
|
|
|
|
565
|
|
|
return apply_filters( 'woocommerce_rate_percent', floatval( $tax_rate ) . '%', $key ); |
566
|
|
|
} |
567
|
|
|
|
568
|
|
|
/** |
569
|
|
|
* Get a rates code. Code is made up of COUNTRY-STATE-NAME-Priority. E.g GB-VAT-1, US-AL-TAX-1. |
570
|
|
|
* |
571
|
|
|
* @access public |
572
|
|
|
* @param mixed $key_or_rate Tax rate ID, or the db row itself in object format |
573
|
|
|
* @return string |
574
|
|
|
*/ |
575
|
|
|
public static function get_rate_code( $key_or_rate ) { |
576
|
|
|
global $wpdb; |
577
|
|
|
|
578
|
|
View Code Duplication |
if ( is_object( $key_or_rate ) ) { |
|
|
|
|
579
|
|
|
$key = $key_or_rate->tax_rate_id; |
580
|
|
|
$rate = $key_or_rate; |
581
|
|
|
} else { |
582
|
|
|
$key = $key_or_rate; |
583
|
|
|
$rate = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); |
584
|
|
|
} |
585
|
|
|
|
586
|
|
|
$code_string = ''; |
587
|
|
|
|
588
|
|
|
if ( null !== $rate ) { |
589
|
|
|
$code = array(); |
590
|
|
|
$code[] = $rate->tax_rate_country; |
591
|
|
|
$code[] = $rate->tax_rate_state; |
592
|
|
|
$code[] = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX'; |
593
|
|
|
$code[] = absint( $rate->tax_rate_priority ); |
594
|
|
|
$code_string = strtoupper( implode( '-', array_filter( $code ) ) ); |
595
|
|
|
} |
596
|
|
|
|
597
|
|
|
return apply_filters( 'woocommerce_rate_code', $code_string, $key ); |
598
|
|
|
} |
599
|
|
|
|
600
|
|
|
/** |
601
|
|
|
* Round tax lines and return the sum. |
602
|
|
|
* |
603
|
|
|
* @param array |
604
|
|
|
* @return float |
605
|
|
|
*/ |
606
|
|
|
public static function get_tax_total( $taxes ) { |
607
|
|
|
return array_sum( array_map( array( __CLASS__, 'round' ), $taxes ) ); |
608
|
|
|
} |
609
|
|
|
|
610
|
|
|
/** |
611
|
|
|
* Get store tax classes. |
612
|
|
|
* @return array |
613
|
|
|
*/ |
614
|
|
|
public static function get_tax_classes() { |
615
|
|
|
return array_filter( array_map( 'trim', explode( "\n", get_option( 'woocommerce_tax_classes' ) ) ) ); |
616
|
|
|
} |
617
|
|
|
|
618
|
|
|
/** |
619
|
|
|
* format the city. |
620
|
|
|
* @param string $city |
621
|
|
|
* @return string |
622
|
|
|
*/ |
623
|
|
|
private static function format_tax_rate_city( $city ) { |
624
|
|
|
return strtoupper( trim( $city ) ); |
625
|
|
|
} |
626
|
|
|
|
627
|
|
|
/** |
628
|
|
|
* format the state. |
629
|
|
|
* @param string $state |
630
|
|
|
* @return string |
631
|
|
|
*/ |
632
|
|
|
private static function format_tax_rate_state( $state ) { |
|
|
|
|
633
|
|
|
$state = strtoupper( $state ); |
634
|
|
|
return $state === '*' ? '' : $state; |
635
|
|
|
} |
636
|
|
|
|
637
|
|
|
/** |
638
|
|
|
* format the country. |
639
|
|
|
* @param string $country |
640
|
|
|
* @return string |
641
|
|
|
*/ |
642
|
|
|
private static function format_tax_rate_country( $country ) { |
|
|
|
|
643
|
|
|
$country = strtoupper( $country ); |
644
|
|
|
return $country === '*' ? '' : $country; |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
/** |
648
|
|
|
* format the tax rate name. |
649
|
|
|
* @param string $name |
650
|
|
|
* @return string |
651
|
|
|
*/ |
652
|
|
|
private static function format_tax_rate_name( $name ) { |
|
|
|
|
653
|
|
|
return $name ? $name : __( 'Tax', 'woocommerce' ); |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
/** |
657
|
|
|
* format the rate. |
658
|
|
|
* @param double $rate |
659
|
|
|
* @return string |
660
|
|
|
*/ |
661
|
|
|
private static function format_tax_rate( $rate ) { |
|
|
|
|
662
|
|
|
return number_format( (double) $rate, 4, '.', '' ); |
663
|
|
|
} |
664
|
|
|
|
665
|
|
|
/** |
666
|
|
|
* format the priority. |
667
|
|
|
* @param string $priority |
668
|
|
|
* @return int |
669
|
|
|
*/ |
670
|
|
|
private static function format_tax_rate_priority( $priority ) { |
|
|
|
|
671
|
|
|
return absint( $priority ); |
672
|
|
|
} |
673
|
|
|
|
674
|
|
|
/** |
675
|
|
|
* format the class. |
676
|
|
|
* @param string $class |
677
|
|
|
* @return string |
678
|
|
|
*/ |
679
|
|
|
public static function format_tax_rate_class( $class ) { |
680
|
|
|
$class = sanitize_title( $class ); |
681
|
|
|
$sanitized_classes = array_map( 'sanitize_title', self::get_tax_classes() ); |
682
|
|
|
if ( ! in_array( $class, $sanitized_classes ) ) { |
683
|
|
|
$class = ''; |
684
|
|
|
} |
685
|
|
|
return $class === 'standard' ? '' : $class; |
686
|
|
|
} |
687
|
|
|
|
688
|
|
|
/** |
689
|
|
|
* Prepare and format tax rate for DB insertion. |
690
|
|
|
* @param array $tax_rate |
691
|
|
|
* @return array |
692
|
|
|
*/ |
693
|
|
|
private static function prepare_tax_rate( $tax_rate ) { |
694
|
|
|
foreach ( $tax_rate as $key => $value ) { |
695
|
|
|
if ( method_exists( __CLASS__, 'format_' . $key ) ) { |
696
|
|
|
$tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), $value ); |
697
|
|
|
} |
698
|
|
|
} |
699
|
|
|
return $tax_rate; |
700
|
|
|
} |
701
|
|
|
|
702
|
|
|
/** |
703
|
|
|
* Insert a new tax rate. |
704
|
|
|
* |
705
|
|
|
* Internal use only. |
706
|
|
|
* |
707
|
|
|
* @since 2.3.0 |
708
|
|
|
* @access private |
709
|
|
|
* |
710
|
|
|
* @param array $tax_rate |
711
|
|
|
* |
712
|
|
|
* @return int tax rate id |
713
|
|
|
*/ |
714
|
|
|
public static function _insert_tax_rate( $tax_rate ) { |
715
|
|
|
global $wpdb; |
716
|
|
|
|
717
|
|
|
$wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', self::prepare_tax_rate( $tax_rate ) ); |
718
|
|
|
|
719
|
|
|
WC_Cache_Helper::incr_cache_prefix( 'taxes' ); |
720
|
|
|
|
721
|
|
|
do_action( 'woocommerce_tax_rate_added', $wpdb->insert_id, $tax_rate ); |
722
|
|
|
|
723
|
|
|
return $wpdb->insert_id; |
724
|
|
|
} |
725
|
|
|
|
726
|
|
|
/** |
727
|
|
|
* Get tax rate. |
728
|
|
|
* |
729
|
|
|
* Internal use only. |
730
|
|
|
* |
731
|
|
|
* @since 2.5.0 |
732
|
|
|
* @access private |
733
|
|
|
* |
734
|
|
|
* @param int $tax_rate_id |
735
|
|
|
* @param string $output_type |
736
|
|
|
* |
737
|
|
|
* @return array |
738
|
|
|
*/ |
739
|
|
|
public static function _get_tax_rate( $tax_rate_id, $output_type = ARRAY_A ) { |
740
|
|
|
global $wpdb; |
741
|
|
|
|
742
|
|
|
return $wpdb->get_row( $wpdb->prepare( " |
743
|
|
|
SELECT * |
744
|
|
|
FROM {$wpdb->prefix}woocommerce_tax_rates |
745
|
|
|
WHERE tax_rate_id = %d |
746
|
|
|
", $tax_rate_id ), $output_type ); |
747
|
|
|
} |
748
|
|
|
|
749
|
|
|
/** |
750
|
|
|
* Update a tax rate. |
751
|
|
|
* |
752
|
|
|
* Internal use only. |
753
|
|
|
* |
754
|
|
|
* @since 2.3.0 |
755
|
|
|
* @access private |
756
|
|
|
* |
757
|
|
|
* @param int $tax_rate_id |
758
|
|
|
* @param array $tax_rate |
759
|
|
|
*/ |
760
|
|
|
public static function _update_tax_rate( $tax_rate_id, $tax_rate ) { |
761
|
|
|
global $wpdb; |
762
|
|
|
|
763
|
|
|
$tax_rate_id = absint( $tax_rate_id ); |
764
|
|
|
|
765
|
|
|
$wpdb->update( |
766
|
|
|
$wpdb->prefix . "woocommerce_tax_rates", |
767
|
|
|
self::prepare_tax_rate( $tax_rate ), |
768
|
|
|
array( |
769
|
|
|
'tax_rate_id' => $tax_rate_id |
770
|
|
|
) |
771
|
|
|
); |
772
|
|
|
|
773
|
|
|
WC_Cache_Helper::incr_cache_prefix( 'taxes' ); |
774
|
|
|
|
775
|
|
|
do_action( 'woocommerce_tax_rate_updated', $tax_rate_id, $tax_rate ); |
776
|
|
|
} |
777
|
|
|
|
778
|
|
|
/** |
779
|
|
|
* Delete a tax rate from the database. |
780
|
|
|
* |
781
|
|
|
* Internal use only. |
782
|
|
|
* |
783
|
|
|
* @since 2.3.0 |
784
|
|
|
* @access private |
785
|
|
|
* |
786
|
|
|
* @param int $tax_rate_id |
787
|
|
|
*/ |
788
|
|
|
public static function _delete_tax_rate( $tax_rate_id ) { |
789
|
|
|
global $wpdb; |
790
|
|
|
|
791
|
|
|
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d;", $tax_rate_id ) ); |
792
|
|
|
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $tax_rate_id ) ); |
793
|
|
|
|
794
|
|
|
WC_Cache_Helper::incr_cache_prefix( 'taxes' ); |
795
|
|
|
|
796
|
|
|
do_action( 'woocommerce_tax_rate_deleted', $tax_rate_id ); |
797
|
|
|
} |
798
|
|
|
|
799
|
|
|
/** |
800
|
|
|
* Update postcodes for a tax rate in the DB. |
801
|
|
|
* |
802
|
|
|
* Internal use only. |
803
|
|
|
* |
804
|
|
|
* @since 2.3.0 |
805
|
|
|
* @access private |
806
|
|
|
* |
807
|
|
|
* @param int $tax_rate_id |
808
|
|
|
* @param string $postcodes String of postcodes separated by ; characters |
809
|
|
|
* @return string |
810
|
|
|
*/ |
811
|
|
|
public static function _update_tax_rate_postcodes( $tax_rate_id, $postcodes ) { |
812
|
|
|
if ( ! is_array( $postcodes ) ) { |
813
|
|
|
$postcodes = explode( ';', $postcodes ); |
814
|
|
|
} |
815
|
|
|
self::_update_tax_rate_locations( $tax_rate_id, array_diff( array_map( 'wc_normalize_postcode', array_filter( $postcodes ) ), array( '*' ) ), 'postcode' ); |
816
|
|
|
} |
817
|
|
|
|
818
|
|
|
/** |
819
|
|
|
* Update cities for a tax rate in the DB. |
820
|
|
|
* |
821
|
|
|
* Internal use only. |
822
|
|
|
* |
823
|
|
|
* @since 2.3.0 |
824
|
|
|
* @access private |
825
|
|
|
* |
826
|
|
|
* @param int $tax_rate_id |
827
|
|
|
* @param string $cities |
828
|
|
|
* @return string |
829
|
|
|
*/ |
830
|
|
|
public static function _update_tax_rate_cities( $tax_rate_id, $cities ) { |
831
|
|
|
if ( ! is_array( $cities ) ) { |
832
|
|
|
$cities = explode( ';', $cities ); |
833
|
|
|
} |
834
|
|
|
$cities = array_filter( array_diff( array_map( array( __CLASS__, 'format_tax_rate_city' ), $cities ), array( '*' ) ) ); |
835
|
|
|
|
836
|
|
|
self::_update_tax_rate_locations( $tax_rate_id, $cities, 'city' ); |
837
|
|
|
} |
838
|
|
|
|
839
|
|
|
/** |
840
|
|
|
* Updates locations (postcode and city). |
841
|
|
|
* |
842
|
|
|
* Internal use only. |
843
|
|
|
* |
844
|
|
|
* @since 2.3.0 |
845
|
|
|
* @access private |
846
|
|
|
* |
847
|
|
|
* @param int $tax_rate_id |
848
|
|
|
* @param string $type |
849
|
|
|
* @return string |
850
|
|
|
*/ |
851
|
|
|
private static function _update_tax_rate_locations( $tax_rate_id, $values, $type ) { |
852
|
|
|
global $wpdb; |
853
|
|
|
|
854
|
|
|
$tax_rate_id = absint( $tax_rate_id ); |
855
|
|
|
|
856
|
|
|
$wpdb->query( |
857
|
|
|
$wpdb->prepare( " |
858
|
|
|
DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d AND location_type = %s; |
859
|
|
|
", $tax_rate_id, $type |
860
|
|
|
) |
861
|
|
|
); |
862
|
|
|
|
863
|
|
|
if ( sizeof( $values ) > 0 ) { |
864
|
|
|
$sql = "( '" . implode( "', $tax_rate_id, '" . esc_sql( $type ) . "' ),( '", array_map( 'esc_sql', $values ) ) . "', $tax_rate_id, '" . esc_sql( $type ) . "' )"; |
865
|
|
|
|
866
|
|
|
$wpdb->query( " |
867
|
|
|
INSERT INTO {$wpdb->prefix}woocommerce_tax_rate_locations ( location_code, tax_rate_id, location_type ) VALUES $sql; |
868
|
|
|
" ); |
869
|
|
|
} |
870
|
|
|
|
871
|
|
|
WC_Cache_Helper::incr_cache_prefix( 'taxes' ); |
872
|
|
|
} |
873
|
|
|
|
874
|
|
|
/** |
875
|
|
|
* Used by admin settings page. |
876
|
|
|
* |
877
|
|
|
* @param string $tax_class |
878
|
|
|
* |
879
|
|
|
* @return array|null|object |
880
|
|
|
*/ |
881
|
|
|
public static function get_rates_for_tax_class( $tax_class ) { |
882
|
|
|
global $wpdb; |
883
|
|
|
|
884
|
|
|
// Get all the rates and locations. Snagging all at once should significantly cut down on the number of queries. |
885
|
|
|
$rates = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rates` WHERE `tax_rate_class` = %s ORDER BY `tax_rate_order`;", sanitize_title( $tax_class ) ) ); |
886
|
|
|
$locations = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rate_locations`" ); |
887
|
|
|
|
888
|
|
|
if ( ! empty( $rates ) ) { |
889
|
|
|
// Set the rates keys equal to their ids. |
890
|
|
|
$rates = array_combine( wp_list_pluck( $rates, 'tax_rate_id' ), $rates ); |
891
|
|
|
} |
892
|
|
|
|
893
|
|
|
// Drop the locations into the rates array. |
894
|
|
|
foreach ( $locations as $location ) { |
895
|
|
|
// Don't set them for unexistent rates. |
896
|
|
|
if ( ! isset( $rates[ $location->tax_rate_id ] ) ) { |
897
|
|
|
continue; |
898
|
|
|
} |
899
|
|
|
// If the rate exists, initialize the array before appending to it. |
900
|
|
|
if ( ! isset( $rates[ $location->tax_rate_id ]->{$location->location_type} ) ) { |
901
|
|
|
$rates[ $location->tax_rate_id ]->{$location->location_type} = array(); |
902
|
|
|
} |
903
|
|
|
$rates[ $location->tax_rate_id ]->{$location->location_type}[] = $location->location_code; |
904
|
|
|
} |
905
|
|
|
|
906
|
|
|
return $rates; |
907
|
|
|
} |
908
|
|
|
} |
909
|
|
|
WC_Tax::init(); |
910
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.