class-wc-cart-totals.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
  1. <?php
  2. /**
  3. * Cart totals calculation class.
  4. *
  5. * Methods are protected and class is final to keep this as an internal API.
  6. * May be opened in the future once structure is stable.
  7. *
  8. * Rounding guide:
  9. * - if something is being stored e.g. item total, store unrounded. This is so taxes can be recalculated later accurately.
  10. * - if calculating a total, round (if settings allow).
  11. *
  12. * @package WooCommerce/Classes
  13. * @version 3.2.0
  14. */
  15. if ( ! defined( 'ABSPATH' ) ) {
  16. exit;
  17. }
  18. /**
  19. * WC_Cart_Totals class.
  20. *
  21. * @since 3.2.0
  22. */
  23. final class WC_Cart_Totals {
  24. /**
  25. * Reference to cart object.
  26. *
  27. * @since 3.2.0
  28. * @var WC_Cart
  29. */
  30. protected $cart;
  31. /**
  32. * Reference to customer object.
  33. *
  34. * @since 3.2.0
  35. * @var array
  36. */
  37. protected $customer;
  38. /**
  39. * Line items to calculate.
  40. *
  41. * @since 3.2.0
  42. * @var array
  43. */
  44. protected $items = array();
  45. /**
  46. * Fees to calculate.
  47. *
  48. * @since 3.2.0
  49. * @var array
  50. */
  51. protected $fees = array();
  52. /**
  53. * Shipping costs.
  54. *
  55. * @since 3.2.0
  56. * @var array
  57. */
  58. protected $shipping = array();
  59. /**
  60. * Applied coupon objects.
  61. *
  62. * @since 3.2.0
  63. * @var array
  64. */
  65. protected $coupons = array();
  66. /**
  67. * Item/coupon discount totals.
  68. *
  69. * @since 3.2.0
  70. * @var array
  71. */
  72. protected $coupon_discount_totals = array();
  73. /**
  74. * Item/coupon discount tax totals.
  75. *
  76. * @since 3.2.0
  77. * @var array
  78. */
  79. protected $coupon_discount_tax_totals = array();
  80. /**
  81. * Should taxes be calculated?
  82. *
  83. * @var boolean
  84. */
  85. protected $calculate_tax = true;
  86. /**
  87. * Stores totals.
  88. *
  89. * @since 3.2.0
  90. * @var array
  91. */
  92. protected $totals = array(
  93. 'fees_total' => 0,
  94. 'fees_total_tax' => 0,
  95. 'items_subtotal' => 0,
  96. 'items_subtotal_tax' => 0,
  97. 'items_total' => 0,
  98. 'items_total_tax' => 0,
  99. 'total' => 0,
  100. 'shipping_total' => 0,
  101. 'shipping_tax_total' => 0,
  102. 'discounts_total' => 0,
  103. );
  104. /**
  105. * Sets up the items provided, and calculate totals.
  106. *
  107. * @since 3.2.0
  108. * @throws Exception If missing WC_Cart object.
  109. * @param WC_Cart $cart Cart object to calculate totals for.
  110. */
  111. public function __construct( &$cart = null ) {
  112. if ( ! is_a( $cart, 'WC_Cart' ) ) {
  113. throw new Exception( 'A valid WC_Cart object is required' );
  114. }
  115. $this->cart = $cart;
  116. $this->calculate_tax = wc_tax_enabled() && ! $cart->get_customer()->get_is_vat_exempt();
  117. $this->calculate();
  118. }
  119. /**
  120. * Run all calculations methods on the given items in sequence.
  121. *
  122. * @since 3.2.0
  123. */
  124. protected function calculate() {
  125. $this->calculate_item_totals();
  126. $this->calculate_shipping_totals();
  127. $this->calculate_fee_totals();
  128. $this->calculate_totals();
  129. }
  130. /**
  131. * Get default blank set of props used per item.
  132. *
  133. * @since 3.2.0
  134. * @return array
  135. */
  136. protected function get_default_item_props() {
  137. return (object) array(
  138. 'object' => null,
  139. 'tax_class' => '',
  140. 'taxable' => false,
  141. 'quantity' => 0,
  142. 'product' => false,
  143. 'price_includes_tax' => false,
  144. 'subtotal' => 0,
  145. 'subtotal_tax' => 0,
  146. 'total' => 0,
  147. 'total_tax' => 0,
  148. 'taxes' => array(),
  149. );
  150. }
  151. /**
  152. * Get default blank set of props used per fee.
  153. *
  154. * @since 3.2.0
  155. * @return array
  156. */
  157. protected function get_default_fee_props() {
  158. return (object) array(
  159. 'object' => null,
  160. 'tax_class' => '',
  161. 'taxable' => false,
  162. 'total_tax' => 0,
  163. 'taxes' => array(),
  164. );
  165. }
  166. /**
  167. * Get default blank set of props used per shipping row.
  168. *
  169. * @since 3.2.0
  170. * @return array
  171. */
  172. protected function get_default_shipping_props() {
  173. return (object) array(
  174. 'object' => null,
  175. 'tax_class' => '',
  176. 'taxable' => false,
  177. 'total' => 0,
  178. 'total_tax' => 0,
  179. 'taxes' => array(),
  180. );
  181. }
  182. /**
  183. * Should we round at subtotal level only?
  184. *
  185. * @return bool
  186. */
  187. protected function round_at_subtotal() {
  188. return 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
  189. }
  190. /**
  191. * Handles a cart or order object passed in for calculation. Normalises data
  192. * into the same format for use by this class.
  193. *
  194. * Each item is made up of the following props, in addition to those returned by get_default_item_props() for totals.
  195. * - key: An identifier for the item (cart item key or line item ID).
  196. * - cart_item: For carts, the cart item from the cart which may include custom data.
  197. * - quantity: The qty for this line.
  198. * - price: The line price in cents.
  199. * - product: The product object this cart item is for.
  200. *
  201. * @since 3.2.0
  202. */
  203. protected function get_items_from_cart() {
  204. $this->items = array();
  205. foreach ( $this->cart->get_cart() as $cart_item_key => $cart_item ) {
  206. $item = $this->get_default_item_props();
  207. $item->key = $cart_item_key;
  208. $item->object = $cart_item;
  209. $item->tax_class = $cart_item['data']->get_tax_class();
  210. $item->taxable = 'taxable' === $cart_item['data']->get_tax_status();
  211. $item->price_includes_tax = wc_prices_include_tax();
  212. $item->quantity = $cart_item['quantity'];
  213. $item->price = wc_add_number_precision_deep( $cart_item['data']->get_price() * $cart_item['quantity'] );
  214. $item->product = $cart_item['data'];
  215. $item->tax_rates = $this->get_item_tax_rates( $item );
  216. $this->items[ $cart_item_key ] = $item;
  217. }
  218. }
  219. /**
  220. * Get item costs grouped by tax class.
  221. *
  222. * @since 3.2.0
  223. * @return array
  224. */
  225. protected function get_tax_class_costs() {
  226. $item_tax_classes = wp_list_pluck( $this->items, 'tax_class' );
  227. $shipping_tax_classes = wp_list_pluck( $this->shipping, 'tax_class' );
  228. $fee_tax_classes = wp_list_pluck( $this->fees, 'tax_class' );
  229. $costs = array_fill_keys( $item_tax_classes + $shipping_tax_classes + $fee_tax_classes, 0 );
  230. $costs['non-taxable'] = 0;
  231. foreach ( $this->items + $this->fees + $this->shipping as $item ) {
  232. if ( 0 > $item->total ) {
  233. continue;
  234. }
  235. if ( ! $item->taxable ) {
  236. $costs['non-taxable'] += $item->total;
  237. } elseif ( 'inherit' === $item->tax_class ) {
  238. $costs[ reset( $item_tax_classes ) ] += $item->total;
  239. } else {
  240. $costs[ $item->tax_class ] += $item->total;
  241. }
  242. }
  243. return array_filter( $costs );
  244. }
  245. /**
  246. * Get fee objects from the cart. Normalises data
  247. * into the same format for use by this class.
  248. *
  249. * @since 3.2.0
  250. */
  251. protected function get_fees_from_cart() {
  252. $this->fees = array();
  253. $this->cart->calculate_fees();
  254. $fee_running_total = 0;
  255. foreach ( $this->cart->get_fees() as $fee_key => $fee_object ) {
  256. $fee = $this->get_default_fee_props();
  257. $fee->object = $fee_object;
  258. $fee->tax_class = $fee->object->tax_class;
  259. $fee->taxable = $fee->object->taxable;
  260. $fee->total = wc_add_number_precision_deep( $fee->object->amount );
  261. // Negative fees should not make the order total go negative.
  262. if ( 0 > $fee->total ) {
  263. $max_discount = round( $this->get_total( 'items_total', true ) + $fee_running_total + $this->get_total( 'shipping_total', true ) ) * -1;
  264. if ( $fee->total < $max_discount ) {
  265. $fee->total = $max_discount;
  266. }
  267. }
  268. $fee_running_total += $fee->total;
  269. if ( $this->calculate_tax ) {
  270. if ( 0 > $fee->total ) {
  271. // Negative fees should have the taxes split between all items so it works as a true discount.
  272. $tax_class_costs = $this->get_tax_class_costs();
  273. $total_cost = array_sum( $tax_class_costs );
  274. if ( $total_cost ) {
  275. foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) {
  276. if ( 'non-taxable' === $tax_class ) {
  277. continue;
  278. }
  279. $proportion = $tax_class_cost / $total_cost;
  280. $cart_discount_proportion = $fee->total * $proportion;
  281. $fee->taxes = wc_array_merge_recursive_numeric( $fee->taxes, WC_Tax::calc_tax( $fee->total * $proportion, WC_Tax::get_rates( $tax_class ) ) );
  282. }
  283. }
  284. } elseif ( $fee->object->taxable ) {
  285. $fee->taxes = WC_Tax::calc_tax( $fee->total, WC_Tax::get_rates( $fee->tax_class, $this->cart->get_customer() ), false );
  286. }
  287. }
  288. $fee->taxes = apply_filters( 'woocommerce_cart_totals_get_fees_from_cart_taxes', $fee->taxes, $fee, $this );
  289. $fee->total_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $fee->taxes ) );
  290. // Set totals within object.
  291. $fee->object->total = wc_remove_number_precision_deep( $fee->total );
  292. $fee->object->tax_data = wc_remove_number_precision_deep( $fee->taxes );
  293. $fee->object->tax = wc_remove_number_precision_deep( $fee->total_tax );
  294. $this->fees[ $fee_key ] = $fee;
  295. }
  296. }
  297. /**
  298. * Get shipping methods from the cart and normalise.
  299. *
  300. * @since 3.2.0
  301. */
  302. protected function get_shipping_from_cart() {
  303. $this->shipping = array();
  304. if ( ! $this->cart->show_shipping() ) {
  305. return;
  306. }
  307. foreach ( $this->cart->calculate_shipping() as $key => $shipping_object ) {
  308. $shipping_line = $this->get_default_shipping_props();
  309. $shipping_line->object = $shipping_object;
  310. $shipping_line->tax_class = get_option( 'woocommerce_shipping_tax_class' );
  311. $shipping_line->taxable = true;
  312. $shipping_line->total = wc_add_number_precision_deep( $shipping_object->cost );
  313. $shipping_line->taxes = wc_add_number_precision_deep( $shipping_object->taxes, false );
  314. $shipping_line->total_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $shipping_line->taxes ) );
  315. $this->shipping[ $key ] = $shipping_line;
  316. }
  317. }
  318. /**
  319. * Return array of coupon objects from the cart. Normalises data
  320. * into the same format for use by this class.
  321. *
  322. * @since 3.2.0
  323. */
  324. protected function get_coupons_from_cart() {
  325. $this->coupons = $this->cart->get_coupons();
  326. foreach ( $this->coupons as $coupon ) {
  327. switch ( $coupon->get_discount_type() ) {
  328. case 'fixed_product':
  329. $coupon->sort = 1;
  330. break;
  331. case 'percent':
  332. $coupon->sort = 2;
  333. break;
  334. case 'fixed_cart':
  335. $coupon->sort = 3;
  336. break;
  337. default:
  338. $coupon->sort = 0;
  339. break;
  340. }
  341. // Allow plugins to override the default order.
  342. $coupon->sort = apply_filters( 'woocommerce_coupon_sort', $coupon->sort, $coupon );
  343. }
  344. uasort( $this->coupons, array( $this, 'sort_coupons_callback' ) );
  345. }
  346. /**
  347. * Sort coupons so discounts apply consistently across installs.
  348. *
  349. * In order of priority;
  350. * - sort param
  351. * - usage restriction
  352. * - coupon value
  353. * - ID
  354. *
  355. * @param WC_Coupon $a Coupon object.
  356. * @param WC_Coupon $b Coupon object.
  357. * @return int
  358. */
  359. protected function sort_coupons_callback( $a, $b ) {
  360. if ( $a->sort === $b->sort ) {
  361. if ( $a->get_limit_usage_to_x_items() === $b->get_limit_usage_to_x_items() ) {
  362. if ( $a->get_amount() === $b->get_amount() ) {
  363. return $b->get_id() - $a->get_id();
  364. }
  365. return ( $a->get_amount() < $b->get_amount() ) ? -1 : 1;
  366. }
  367. return ( $a->get_limit_usage_to_x_items() < $b->get_limit_usage_to_x_items() ) ? -1 : 1;
  368. }
  369. return ( $a->sort < $b->sort ) ? -1 : 1;
  370. }
  371. /**
  372. * Ran to remove all base taxes from an item. Used when prices include tax, and the customer is tax exempt.
  373. *
  374. * @since 3.2.2
  375. * @param object $item Item to adjust the prices of.
  376. * @return object
  377. */
  378. protected function remove_item_base_taxes( $item ) {
  379. if ( $item->price_includes_tax && $item->taxable ) {
  380. $base_tax_rates = WC_Tax::get_base_tax_rates( $item->product->get_tax_class( 'unfiltered' ) );
  381. // Work out a new base price without the shop's base tax.
  382. $taxes = WC_Tax::calc_tax( $item->price, $base_tax_rates, true );
  383. // Now we have a new item price (excluding TAX).
  384. $item->price = round( $item->price - array_sum( $taxes ) );
  385. $item->price_includes_tax = false;
  386. }
  387. return $item;
  388. }
  389. /**
  390. * Only ran if woocommerce_adjust_non_base_location_prices is true.
  391. *
  392. * If the customer is outside of the base location, this removes the base
  393. * taxes. This is off by default unless the filter is used.
  394. *
  395. * Uses edit context so unfiltered tax class is returned.
  396. *
  397. * @since 3.2.0
  398. * @param object $item Item to adjust the prices of.
  399. * @return object
  400. */
  401. protected function adjust_non_base_location_price( $item ) {
  402. if ( $item->price_includes_tax && $item->taxable ) {
  403. $base_tax_rates = WC_Tax::get_base_tax_rates( $item->product->get_tax_class( 'unfiltered' ) );
  404. if ( $item->tax_rates !== $base_tax_rates ) {
  405. // Work out a new base price without the shop's base tax.
  406. $taxes = WC_Tax::calc_tax( $item->price, $base_tax_rates, true );
  407. $new_taxes = WC_Tax::calc_tax( $item->price - array_sum( $taxes ), $item->tax_rates, false );
  408. // Now we have a new item price.
  409. $item->price = round( $item->price - array_sum( $taxes ) + array_sum( $new_taxes ) );
  410. }
  411. }
  412. return $item;
  413. }
  414. /**
  415. * Get discounted price of an item with precision (in cents).
  416. *
  417. * @since 3.2.0
  418. * @param object $item_key Item to get the price of.
  419. * @return int
  420. */
  421. protected function get_discounted_price_in_cents( $item_key ) {
  422. $item = $this->items[ $item_key ];
  423. $price = isset( $this->coupon_discount_totals[ $item_key ] ) ? $item->price - $this->coupon_discount_totals[ $item_key ] : $item->price;
  424. return $price;
  425. }
  426. /**
  427. * Get tax rates for an item. Caches rates in class to avoid multiple look ups.
  428. *
  429. * @param object $item Item to get tax rates for.
  430. * @return array of taxes
  431. */
  432. protected function get_item_tax_rates( $item ) {
  433. if ( ! wc_tax_enabled() ) {
  434. return array();
  435. }
  436. $tax_class = $item->product->get_tax_class();
  437. return isset( $this->item_tax_rates[ $tax_class ] ) ? $this->item_tax_rates[ $tax_class ] : $this->item_tax_rates[ $tax_class ] = WC_Tax::get_rates( $item->product->get_tax_class(), $this->cart->get_customer() );
  438. }
  439. /**
  440. * Get item costs grouped by tax class.
  441. *
  442. * @since 3.2.0
  443. * @return array
  444. */
  445. protected function get_item_costs_by_tax_class() {
  446. $tax_classes = array(
  447. 'non-taxable' => 0,
  448. );
  449. foreach ( $this->items + $this->fees + $this->shipping as $item ) {
  450. if ( ! isset( $tax_classes[ $item->tax_class ] ) ) {
  451. $tax_classes[ $item->tax_class ] = 0;
  452. }
  453. if ( $item->taxable ) {
  454. $tax_classes[ $item->tax_class ] += $item->total;
  455. } else {
  456. $tax_classes['non-taxable'] += $item->total;
  457. }
  458. }
  459. return $tax_classes;
  460. }
  461. /**
  462. * Get a single total with or without precision (in cents).
  463. *
  464. * @since 3.2.0
  465. * @param string $key Total to get.
  466. * @param bool $in_cents Should the totals be returned in cents, or without precision.
  467. * @return int|float
  468. */
  469. public function get_total( $key = 'total', $in_cents = false ) {
  470. $totals = $this->get_totals( $in_cents );
  471. return isset( $totals[ $key ] ) ? $totals[ $key ] : 0;
  472. }
  473. /**
  474. * Set a single total.
  475. *
  476. * @since 3.2.0
  477. * @param string $key Total name you want to set.
  478. * @param int $total Total to set.
  479. */
  480. protected function set_total( $key = 'total', $total ) {
  481. $this->totals[ $key ] = $total;
  482. }
  483. /**
  484. * Get all totals with or without precision (in cents).
  485. *
  486. * @since 3.2.0
  487. * @param bool $in_cents Should the totals be returned in cents, or without precision.
  488. * @return array.
  489. */
  490. public function get_totals( $in_cents = false ) {
  491. return $in_cents ? $this->totals : wc_remove_number_precision_deep( $this->totals );
  492. }
  493. /**
  494. * Get taxes merged by type.
  495. *
  496. * @since 3.2.0
  497. * @param bool $in_cents If returned value should be in cents.
  498. * @param array|string $types Types to merge and return. Defaults to all.
  499. * @return array
  500. */
  501. protected function get_merged_taxes( $in_cents = false, $types = array( 'items', 'fees', 'shipping' ) ) {
  502. $items = array();
  503. $taxes = array();
  504. if ( is_string( $types ) ) {
  505. $types = array( $types );
  506. }
  507. foreach ( $types as $type ) {
  508. if ( isset( $this->$type ) ) {
  509. $items = array_merge( $items, $this->$type );
  510. }
  511. }
  512. foreach ( $items as $item ) {
  513. foreach ( $item->taxes as $rate_id => $rate ) {
  514. if ( ! isset( $taxes[ $rate_id ] ) ) {
  515. $taxes[ $rate_id ] = 0;
  516. }
  517. $taxes[ $rate_id ] += $this->round_line_tax( $rate );
  518. }
  519. }
  520. return $in_cents ? $taxes : wc_remove_number_precision_deep( $taxes );
  521. }
  522. /**
  523. * Combine item taxes into a single array, preserving keys.
  524. *
  525. * @since 3.2.0
  526. * @param array $item_taxes Taxes to combine.
  527. * @return array
  528. */
  529. protected function combine_item_taxes( $item_taxes ) {
  530. $merged_taxes = array();
  531. foreach ( $item_taxes as $taxes ) {
  532. foreach ( $taxes as $tax_id => $tax_amount ) {
  533. if ( ! isset( $merged_taxes[ $tax_id ] ) ) {
  534. $merged_taxes[ $tax_id ] = 0;
  535. }
  536. $merged_taxes[ $tax_id ] += $tax_amount;
  537. }
  538. }
  539. return $merged_taxes;
  540. }
  541. /*
  542. |--------------------------------------------------------------------------
  543. | Calculation methods.
  544. |--------------------------------------------------------------------------
  545. */
  546. /**
  547. * Calculate item totals.
  548. *
  549. * @since 3.2.0
  550. */
  551. protected function calculate_item_totals() {
  552. $this->get_items_from_cart();
  553. $this->calculate_item_subtotals();
  554. $this->calculate_discounts();
  555. foreach ( $this->items as $item_key => $item ) {
  556. $item->total = $this->get_discounted_price_in_cents( $item_key );
  557. $item->total_tax = 0;
  558. if ( has_filter( 'woocommerce_get_discounted_price' ) ) {
  559. /**
  560. * Allow plugins to filter this price like in the legacy cart class.
  561. *
  562. * This is legacy and should probably be deprecated in the future.
  563. * $item->object is the cart item object.
  564. * $this->cart is the cart object.
  565. */
  566. $item->total = wc_add_number_precision(
  567. apply_filters( 'woocommerce_get_discounted_price', wc_remove_number_precision( $item->total ), $item->object, $this->cart )
  568. );
  569. }
  570. if ( $this->calculate_tax && $item->product->is_taxable() ) {
  571. $total_taxes = apply_filters( 'woocommerce_calculate_item_totals_taxes', WC_Tax::calc_tax( $item->total, $item->tax_rates, $item->price_includes_tax ), $item, $this );
  572. $item->taxes = $total_taxes;
  573. $item->total_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $item->taxes ) );
  574. if ( $item->price_includes_tax ) {
  575. // Use unrounded taxes so we can re-calculate from the orders screen accurately later.
  576. $item->total = $item->total - array_sum( $item->taxes );
  577. }
  578. }
  579. $this->cart->cart_contents[ $item_key ]['line_tax_data']['total'] = wc_remove_number_precision_deep( $item->taxes );
  580. $this->cart->cart_contents[ $item_key ]['line_total'] = wc_remove_number_precision( $item->total );
  581. $this->cart->cart_contents[ $item_key ]['line_tax'] = wc_remove_number_precision( $item->total_tax );
  582. }
  583. $this->set_total( 'items_total', array_sum( array_map( 'round', array_values( wp_list_pluck( $this->items, 'total' ) ) ) ) );
  584. $this->set_total( 'items_total_tax', array_sum( array_values( wp_list_pluck( $this->items, 'total_tax' ) ) ) );
  585. $this->cart->set_cart_contents_total( $this->get_total( 'items_total' ) );
  586. $this->cart->set_cart_contents_tax( array_sum( $this->get_merged_taxes( false, 'items' ) ) );
  587. $this->cart->set_cart_contents_taxes( $this->get_merged_taxes( false, 'items' ) );
  588. }
  589. /**
  590. * Subtotals are costs before discounts.
  591. *
  592. * To prevent rounding issues we need to work with the inclusive price where possible.
  593. * otherwise we'll see errors such as when working with a 9.99 inc price, 20% VAT which would.
  594. * be 8.325 leading to totals being 1p off.
  595. *
  596. * Pre tax coupons come off the price the customer thinks they are paying - tax is calculated.
  597. * afterwards.
  598. *
  599. * e.g. $100 bike with $10 coupon = customer pays $90 and tax worked backwards from that.
  600. *
  601. * @since 3.2.0
  602. */
  603. protected function calculate_item_subtotals() {
  604. foreach ( $this->items as $item_key => $item ) {
  605. if ( $item->price_includes_tax ) {
  606. if ( $this->cart->get_customer()->get_is_vat_exempt() ) {
  607. $item = $this->remove_item_base_taxes( $item );
  608. } elseif ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
  609. $item = $this->adjust_non_base_location_price( $item );
  610. }
  611. }
  612. $item->subtotal = $item->price;
  613. $subtotal_taxes = array();
  614. if ( $this->calculate_tax && $item->product->is_taxable() ) {
  615. $subtotal_taxes = WC_Tax::calc_tax( $item->subtotal, $item->tax_rates, $item->price_includes_tax );
  616. $item->subtotal_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $subtotal_taxes ) );
  617. if ( $item->price_includes_tax ) {
  618. // Use unrounded taxes so we can re-calculate from the orders screen accurately later.
  619. $item->subtotal = $item->subtotal - array_sum( $subtotal_taxes );
  620. }
  621. }
  622. $this->cart->cart_contents[ $item_key ]['line_tax_data'] = array( 'subtotal' => wc_remove_number_precision_deep( $subtotal_taxes ) );
  623. $this->cart->cart_contents[ $item_key ]['line_subtotal'] = wc_remove_number_precision( $item->subtotal );
  624. $this->cart->cart_contents[ $item_key ]['line_subtotal_tax'] = wc_remove_number_precision( $item->subtotal_tax );
  625. }
  626. $this->set_total( 'items_subtotal', array_sum( array_map( 'round', array_values( wp_list_pluck( $this->items, 'subtotal' ) ) ) ) );
  627. $this->set_total( 'items_subtotal_tax', array_sum( array_values( wp_list_pluck( $this->items, 'subtotal_tax' ) ) ) );
  628. $this->cart->set_subtotal( $this->get_total( 'items_subtotal' ) );
  629. $this->cart->set_subtotal_tax( $this->get_total( 'items_subtotal_tax' ) );
  630. }
  631. /**
  632. * Calculate COUPON based discounts which change item prices.
  633. *
  634. * @since 3.2.0
  635. * @uses WC_Discounts class.
  636. */
  637. protected function calculate_discounts() {
  638. $this->get_coupons_from_cart();
  639. $discounts = new WC_Discounts( $this->cart );
  640. // Set items directly so the discounts class can see any tax adjustments made thus far using subtotals.
  641. $discounts->set_items( $this->items );
  642. foreach ( $this->coupons as $coupon ) {
  643. $discounts->apply_coupon( $coupon );
  644. }
  645. $coupon_discount_amounts = $discounts->get_discounts_by_coupon( true );
  646. $coupon_discount_tax_amounts = array();
  647. // See how much tax was 'discounted' per item and per coupon.
  648. if ( $this->calculate_tax ) {
  649. foreach ( $discounts->get_discounts( true ) as $coupon_code => $coupon_discounts ) {
  650. $coupon_discount_tax_amounts[ $coupon_code ] = 0;
  651. foreach ( $coupon_discounts as $item_key => $coupon_discount ) {
  652. $item = $this->items[ $item_key ];
  653. if ( $item->product->is_taxable() ) {
  654. // Item subtotals were sent, so set 3rd param.
  655. $item_tax = wc_round_tax_total( array_sum( WC_Tax::calc_tax( $coupon_discount, $item->tax_rates, $item->price_includes_tax ) ), 0 );
  656. // Sum total tax.
  657. $coupon_discount_tax_amounts[ $coupon_code ] += $item_tax;
  658. // Remove tax from discount total.
  659. if ( $item->price_includes_tax ) {
  660. $coupon_discount_amounts[ $coupon_code ] -= $item_tax;
  661. }
  662. }
  663. }
  664. }
  665. }
  666. $this->coupon_discount_totals = (array) $discounts->get_discounts_by_item( true );
  667. $this->coupon_discount_tax_totals = $coupon_discount_tax_amounts;
  668. if ( wc_prices_include_tax() ) {
  669. $this->set_total( 'discounts_total', array_sum( $this->coupon_discount_totals ) - array_sum( $this->coupon_discount_tax_totals ) );
  670. $this->set_total( 'discounts_tax_total', array_sum( $this->coupon_discount_tax_totals ) );
  671. } else {
  672. $this->set_total( 'discounts_total', array_sum( $this->coupon_discount_totals ) );
  673. $this->set_total( 'discounts_tax_total', array_sum( $this->coupon_discount_tax_totals ) );
  674. }
  675. $this->cart->set_coupon_discount_totals( wc_remove_number_precision_deep( $coupon_discount_amounts ) );
  676. $this->cart->set_coupon_discount_tax_totals( wc_remove_number_precision_deep( $coupon_discount_tax_amounts ) );
  677. // Add totals to cart object. Note: Discount total for cart is excl tax.
  678. $this->cart->set_discount_total( $this->get_total( 'discounts_total' ) );
  679. $this->cart->set_discount_tax( $this->get_total( 'discounts_tax_total' ) );
  680. }
  681. /**
  682. * Triggers the cart fees API, grabs the list of fees, and calculates taxes.
  683. *
  684. * Note: This class sets the totals for the 'object' as they are calculated. This is so that APIs like the fees API can see these totals if needed.
  685. *
  686. * @since 3.2.0
  687. */
  688. protected function calculate_fee_totals() {
  689. $this->get_fees_from_cart();
  690. $this->set_total( 'fees_total', array_sum( wp_list_pluck( $this->fees, 'total' ) ) );
  691. $this->set_total( 'fees_total_tax', array_sum( wp_list_pluck( $this->fees, 'total_tax' ) ) );
  692. $this->cart->fees_api()->set_fees( wp_list_pluck( $this->fees, 'object' ) );
  693. $this->cart->set_fee_total( wc_remove_number_precision_deep( array_sum( wp_list_pluck( $this->fees, 'total' ) ) ) );
  694. $this->cart->set_fee_tax( wc_remove_number_precision_deep( array_sum( wp_list_pluck( $this->fees, 'total_tax' ) ) ) );
  695. $this->cart->set_fee_taxes( wc_remove_number_precision_deep( $this->combine_item_taxes( wp_list_pluck( $this->fees, 'taxes' ) ) ) );
  696. }
  697. /**
  698. * Calculate any shipping taxes.
  699. *
  700. * @since 3.2.0
  701. */
  702. protected function calculate_shipping_totals() {
  703. $this->get_shipping_from_cart();
  704. $this->set_total( 'shipping_total', array_sum( wp_list_pluck( $this->shipping, 'total' ) ) );
  705. $this->set_total( 'shipping_tax_total', array_sum( wp_list_pluck( $this->shipping, 'total_tax' ) ) );
  706. $this->cart->set_shipping_total( $this->get_total( 'shipping_total' ) );
  707. $this->cart->set_shipping_tax( $this->get_total( 'shipping_tax_total' ) );
  708. $this->cart->set_shipping_taxes( wc_remove_number_precision_deep( $this->combine_item_taxes( wp_list_pluck( $this->shipping, 'taxes' ) ) ) );
  709. }
  710. /**
  711. * Main cart totals.
  712. *
  713. * @since 3.2.0
  714. */
  715. protected function calculate_totals() {
  716. $this->set_total( 'total', round( $this->get_total( 'items_total', true ) + $this->get_total( 'fees_total', true ) + $this->get_total( 'shipping_total', true ) + array_sum( $this->get_merged_taxes( true ) ), 0 ) );
  717. $this->cart->set_total_tax( array_sum( $this->get_merged_taxes( false ) ) );
  718. // Allow plugins to hook and alter totals before final total is calculated.
  719. if ( has_action( 'woocommerce_calculate_totals' ) ) {
  720. do_action( 'woocommerce_calculate_totals', $this->cart );
  721. }
  722. // Allow plugins to filter the grand total, and sum the cart totals in case of modifications.
  723. $this->cart->set_total( max( 0, apply_filters( 'woocommerce_calculated_total', $this->get_total( 'total' ), $this->cart ) ) );
  724. }
  725. /**
  726. * Apply rounding to an array of taxes before summing. Rounds to store DP setting, ignoring precision.
  727. *
  728. * @since 3.2.6
  729. * @param float $value Tax value.
  730. * @return float
  731. */
  732. protected function round_line_tax( $value ) {
  733. if ( ! $this->round_at_subtotal() ) {
  734. $value = wc_round_tax_total( $value, 0 );
  735. }
  736. return $value;
  737. }
  738. }