/includes/class-wc-tax.php

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