Açıklama Yok

class-wc-cart-totals.php 28KB

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