AeliaWCEU_VAT_AssistantOrder

Extends the Aelia Order class to add convenience methods to store and retrieve EU VAT evidence.

Defined (1)

The class is defined in the following location(s).

/src/lib/classes/order/order.php  
  1. class Order extends \Aelia\WC\Order { 
  2. // @var string The meta key that will hold the data about the VAT applied to the order. 
  3. const META_EU_VAT_DATA = '_eu_vat_data'; 
  4. // @var string The meta key that will hold the VAT evidence required by EU regulations. 
  5. const META_EU_VAT_EVIDENCE = '_eu_vat_evidence'; 
  6.  
  7. /** 
  8. * Convenience method to access the settings controller. 
  9. * @return \Aelia\WC\EU_VAT_Assistant\Settings 
  10. */ 
  11. public static function settings() { 
  12. return WC_Aelia_EU_VAT_Assistant::settings(); 
  13.  
  14. /** 
  15. * Stores the evidence required by EU VAT compliance regulations. 
  16. * @return array The VAT evidence stored against the order. 
  17. */ 
  18. public function store_vat_evidence() { 
  19. $euva_instance = WC_Aelia_EU_VAT_Assistant::instance(); 
  20.  
  21. $vat_evidence = array( 
  22. // "Sign" the VAT data, so that we can determine which plugin version created 
  23. // it and how to update it, if needed 
  24. 'eu_vat_assistant_version' => WC_Aelia_EU_VAT_Assistant::$version,  
  25. 'location' => array( 
  26. 'is_eu_country' => (int)$euva_instance->is_eu_country($this->get_billing_country()),  
  27. 'billing_country' => $this->get_billing_country(),  
  28. 'shipping_country' => $this->get_shipping_country(),  
  29. 'customer_ip_address' => $this->get_customer_ip_address(),  
  30. 'customer_ip_address_country' => IP2Location::factory()->get_country_code($this->get_customer_ip_address()),  
  31. 'self_certified' => ($this->get_meta('_customer_location_self_certified')) == 'yes' ? 'yes' : 'no',  
  32. ),  
  33. 'exemption' => array( 
  34. 'vat_number' => $this->get_customer_vat_number(),  
  35. 'vat_country' => $this->get_meta('_vat_country'),  
  36. 'vat_number_validated' => $this->get_meta('_vat_number_validated'),  
  37. ),  
  38. ); 
  39. $vat_evidence = apply_filters('wc_aelia_eu_vat_assistant_store_vat_evidence', $vat_evidence, $this); 
  40.  
  41. // Debug 
  42. //var_dump($vat_evidence);die(); 
  43.  
  44. $this->set_meta(self::META_EU_VAT_EVIDENCE, $vat_evidence); 
  45.  
  46. return $vat_evidence; 
  47.  
  48. /** 
  49. * Returns the EU VAT evidence stored against the order. 
  50. * @return array The VAT evidence stored against the order. 
  51. */ 
  52. public function get_vat_evidence() { 
  53. return $this->get_meta(self::META_EU_VAT_EVIDENCE); 
  54.  
  55. /** 
  56. * Populates an array of tax descriptors with additional information about the 
  57. * tax, such as the rate applied, the class, etc. 
  58. * @param array taxes An array of tax descriptors, as returned by WC_Order::get_taxes(). 
  59. * @return array The array of tax descriptors, with the additional tax information. 
  60. * @see \WC_Order::get_taxes() 
  61. * @deprecated since WC 3.0 
  62. */ 
  63. protected function legacy_add_tax_rates_details(array $taxes) { 
  64. global $wpdb; 
  65.  
  66. if(empty($taxes)) { 
  67. return $taxes; 
  68.  
  69. // Debug 
  70. //var_dump($taxes); 
  71.  
  72. $tax_rate_ids = array(); 
  73. foreach($taxes as $order_tax_id => $tax) { 
  74. // Keep track of which tax ID corresponds to which ID within the order. 
  75. // This information will be used to add the new information to the correct 
  76. // elements in the $taxes array 
  77. $tax_rate_ids[(int)$tax['rate_id']] = $order_tax_id; 
  78.  
  79. $SQL = " 
  80. SELECT 
  81. TR.tax_rate_id 
  82. , TR.tax_rate 
  83. , TR.tax_rate_class 
  84. , TR.tax_rate_country 
  85. , COALESCE(TR.tax_payable_to_country, TR.tax_rate_country) AS tax_payable_to_country 
  86. FROM 
  87. {$wpdb->prefix}woocommerce_tax_rates TR 
  88. WHERE 
  89. (TR.tax_rate_id IN (%s)) 
  90. "; 
  91. // We cannot use $wpdb::prepare(). We need the result of the implode() 
  92. // call to be injected as is, while the prepare() method would wrap it in quotes. 
  93. $SQL = sprintf($SQL, implode(', ', array_keys($tax_rate_ids))); 
  94.  
  95. // Populate the original tax array with the tax details 
  96. $tax_rates_info = $wpdb->get_results($SQL, ARRAY_A); 
  97. foreach($tax_rates_info as $tax_rate_info) { 
  98. // Find to which item the details belong, amongst the order taxes 
  99. $order_tax_id = (int)$tax_rate_ids[$tax_rate_info['tax_rate_id']]; 
  100. $taxes[$order_tax_id]['tax_rate'] = $tax_rate_info['tax_rate']; 
  101. // Note: an empty tax rate class is not an error. It simply represents the 
  102. // "Standard" class 
  103. $taxes[$order_tax_id]['tax_rate_class'] = $tax_rate_info['tax_rate_class']; 
  104. $taxes[$order_tax_id]['tax_rate_country'] = $tax_rate_info['tax_rate_country']; 
  105. $taxes[$order_tax_id]['tax_payable_to_country'] = $tax_rate_info['tax_payable_to_country']; 
  106.  
  107. // Attach the rest tax information to the original array, for convenience 
  108. $taxes[$order_tax_id]['tax_info'] = $tax_rate_info; 
  109. // Debug 
  110. //var_dump($taxes);die(); 
  111. return $taxes; 
  112.  
  113. /** 
  114. * Populates an array of tax descriptors with additional information about the 
  115. * tax, such as the rate applied, the class, etc. 
  116. * @param array taxes An array of tax descriptors, as returned by WC_Order::get_taxes(). 
  117. * @return array The array of tax descriptors, with the additional tax information. 
  118. * @see \WC_Order::get_taxes() 
  119. * @since WC 3.0 
  120. */ 
  121. protected function add_tax_rates_details(array $taxes) { 
  122. global $wpdb; 
  123.  
  124. if(empty($taxes)) { 
  125. return $taxes; 
  126.  
  127. // Debug 
  128. //var_dump($taxes); 
  129.  
  130. $tax_rate_ids = array(); 
  131. $result = array(); 
  132. foreach($taxes as $order_tax_id => $tax) { 
  133. // Keep track of which tax ID corresponds to which ID within the order. 
  134. // This information will be used to add the new information to the correct 
  135. // elements in the $taxes array 
  136. $tax_rate_ids[(int)$tax->get_rate_id()] = $order_tax_id; 
  137.  
  138. $SQL = " 
  139. SELECT 
  140. TR.tax_rate_id 
  141. , TR.tax_rate 
  142. , TR.tax_rate_class 
  143. , TR.tax_rate_country 
  144. , COALESCE(TR.tax_payable_to_country, TR.tax_rate_country) AS tax_payable_to_country 
  145. FROM 
  146. {$wpdb->prefix}woocommerce_tax_rates TR 
  147. WHERE 
  148. (TR.tax_rate_id IN (%s)) 
  149. "; 
  150. // We cannot use $wpdb::prepare(). We need the result of the implode() 
  151. // call to be injected as is, while the prepare() method would wrap it in quotes. 
  152. $SQL = sprintf($SQL, implode(', ', array_keys($tax_rate_ids))); 
  153.  
  154. // Populate the original tax array with the tax details 
  155. $tax_rates_info = $wpdb->get_results($SQL, ARRAY_A); 
  156. foreach($tax_rates_info as $tax_rate_info) { 
  157. // Find to which item the details belong, amongst the order taxes 
  158. $order_tax_id = (int)$tax_rate_ids[$tax_rate_info['tax_rate_id']]; 
  159.  
  160. $result[$order_tax_id] = array( 
  161. 'rate_id' => $tax->get_rate_id(),  
  162. 'label' => $tax->get_label(),  
  163. 'tax_rate' => $tax_rate_info['tax_rate'],  
  164. // Note: an empty tax rate class is not an error. It simply represents the 
  165. // "Standard" class 
  166. 'tax_rate_class' => $tax_rate_info['tax_rate_class'],  
  167. 'tax_rate_country' => $tax_rate_info['tax_rate_country'],  
  168. 'tax_payable_to_country' => $tax_rate_info['tax_payable_to_country'],  
  169. // Attach the rest tax information to the original array, for convenience 
  170. 'tax_info' => $tax_rate_info,  
  171. ); 
  172. // Debug 
  173. //var_dump($result);die(); 
  174. return $result; 
  175.  
  176. /** 
  177. * Adds tax rate details to the tax data associated with the order. This method 
  178. * takes a "snapshot" of the tax details active at the moment in which the order 
  179. * was placed, so that further changes to them, made after the order, won't 
  180. * impact on the reports. 
  181. * @param array vat_data An array of data produced by Order::update_vat_data() 
  182. * @return array The array of VAT data including the details of the tax rates. 
  183. * @since 0.9.9.141223 
  184. * @see Order::update_vat_data() 
  185. */ 
  186. public function add_tax_rates_data(array $vat_data) { 
  187. // Get order taxes details 
  188. if(aelia_wc_version_is('>=', '3.0')) { 
  189. $taxes = $this->add_tax_rates_details($this->get_taxes()); 
  190. else { 
  191. $taxes = $this->legacy_add_tax_rates_details($this->get_taxes()); 
  192.  
  193. $taxes_data = array(); 
  194. // Debug 
  195. //var_dump($taxes);die(); 
  196. // Save the information about each VAT rate applied to the order 
  197. foreach($taxes as $tax) { 
  198. if(!$this->is_valid_vat($tax)) { 
  199. continue; 
  200.  
  201. $tax_rate_id = $tax['rate_id']; 
  202. if(!isset($taxes_data[$tax_rate_id])) { 
  203. $taxes_data[$tax_rate_id] = array( 
  204. 'label' => $tax['label'],  
  205. 'vat_rate' => $tax['tax_rate'],  
  206. 'country' => $tax['tax_rate_country'],  
  207. 'tax_rate_class' => $tax['tax_rate_class'],  
  208. 'tax_payable_to_country' => $tax['tax_payable_to_country'],  
  209. ); 
  210. $vat_data['taxes'] = $taxes_data; 
  211. return $vat_data; 
  212.  
  213. /** 
  214. * Stores some basic VAT details, which don't depend on the fact that VAT was 
  215. * actually applied to the order. Such information will be useful for reporting 
  216. * purposes. 
  217. * @return array The VAT data associated to the order. 
  218. */ 
  219. protected function update_basic_vat_data() { 
  220. $rounding_decimals = self::settings()->get(Settings::FIELD_VAT_ROUNDING_DECIMALS); 
  221. $order_currency = $this->get_currency(); 
  222. $vat_currency = self::settings()->vat_currency(); 
  223.  
  224. $vat_data = $this->get_vat_data(); 
  225. // If VAT data is lacking an exchange rate, then it was not populated yet 
  226. // In sucj case, populate it with some defaults. If already populated,  
  227. // leave the data that was already there 
  228. if(empty($vat_data['vat_currency_exchange_rate'])) { 
  229. $vat_data = array( 
  230. 'invoice_currency' => $order_currency,  
  231. 'vat_currency' => $vat_currency,  
  232. 'vat_currency_exchange_rate' => apply_filters('wc_aelia_eu_vat_assistant_convert',  
  233. (float)1.00,  
  234. $order_currency,  
  235. $vat_currency,  
  236. $rounding_decimals),  
  237. 'vat_currency_exchange_rate_timestamp' => $this->settings()->get(Settings::FIELD_EXCHANGE_RATES_LAST_UPDATE),  
  238. 'exchange_rates_provider_label' => $this->settings()->get_current_exchange_rates_provider_label(),  
  239. ); 
  240. return $vat_data; 
  241.  
  242. /** 
  243. * Extracts the tax data from an instance of WC_Order_Item_Tax. 
  244. * @param WC_Order_Item_Tax tax A tax item instance. 
  245. * @return array 
  246. * @since 1.7.6.170415 
  247. */ 
  248. protected function extract_tax_data($tax) { 
  249. return array( 
  250. 'rate_id' => $tax->get_rate_id(),  
  251. 'tax_amount' => $tax->get_tax_total(),  
  252. 'shipping_tax_amount' => $tax->get_shipping_tax_total(),  
  253. ); 
  254.  
  255. /** 
  256. * Processes the order information to generate the data about the VAT applied 
  257. * to the order, and stores it in order's metadata. 
  258. * @return array An array with the details of the VAT applied to the order. 
  259. */ 
  260. public function update_vat_data() { 
  261. // Store basic VAT information that apply whether VAT was added or not to the 
  262. // order 
  263. $vat_data = $this->update_basic_vat_data(); 
  264.  
  265. $taxes = $this->get_taxes(); 
  266. if(is_array($taxes)) { 
  267. /** Add tax details, such as labels and rates. These details must be saved 
  268. * with the order because they can change later on. Admins can change tax 
  269. * rates and labels in WooCommerce > Tax Settings, thus making the tax rate 
  270. * ID a useless information. 
  271. */ 
  272. $vat_data = $this->add_tax_rates_data($vat_data); 
  273.  
  274. // Calculate totals, in order currency, for each VAT rate 
  275. foreach($taxes as $tax) { 
  276. // WooCommerce 3.0 passes an instance of WC_Order_Item_Tax instead of an 
  277. // array, so we need to extract the required data from it 
  278. if(aelia_wc_version_is('>=', '3.0')) { 
  279. $tax = $this->extract_tax_data($tax); 
  280.  
  281. $tax_rate_id = $tax['rate_id']; 
  282. if(!isset($vat_data['taxes'][$tax_rate_id]['amounts'])) { 
  283. $vat_data['taxes'][$tax_rate_id]['amounts'] = array( 
  284. 'items_total' => 0,  
  285. 'shipping_total' => 0,  
  286. ); 
  287.  
  288. // Debug 
  289. //var_dump("TAX DATA", $tax);die(); 
  290.  
  291. $vat_data['taxes'][$tax_rate_id]['amounts']['items_total'] += get_value('tax_amount', $tax, 0); 
  292. $vat_data['taxes'][$tax_rate_id]['amounts']['shipping_total'] += get_value('shipping_tax_amount', $tax, 0); 
  293. // "Sign" the VAT data, so that we can determine which plugin version created 
  294. // it and how to update it, if needed 
  295. $vat_data['eu_vat_assistant_version'] = WC_Aelia_EU_VAT_Assistant::$version; 
  296. // Remove old version signature 
  297. if(isset($vat_data['aelia_euva_version'])) { 
  298. unset($vat_data['aelia_euva_version']); 
  299. // Sort VAT data by key. It's easier to debug discrepancies when the fields 
  300. // are always in the same order 
  301. ksort($vat_data); 
  302.  
  303. // Debug 
  304. //var_dump($vat_data);die(); 
  305.  
  306. // Allow 3rd parties to add more data, if needed 
  307. $vat_data = apply_filters('wc_aelia_eu_vat_assistant_set_order_vat_data', $vat_data, $this); 
  308. $this->set_meta(self::META_EU_VAT_DATA, $vat_data); 
  309.  
  310. return $vat_data; 
  311.  
  312. /** 
  313. * Indicates if the tax is a VAT. 
  314. * @param array tax A tax descriptor. 
  315. * @return bool True if the tax is a VAT, false if it's another type of tax. 
  316. */ 
  317. protected function tax_is_vat($tax) { 
  318. // TODO Implement logic to determine if a tax is a VAT. 
  319. /** Suggestion by D.Anderson 
  320. * The logic used to determine it could simply be comparison of the tax label 
  321. * with a list of labels that indicate VATs. If the tax label matches, then 
  322. * it's considered a VAT. If not, then it's considered another tax type. 
  323. */ 
  324. return true; 
  325.  
  326. /** 
  327. * Indicates if the tax is a valid VAT. 
  328. * @param array tax A tax descriptor. 
  329. * @return bool 
  330. */ 
  331. protected function is_valid_vat($tax) { 
  332. return is_array($tax) && !empty($tax['label']) && $this->tax_is_vat($tax); 
  333.  
  334. /** 
  335. * Returns the VAT totals paid with the order. This method relies on the __get() 
  336. * magic method introduced in WooCommerce 2.1, which retrieves post meta automatically. 
  337. * @param string key If specified, the method will only return the VAT data 
  338. * identified by the key. If left empty, the whole VAT data object is returned. 
  339. * @return array|false 
  340. * @see \WC_Order::__get() 
  341. */ 
  342. public function get_vat_data($key = null) { 
  343. // Get the tax data 
  344. $vat_data = $this->get_meta(self::META_EU_VAT_DATA); 
  345.  
  346. if(empty($vat_data)) { 
  347. return $vat_data; 
  348.  
  349. // Step 1 - Retrieve VAT refunds, they will be added to the totals 
  350. $vat_refunds = $this->get_vat_refunds(array_keys(get_value('taxes', $vat_data, array()))); 
  351.  
  352. // Step 2 - Calculate the totals for each VAT rate and the grand totals 
  353. $vat_grand_totals = array( 
  354. 'items_total' => 0,  
  355. 'shipping_total' => 0,  
  356. 'items_refund' => 0,  
  357. 'shipping_refund' => 0,  
  358. 'total' => 0,  
  359. ); 
  360. if(empty($vat_data['taxes'])) { 
  361. $vat_data['taxes'] = array(); 
  362. foreach($vat_data['taxes'] as $tax_rate_id => $vat_info) { 
  363. if(!empty($vat_refunds[$tax_rate_id])) { 
  364. $vat_amounts = array_merge($vat_info['amounts'], $vat_refunds[$tax_rate_id]); 
  365. else { 
  366. $vat_amounts = $vat_info['amounts']; 
  367.  
  368. $vat_amounts['total'] = $vat_amounts['items_total'] + $vat_amounts['shipping_total']; 
  369.  
  370. if(!empty($vat_refunds)) { 
  371. $vat_amounts['total'] = $vat_amounts['total'] 
  372. - $vat_amounts['items_refund'] 
  373. - $vat_amounts['shipping_refund']; 
  374.  
  375. // Attach the totals to the VAT item 
  376. $vat_data['taxes'][$tax_rate_id]['amounts'] = $vat_amounts; 
  377.  
  378. // Update the grand totals as well 
  379. foreach($vat_amounts as $vat_key => $vat_amount) { 
  380. $vat_grand_totals[$vat_key] += $vat_amount; 
  381. $vat_data['totals'] = $vat_grand_totals; 
  382.  
  383. // Debug 
  384. //var_dump($vat_data);die(); 
  385.  
  386. // Allow 3rd parties to add more data, if needed 
  387. $vat_data = apply_filters('wc_aelia_eu_vat_assistant_get_order_vat_data', $vat_data, $this); 
  388.  
  389. // If a key was specified, return only the VAT data associated to that key 
  390. if(!empty($key)) { 
  391. return get_value($key, $vat_data, false); 
  392. return $vat_data; 
  393.  
  394. /** 
  395. * Returns the refunds stored against the orders for a given set of tax IDs. 
  396. * @param array tax_ids A list of tax ids. 
  397. * @ return array A list of tax id => refund total pairs. 
  398. */ 
  399. public function get_vat_refunds(array $tax_ids) { 
  400. // Refunds are supported only from WooCommerce 2.2 onwards 
  401. if(!method_exists($this, 'get_refunds')) { 
  402. return array(); 
  403.  
  404. $totals = array( 
  405. 'totals' => array( 
  406. 'items_refund' => 0,  
  407. 'shipping_refund' => 0,  
  408. ),  
  409. ); 
  410. // No point in doing any work if there are no Tax IDs to process 
  411. if(empty($tax_ids)) { 
  412. return $totals; 
  413.  
  414. foreach($tax_ids as $tax_id) { 
  415. $totals[$tax_id] = array( 
  416. 'items_refund' => 0,  
  417. 'shipping_refund' => 0,  
  418. ); 
  419.  
  420. $item_types = array('line_item', 'shipping'); 
  421. foreach($this->get_refunds() as $refund) { 
  422. foreach($refund->get_items($item_types) as $refunded_item) { 
  423. if(isset($refunded_item['refunded_item_id'])) { 
  424. switch($refunded_item['type']) { 
  425. case 'shipping': 
  426. $tax_data = maybe_unserialize($refunded_item['taxes']); 
  427. $refund_type = 'shipping_refund'; 
  428. break; 
  429. default: 
  430. $tax_data = maybe_unserialize($refunded_item['line_tax_data']); 
  431. $tax_data = $tax_data['total']; 
  432. $refund_type = 'items_refund'; 
  433. break; 
  434. // Update the totals for each tax ID 
  435. foreach($tax_data as $tax_id => $tax_amount) { 
  436. if(isset($totals[$tax_id])) { 
  437. $totals[$tax_id][$refund_type] += $tax_amount; 
  438. // Make the totals positive (they are stored as negative numbers) 
  439. foreach($totals as $tax_id => $amounts) { 
  440. foreach($amounts as $refund_type => $value) { 
  441. $totals[$tax_id][$refund_type] = $value * -1; 
  442. $totals['totals'][$refund_type] += $totals[$tax_id][$refund_type]; 
  443. return $totals;