diff --git a/app/code/Magento/Sales/etc/extension_attributes.xml b/app/code/Magento/Sales/etc/extension_attributes.xml deleted file mode 100644 index 970a003625928f218f2073cbb4262cd7d3876499..0000000000000000000000000000000000000000 --- a/app/code/Magento/Sales/etc/extension_attributes.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © 2015 Magento. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Api/etc/extension_attributes.xsd"> - <extension_attributes for="Magento\Sales\Api\Data\OrderInterface"> - <attribute code="applied_taxes" type="Magento\Tax\Api\Data\OrderTaxDetailsAppliedTaxInterface[]" /> - <attribute code="item_applied_taxes" type="Magento\Tax\Api\Data\OrderTaxDetailsItemInterface[]" /> - <attribute code="converting_from_quote" type="boolean" /> - </extension_attributes> -</config> diff --git a/app/code/Magento/Tax/Model/Observer.php b/app/code/Magento/Tax/Model/Observer.php index 6f8d41fbb17e96ece0bcaad9c87cb57e14498fac..25bf3ea1b56461e019947d147ca109ca6297151a 100644 --- a/app/code/Magento/Tax/Model/Observer.php +++ b/app/code/Magento/Tax/Model/Observer.php @@ -20,16 +20,6 @@ class Observer */ protected $_taxData; - /** - * @var \Magento\Tax\Model\Sales\Order\TaxFactory - */ - protected $_orderTaxFactory; - - /** - * @var \Magento\Sales\Model\Order\Tax\ItemFactory - */ - protected $_taxItemFactory; - /** * @var \Magento\Tax\Model\Calculation */ @@ -57,8 +47,6 @@ class Observer /** * @param \Magento\Tax\Helper\Data $taxData - * @param \Magento\Tax\Model\Sales\Order\TaxFactory $orderTaxFactory - * @param \Magento\Sales\Model\Order\Tax\ItemFactory $taxItemFactory * @param \Magento\Tax\Model\Calculation $calculation * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Tax\Model\Resource\Report\TaxFactory $reportTaxFactory @@ -67,8 +55,6 @@ class Observer */ public function __construct( \Magento\Tax\Helper\Data $taxData, - \Magento\Tax\Model\Sales\Order\TaxFactory $orderTaxFactory, - \Magento\Sales\Model\Order\Tax\ItemFactory $taxItemFactory, \Magento\Tax\Model\Calculation $calculation, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, \Magento\Tax\Model\Resource\Report\TaxFactory $reportTaxFactory, @@ -76,8 +62,6 @@ class Observer \Magento\Framework\Registry $registry ) { $this->_taxData = $taxData; - $this->_orderTaxFactory = $orderTaxFactory; - $this->_taxItemFactory = $taxItemFactory; $this->_calculation = $calculation; $this->_localeDate = $localeDate; $this->_reportTaxFactory = $reportTaxFactory; @@ -114,145 +98,6 @@ class Observer } } - /** - * Save order tax information - * - * @param \Magento\Framework\Event\Observer $observer - * @return void - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function salesEventOrderAfterSave(\Magento\Framework\Event\Observer $observer) - { - $order = $observer->getEvent()->getOrder(); - - if (!$order->getConvertingFromQuote() || $order->getAppliedTaxIsSaved()) { - return; - } - - $taxesAttr = $order->getCustomAttribute('applied_taxes'); - if (is_null($taxesAttr) || !is_array($taxesAttr->getValue())) { - $taxes = []; - } else { - $taxes = $taxesAttr->getValue(); - } - - $getTaxesForItemsAttr = $order->getCustomAttribute('item_applied_taxes'); - if (is_null($getTaxesForItemsAttr) || !is_array($getTaxesForItemsAttr->getValue())) { - $getTaxesForItems = []; - } else { - $getTaxesForItems = $getTaxesForItemsAttr->getValue(); - } - - $ratesIdQuoteItemId = []; - foreach ($getTaxesForItems as $taxesArray) { - foreach ($taxesArray as $rates) { - if (count($rates['rates']) == 1) { - $ratesIdQuoteItemId[$rates['id']][] = [ - 'id' => $rates['item_id'], - 'percent' => $rates['percent'], - 'code' => $rates['rates'][0]['code'], - 'associated_item_id' => $rates['associated_item_id'], - 'item_type' => $rates['item_type'], - 'amount' => $rates['amount'], - 'base_amount' => $rates['base_amount'], - 'real_amount' => $rates['amount'], - 'real_base_amount' => $rates['base_amount'], - ]; - } else { - $percentSum = 0; - foreach ($rates['rates'] as $rate) { - $real_amount = $rates['amount'] * $rate['percent'] / $rates['percent']; - $real_base_amount = $rates['base_amount'] * $rate['percent'] / $rates['percent']; - $ratesIdQuoteItemId[$rates['id']][] = [ - 'id' => $rates['item_id'], - 'percent' => $rate['percent'], - 'code' => $rate['code'], - 'associated_item_id' => $rates['associated_item_id'], - 'item_type' => $rates['item_type'], - 'amount' => $rates['amount'], - 'base_amount' => $rates['base_amount'], - 'real_amount' => $real_amount, - 'real_base_amount' => $real_base_amount, - ]; - $percentSum += $rate['percent']; - } - } - } - } - - foreach ($taxes as $row) { - $id = $row['id']; - foreach ($row['rates'] as $tax) { - if (is_null($row['percent'])) { - $baseRealAmount = $row['base_amount']; - } else { - if ($row['percent'] == 0 || $tax['percent'] == 0) { - continue; - } - $baseRealAmount = $row['base_amount'] / $row['percent'] * $tax['percent']; - } - $hidden = isset($row['hidden']) ? $row['hidden'] : 0; - $priority = isset($tax['priority']) ? $tax['priority'] : 0; - $position = isset($tax['position']) ? $tax['position'] : 0; - $process = isset($row['process']) ? $row['process'] : 0; - $data = [ - 'order_id' => $order->getId(), - 'code' => $tax['code'], - 'title' => $tax['title'], - 'hidden' => $hidden, - 'percent' => $tax['percent'], - 'priority' => $priority, - 'position' => $position, - 'amount' => $row['amount'], - 'base_amount' => $row['base_amount'], - 'process' => $process, - 'base_real_amount' => $baseRealAmount, - ]; - - /** @var $orderTax \Magento\Tax\Model\Sales\Order\Tax */ - $orderTax = $this->_orderTaxFactory->create(); - $result = $orderTax->setData($data)->save(); - - if (isset($ratesIdQuoteItemId[$id])) { - foreach ($ratesIdQuoteItemId[$id] as $quoteItemId) { - if ($quoteItemId['code'] == $tax['code']) { - $itemId = null; - $associatedItemId = null; - if (isset($quoteItemId['id'])) { - //This is a product item - $item = $order->getItemByQuoteItemId($quoteItemId['id']); - $itemId = $item->getId(); - } elseif (isset($quoteItemId['associated_item_id'])) { - //This item is associated with a product item - $item = $order->getItemByQuoteItemId($quoteItemId['associated_item_id']); - $associatedItemId = $item->getId(); - } - - $data = [ - 'item_id' => $itemId, - 'tax_id' => $result->getTaxId(), - 'tax_percent' => $quoteItemId['percent'], - 'associated_item_id' => $associatedItemId, - 'amount' => $quoteItemId['amount'], - 'base_amount' => $quoteItemId['base_amount'], - 'real_amount' => $quoteItemId['real_amount'], - 'real_base_amount' => $quoteItemId['real_base_amount'], - 'taxable_item_type' => $quoteItemId['item_type'], - ]; - /** @var $taxItem \Magento\Sales\Model\Order\Tax\Item */ - $taxItem = $this->_taxItemFactory->create(); - $taxItem->setData($data)->save(); - } - } - } - } - } - - $order->setAppliedTaxIsSaved(true); - } - /** * Refresh sales tax report statistics for last day * diff --git a/app/code/Magento/Tax/Model/Plugin/OrderSave.php b/app/code/Magento/Tax/Model/Plugin/OrderSave.php new file mode 100644 index 0000000000000000000000000000000000000000..145c98ad60e38784b8c52cc9818ffff9fa5cd6bc --- /dev/null +++ b/app/code/Magento/Tax/Model/Plugin/OrderSave.php @@ -0,0 +1,184 @@ +<?php +/** + * + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Tax\Model\Plugin; + +class OrderSave +{ + /** + * @var \Magento\Tax\Model\Sales\Order\TaxFactory + */ + protected $orderTaxFactory; + + /** + * @var \Magento\Sales\Model\Order\Tax\ItemFactory + */ + protected $taxItemFactory; + + /** + * @param \Magento\Tax\Model\Sales\Order\TaxFactory $orderTaxFactory + * @param \Magento\Sales\Model\Order\Tax\ItemFactory $taxItemFactory + */ + public function __construct( + \Magento\Tax\Model\Sales\Order\TaxFactory $orderTaxFactory, + \Magento\Sales\Model\Order\Tax\ItemFactory $taxItemFactory + ) { + $this->orderTaxFactory = $orderTaxFactory; + $this->taxItemFactory = $taxItemFactory; + } + + /** + * Save order tax + * + * @param \Magento\Sales\Api\OrderRepositoryInterface $subject + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @return \Magento\Sales\Api\Data\OrderInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave( + \Magento\Sales\Api\OrderRepositoryInterface $subject, + \Magento\Sales\Api\Data\OrderInterface $order + ) { + $this->saveOrderTax($order); + return $order; + } + + /** + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @return $this + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function saveOrderTax(\Magento\Sales\Api\Data\OrderInterface $order) + { + $extensionAttribute = $order->getExtensionAttributes(); + if (!$extensionAttribute || + !$extensionAttribute->getConvertingFromQuote() || + $order->getAppliedTaxIsSaved()) { + return; + } + + $taxes = $extensionAttribute->getAppliedTaxes(); + if ($taxes == null) { + $taxes = []; + } + + $taxesForItems = $extensionAttribute->getItemAppliedTaxes(); + if ($taxesForItems == null) { + $taxesForItems = []; + } + + $ratesIdQuoteItemId = []; + foreach ($taxesForItems as $taxesArray) { + foreach ($taxesArray as $rates) { + if (count($rates['rates']) == 1) { + $ratesIdQuoteItemId[$rates['id']][] = [ + 'id' => $rates['item_id'], + 'percent' => $rates['percent'], + 'code' => $rates['rates'][0]['code'], + 'associated_item_id' => $rates['associated_item_id'], + 'item_type' => $rates['item_type'], + 'amount' => $rates['amount'], + 'base_amount' => $rates['base_amount'], + 'real_amount' => $rates['amount'], + 'real_base_amount' => $rates['base_amount'], + ]; + } else { + $percentSum = 0; + foreach ($rates['rates'] as $rate) { + $realAmount = $rates['amount'] * $rate['percent'] / $rates['percent']; + $realBaseAmount = $rates['base_amount'] * $rate['percent'] / $rates['percent']; + $ratesIdQuoteItemId[$rates['id']][] = [ + 'id' => $rates['item_id'], + 'percent' => $rate['percent'], + 'code' => $rate['code'], + 'associated_item_id' => $rates['associated_item_id'], + 'item_type' => $rates['item_type'], + 'amount' => $rates['amount'], + 'base_amount' => $rates['base_amount'], + 'real_amount' => $realAmount, + 'real_base_amount' => $realBaseAmount, + ]; + $percentSum += $rate['percent']; + } + } + } + } + + foreach ($taxes as $row) { + $id = $row['id']; + foreach ($row['rates'] as $tax) { + if ($row['percent'] == null) { + $baseRealAmount = $row['base_amount']; + } else { + if ($row['percent'] == 0 || $tax['percent'] == 0) { + continue; + } + $baseRealAmount = $row['base_amount'] / $row['percent'] * $tax['percent']; + } + $hidden = isset($row['hidden']) ? $row['hidden'] : 0; + $priority = isset($tax['priority']) ? $tax['priority'] : 0; + $position = isset($tax['position']) ? $tax['position'] : 0; + $process = isset($row['process']) ? $row['process'] : 0; + $data = [ + 'order_id' => $order->getEntityId(), + 'code' => $tax['code'], + 'title' => $tax['title'], + 'hidden' => $hidden, + 'percent' => $tax['percent'], + 'priority' => $priority, + 'position' => $position, + 'amount' => $row['amount'], + 'base_amount' => $row['base_amount'], + 'process' => $process, + 'base_real_amount' => $baseRealAmount, + ]; + + /** @var $orderTax \Magento\Tax\Model\Sales\Order\Tax */ + $orderTax = $this->orderTaxFactory->create(); + $result = $orderTax->setData($data)->save(); + + if (isset($ratesIdQuoteItemId[$id])) { + foreach ($ratesIdQuoteItemId[$id] as $quoteItemId) { + if ($quoteItemId['code'] == $tax['code']) { + $itemId = null; + $associatedItemId = null; + if (isset($quoteItemId['id'])) { + //This is a product item + $item = $order->getItemByQuoteItemId($quoteItemId['id']); + $itemId = $item->getId(); + } elseif (isset($quoteItemId['associated_item_id'])) { + //This item is associated with a product item + $item = $order->getItemByQuoteItemId($quoteItemId['associated_item_id']); + $associatedItemId = $item->getId(); + } + + $data = [ + 'item_id' => $itemId, + 'tax_id' => $result->getTaxId(), + 'tax_percent' => $quoteItemId['percent'], + 'associated_item_id' => $associatedItemId, + 'amount' => $quoteItemId['amount'], + 'base_amount' => $quoteItemId['base_amount'], + 'real_amount' => $quoteItemId['real_amount'], + 'real_base_amount' => $quoteItemId['real_base_amount'], + 'taxable_item_type' => $quoteItemId['item_type'], + ]; + /** @var $taxItem \Magento\Sales\Model\Order\Tax\Item */ + $taxItem = $this->taxItemFactory->create(); + $taxItem->setData($data)->save(); + } + } + } + } + } + + $order->setAppliedTaxIsSaved(true); + return $this; + } +} diff --git a/app/code/Magento/Tax/Model/Quote/ToOrderConverter.php b/app/code/Magento/Tax/Model/Quote/ToOrderConverter.php index c269ee02dad4e39942e757265796d51cd4f095fd..f45aebc00cbbc60b9c0b60da06024f69f23b7c0d 100644 --- a/app/code/Magento/Tax/Model/Quote/ToOrderConverter.php +++ b/app/code/Magento/Tax/Model/Quote/ToOrderConverter.php @@ -16,6 +16,20 @@ class ToOrderConverter */ protected $quoteAddress; + /** + * @var \Magento\Sales\Api\Data\OrderExtensionFactory + */ + protected $orderExtensionFactory; + + /** + * @param \Magento\Sales\Api\Data\OrderExtensionFactory $orderExtensionFactory + */ + public function __construct( + \Magento\Sales\Api\Data\OrderExtensionFactory $orderExtensionFactory + ) { + $this->orderExtensionFactory = $orderExtensionFactory; + } + /** * @param QuoteAddressToOrder $subject * @param QuoteAddress $address @@ -39,21 +53,20 @@ class ToOrderConverter { /** @var \Magento\Sales\Model\Order $order */ $taxes = $this->quoteAddress->getAppliedTaxes(); - if (is_array($taxes)) { - if (is_array($order->getAppliedTaxes())) { - $taxes = array_merge($order->getAppliedTaxes(), $taxes); - } - $order->setCustomAttribute('applied_taxes', $taxes); - $order->setCustomAttribute('converting_from_quote', true); + $extensionAttributes = $order->getExtensionAttributes(); + if ($extensionAttributes == null) { + $extensionAttributes = $this->orderExtensionFactory->create(); + } + if (!empty($taxes)) { + $extensionAttributes->setAppliedTaxes($taxes); + $extensionAttributes->setConvertingFromQuote(true); } $itemAppliedTaxes = $this->quoteAddress->getItemsAppliedTaxes(); - if (is_array($itemAppliedTaxes)) { - if (is_array($order->getItemAppliedTaxes())) { - $itemAppliedTaxes = array_merge($order->getItemAppliedTaxes(), $itemAppliedTaxes); - } - $order->setCustomAttribute('item_applied_taxes', $itemAppliedTaxes); + if (!empty($itemAppliedTaxes)) { + $extensionAttributes->setItemAppliedTaxes($itemAppliedTaxes); } + $order->setExtensionAttributes($extensionAttributes); return $order; } } diff --git a/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php b/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f50e79c9658b377c088ab51aed488d2494ee8f82 --- /dev/null +++ b/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php @@ -0,0 +1,463 @@ +<?php +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Tax\Test\Unit\Model\Plugin; + +use \Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class OrderSaveTest extends \PHPUnit_Framework_TestCase +{ + const ORDERID = 123; + const ITEMID = 151; + const ORDER_ITEM_ID = 116; + + /** + * @var \Magento\Tax\Model\Sales\Order\TaxFactory|\PHPUnit_Framework_MockObject_MockObject + */ + protected $orderTaxFactoryMock; + + /** + * @var \Magento\Sales\Model\Order\Tax\ItemFactory|\PHPUnit_Framework_MockObject_MockObject + */ + protected $taxItemFactoryMock; + + /** + * @var \Magento\Sales\Api\OrderRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $subjectMock; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + protected $objectManagerHelper; + + /** + * @var \Magento\Tax\Model\Plugin\OrderSave + */ + protected $model; + + public function setUp() + { + $this->orderTaxFactoryMock = $this->getMockBuilder( + '\Magento\Tax\Model\Sales\Order\TaxFactory' + )->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->taxItemFactoryMock = $this->getMockBuilder('\Magento\Sales\Model\Order\Tax\ItemFactory') + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->subjectMock = $this->getMockForAbstractClass('\Magento\Sales\Api\OrderRepositoryInterface'); + + $this->objectManagerHelper = new ObjectManager($this); + $this->model = $this->objectManagerHelper->getObject( + '\Magento\Tax\Model\Plugin\OrderSave', + [ + 'orderTaxFactory' => $this->orderTaxFactoryMock, + 'taxItemFactory' => $this->taxItemFactoryMock, + ] + ); + } + + protected function setupOrderMock() + { + $orderMock = $this->getMockBuilder('\Magento\Sales\Model\Order') + ->disableOriginalConstructor() + ->setMethods( + [ + 'getExtensionAttributes', + 'getAppliedTaxIsSaved', + 'getItemByQuoteItemId', + 'setAppliedTaxIsSaved', + 'getEntityId', + ] + )->getMock(); + + return $orderMock; + } + + protected function setupExtensionAttributeMock() + { + $orderExtensionAttributeMock = $this->getMockBuilder('\Magento\Sales\Api\Data\OrderExtensionInterface') + ->disableOriginalConstructor() + ->setMethods( + [ + 'getAppliedTaxes', + 'getConvertingFromQuote', + 'getItemAppliedTaxes', + ] + )->getMock(); + + return $orderExtensionAttributeMock; + } + + protected function verifyOrderTaxes($expectedTaxes) + { + $index = 0; + $orderTaxes = []; + foreach ($expectedTaxes as $orderTaxId => $orderTaxData) { + $orderTaxMock = $this->getMockBuilder('\Magento\Tax\Model\Sales\Order\Tax') + ->disableOriginalConstructor() + ->setMethods( + [ + 'getTaxId', + 'setData', + 'save', + ] + )->getMock(); + $orderTaxMock->expects($this->once()) + ->method('setData') + ->with($orderTaxData) + ->willReturnSelf(); + $orderTaxMock->expects($this->once()) + ->method('save') + ->willReturnSelf(); + $orderTaxMock->expects($this->atLeastOnce()) + ->method('getTaxId') + ->willReturn($orderTaxId); + $this->orderTaxFactoryMock->expects($this->at($index)) + ->method('create') + ->willReturn($orderTaxMock); + $orderTaxes[] = $orderTaxMock; + $index++; + } + } + + public function verifyItemTaxes($expectedItemTaxes) + { + $index = 0; + $itemTaxes = []; + foreach ($expectedItemTaxes as $itemTax) { + $itemTaxMock = $this->getMockBuilder('\Magento\Tax\Model\Sales\Order\Tax\Item') + ->disableOriginalConstructor() + ->setMethods( + [ + 'setData', + 'save', + ] + )->getMock(); + $itemTaxMock->expects($this->once()) + ->method('setData') + ->with($itemTax) + ->willReturnSelf(); + $itemTaxMock->expects($this->once()) + ->method('save') + ->willReturnSelf(); + $this->taxItemFactoryMock->expects($this->at($index)) + ->method('create') + ->willReturn($itemTaxMock); + $itemTaxes[] = $itemTaxMock; + $index++; + } + } + + /** + * @dataProvider afterSaveDataProvider + */ + public function testAfterSave( + $appliedTaxes, + $itemAppliedTaxes, + $expectedTaxes, + $expectedItemTaxes + ) { + $orderMock = $this->setupOrderMock(); + + $extensionAttributeMock = $this->setupExtensionAttributeMock(); + $extensionAttributeMock->expects($this->any()) + ->method('getConvertingFromQuote') + ->willReturn(true); + $extensionAttributeMock->expects($this->any()) + ->method('getAppliedTaxes') + ->willReturn($appliedTaxes); + $extensionAttributeMock->expects($this->any()) + ->method('getItemAppliedTaxes') + ->willReturn($itemAppliedTaxes); + + + $orderItemMock = $this->getMockBuilder('\Magento\Sales\Model\Order\Item') + ->disableOriginalConstructor() + ->setMethods(['getId', ]) + ->getMock(); + $orderItemMock->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn(self::ORDER_ITEM_ID); + $orderMock->expects($this->once()) + ->method('getAppliedTaxIsSaved') + ->willReturn(false); + $orderMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($extensionAttributeMock); + $orderMock->expects($this->atLeastOnce()) + ->method('getItemByQuoteItemId') + ->with(self::ITEMID) + ->willReturn($orderItemMock); + $orderMock->expects($this->atLeastOnce()) + ->method('getEntityId') + ->willReturn(self::ORDERID); + + $orderMock->expects($this->once()) + ->method('setAppliedTaxIsSaved') + ->with(true); + + $this->verifyOrderTaxes($expectedTaxes); + $this->verifyItemTaxes($expectedItemTaxes); + + $this->assertEquals($orderMock, $this->model->afterSave($this->subjectMock, $orderMock)); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function afterSaveDataProvider() + { + return [ + //one item with shipping + //three tax rates: state and national tax rates of 6 and 5 percent with priority 0 + //city tax rate of 3 percent with priority 1 + 'item_with_shipping_three_tax' => [ + 'applied_taxes' => [ + [ + 'amount' => 0.66, + 'base_amount' => 0.66, + 'percent' => 11, + 'id' => 'ILUS', + 'rates' => [ + [ + 'percent' => 6, + 'code' => 'IL', + 'title' => 'IL', + ], + [ + 'percent' => 5, + 'code' => 'US', + 'title' => 'US', + ], + ], + ], + [ + 'amount' => 0.2, + 'base_amount' => 0.2, + 'percent' => 3.33, + 'id' => 'CityTax', + 'rates' => [ + [ + 'percent' => 3, + 'code' => 'CityTax', + 'title' => 'CityTax', + ], + ], + ], + ], + 'item_applied_taxes' => [ + //item tax, three tax rates + [ + //first two taxes are combined + [ + 'amount' => 0.11, + 'base_amount' => 0.11, + 'percent' => 11, + 'id' => 'ILUS', + 'rates' => [ + [ + 'percent' => 6, + 'code' => 'IL', + 'title' => 'IL', + ], + [ + 'percent' => 5, + 'code' => 'US', + 'title' => 'US', + ], + ], + 'item_id' => self::ITEMID, + 'item_type' => 'product', + 'associated_item_id' => null, + ], + //city tax + [ + 'amount' => 0.03, + 'base_amount' => 0.03, + 'percent' => 3.33, + 'id' => 'CityTax', + 'rates' => [ + [ + 'percent' => 3, + 'code' => 'CityTax', + 'title' => 'CityTax', + ], + ], + 'item_id' => self::ITEMID, + 'item_type' => 'product', + 'associated_item_id' => null, + ], + ], + //shipping tax + [ + //first two taxes are combined + [ + 'amount' => 0.55, + 'base_amount' => 0.55, + 'percent' => 11, + 'id' => 'ILUS', + 'rates' => [ + [ + 'percent' => 6, + 'code' => 'IL', + 'title' => 'IL', + ], + [ + 'percent' => 5, + 'code' => 'US', + 'title' => 'US', + ], + ], + 'item_id' => null, + 'item_type' => 'shipping', + 'associated_item_id' => null, + ], + //city tax + [ + 'amount' => 0.17, + 'base_amount' => 0.17, + 'percent' => 3.33, + 'id' => 'CityTax', + 'rates' => [ + [ + 'percent' => 3, + 'code' => 'CityTax', + 'title' => 'CityTax', + ], + ], + 'item_id' => null, + 'item_type' => 'shipping', + 'associated_item_id' => null, + ], + ], + ], + 'expected_order_taxes' => [ + //state tax + '35' => [ + 'order_id' => self::ORDERID, + 'code' => 'IL', + 'title' => 'IL', + 'hidden' => 0, + 'percent' => 6, + 'priority' => 0, + 'position' => 0, + 'amount' => 0.66, + 'base_amount' => 0.66, + 'process' => 0, + 'base_real_amount' => 0.36, + ], + //federal tax + '36' => [ + 'order_id' => self::ORDERID, + 'code' => 'US', + 'title' => 'US', + 'hidden' => 0, + 'percent' => 5, + 'priority' => 0, + 'position' => 0, + 'amount' => 0.66, //combined amount + 'base_amount' => 0.66, + 'process' => 0, + 'base_real_amount' => 0.3, //portion for specific rate + ], + //city tax + '37' => [ + 'order_id' => self::ORDERID, + 'code' => 'CityTax', + 'title' => 'CityTax', + 'hidden' => 0, + 'percent' => 3, + 'priority' => 0, + 'position' => 0, + 'amount' => 0.2, //combined amount + 'base_amount' => 0.2, + 'process' => 0, + 'base_real_amount' => 0.18018018018018, //this number is meaningless since this is single rate + ], + ], + 'expected_item_taxes' => [ + [ + //state tax for item + 'item_id' => self::ORDER_ITEM_ID, + 'tax_id' => '35', + 'tax_percent' => 6, + 'associated_item_id' => null, + 'amount' => 0.11, + 'base_amount' => 0.11, + 'real_amount' => 0.06, + 'real_base_amount' => 0.06, + 'taxable_item_type' => 'product', + ], + [ + //state tax for shipping + 'item_id' => null, + 'tax_id' => '35', + 'tax_percent' => 6, + 'associated_item_id' => null, + 'amount' => 0.55, + 'base_amount' => 0.55, + 'real_amount' => 0.3, + 'real_base_amount' => 0.3, + 'taxable_item_type' => 'shipping', + ], + [ + //federal tax for item + 'item_id' => self::ORDER_ITEM_ID, + 'tax_id' => '36', + 'tax_percent' => 5, + 'associated_item_id' => null, + 'amount' => 0.11, + 'base_amount' => 0.11, + 'real_amount' => 0.05, + 'real_base_amount' => 0.05, + 'taxable_item_type' => 'product', + ], + [ + //federal tax for shipping + 'item_id' => null, + 'tax_id' => '36', + 'tax_percent' => 5, + 'associated_item_id' => null, + 'amount' => 0.55, + 'base_amount' => 0.55, + 'real_amount' => 0.25, + 'real_base_amount' => 0.25, + 'taxable_item_type' => 'shipping', + ], + [ + //city tax for item + 'item_id' => self::ORDER_ITEM_ID, + 'tax_id' => '37', + 'tax_percent' => 3.33, + 'associated_item_id' => null, + 'amount' => 0.03, + 'base_amount' => 0.03, + 'real_amount' => 0.03, + 'real_base_amount' => 0.03, + 'taxable_item_type' => 'product', + ], + [ + //city tax for shipping + 'item_id' => null, + 'tax_id' => '37', + 'tax_percent' => 3.33, + 'associated_item_id' => null, + 'amount' => 0.17, + 'base_amount' => 0.17, + 'real_amount' => 0.17, + 'real_base_amount' => 0.17, + 'taxable_item_type' => 'shipping', + ], + ], + ], + ]; + } +} diff --git a/app/code/Magento/Tax/Test/Unit/Model/Quote/ToOrderConverterTest.php b/app/code/Magento/Tax/Test/Unit/Model/Quote/ToOrderConverterTest.php new file mode 100644 index 0000000000000000000000000000000000000000..cc72250a496783692451f8c12ecc610f9de28d43 --- /dev/null +++ b/app/code/Magento/Tax/Test/Unit/Model/Quote/ToOrderConverterTest.php @@ -0,0 +1,195 @@ +<?php +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Tax\Test\Unit\Model\Quote; + +use \Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class ToOrderConverterTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Sales\Api\Data\OrderExtensionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + protected $orderExtensionFactoryMock; + + /** + * @var \Magento\Quote\Model\Quote\Address|\PHPUnit_Framework_MockObject_MockObject + */ + protected $quoteAddressMock; + + /** + * @var \Magento\Quote\Model\Quote\Address\ToOrder|\PHPUnit_Framework_MockObject_MockObject + */ + protected $subjectMock; + + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + protected $objectManagerHelper; + + /** + * @var \Magento\Tax\Model\Quote\ToOrderConverter + */ + protected $model; + + public function setUp() + { + $this->orderExtensionFactoryMock = $this->getMockBuilder( + '\Magento\Sales\Api\Data\OrderExtensionFactory' + )->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->quoteAddressMock = $this->getMockBuilder('\Magento\Quote\Model\Quote\Address') + ->disableOriginalConstructor() + ->setMethods(['getAppliedTaxes', 'getItemsAppliedTaxes']) + ->getMock(); + $this->subjectMock = $this->getMockBuilder('\Magento\Quote\Model\Quote\Address\ToOrder') + ->disableOriginalConstructor() + ->getMock(); + + $this->objectManagerHelper = new ObjectManager($this); + $this->model = $this->objectManagerHelper->getObject( + '\Magento\Tax\Model\Quote\ToOrderConverter', + [ + 'orderExtensionFactory' => $this->orderExtensionFactoryMock, + ] + ); + } + + protected function setupOrderExtensionAttributeMock() + { + $orderExtensionAttributeMock = $this->getMockBuilder('\Magento\Sales\Api\Data\OrderExtensionInterface') + ->setMethods( + [ + 'setAppliedTaxes', + 'setConvertingFromQuote', + 'setItemAppliedTaxes' + ] + )->getMock(); + + return $orderExtensionAttributeMock; + } + + /** + * @dataProvider afterConvertDataProvider + */ + public function testAfterConvert($appliedTaxes, $itemsAppliedTaxes) + { + $this->model->beforeConvert($this->subjectMock, $this->quoteAddressMock); + + $this->quoteAddressMock->expects($this->once()) + ->method('getAppliedTaxes') + ->willReturn($appliedTaxes); + $this->quoteAddressMock->expects($this->once()) + ->method('getItemsAppliedTaxes') + ->willReturn($itemsAppliedTaxes); + + $orderMock = $this->getMockBuilder('\Magento\Sales\Model\Order') + ->disableOriginalConstructor() + ->getMock(); + + $orderExtensionAttributeMock = $this->setupOrderExtensionAttributeMock(); + + $orderMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($orderExtensionAttributeMock); + + $orderExtensionAttributeMock->expects($this->once()) + ->method('setAppliedTaxes') + ->with($appliedTaxes); + $orderExtensionAttributeMock->expects($this->once()) + ->method('setConvertingFromQuote') + ->with(true); + $orderExtensionAttributeMock->expects($this->once()) + ->method('setItemAppliedTaxes') + ->with($itemsAppliedTaxes); + $orderMock->expects($this->once()) + ->method('setExtensionAttributes') + ->with($orderExtensionAttributeMock); + + $this->assertEquals($orderMock, $this->model->afterConvert($this->subjectMock, $orderMock)); + } + + /** + * @dataProvider afterConvertDataProvider + */ + public function testAfterConvertNullExtensionAttribute($appliedTaxes, $itemsAppliedTaxes) + { + $this->model->beforeConvert($this->subjectMock, $this->quoteAddressMock); + + $this->quoteAddressMock->expects($this->once()) + ->method('getAppliedTaxes') + ->willReturn($appliedTaxes); + $this->quoteAddressMock->expects($this->once()) + ->method('getItemsAppliedTaxes') + ->willReturn($itemsAppliedTaxes); + + $orderExtensionAttributeMock = $this->setupOrderExtensionAttributeMock(); + + $orderMock = $this->getMockBuilder('\Magento\Sales\Model\Order') + ->disableOriginalConstructor() + ->getMock(); + + $orderMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn(null); + + $this->orderExtensionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($orderExtensionAttributeMock); + + $orderExtensionAttributeMock->expects($this->once()) + ->method('setAppliedTaxes') + ->with($appliedTaxes); + $orderExtensionAttributeMock->expects($this->once()) + ->method('setConvertingFromQuote') + ->with(true); + $orderExtensionAttributeMock->expects($this->once()) + ->method('setItemAppliedTaxes') + ->with($itemsAppliedTaxes); + $orderMock->expects($this->once()) + ->method('setExtensionAttributes') + ->with($orderExtensionAttributeMock); + + $this->assertEquals($orderMock, $this->model->afterConvert($this->subjectMock, $orderMock)); + } + + public function afterConvertDataProvider() + { + return [ + 'afterConvert' => [ + 'applied_taxes' => [ + 'IL' => [ + 'amount' => 0.36, + 'percent' => 6, + 'rates' => [ + [ + 'percent' => 6, + 'code' => 'IL', + 'title' => 'IL', + ] + ] + ] + ], + 'item_applied_taxes' => [ + 'sequence-1' => [ + [ + 'amount' => 0.06, + 'item_id' => 146, + ], + ], + 'shipping' => [ + [ + 'amount' => 0.30, + 'item_type' => 'shipping', + ] + ], + ], + ], + ]; + } +} diff --git a/app/code/Magento/Tax/etc/di.xml b/app/code/Magento/Tax/etc/di.xml index 7b8eb051ac01d4018fa41484b157c856cc5e2d09..48b3173d60f09e739305f3c68cf501be8315be96 100644 --- a/app/code/Magento/Tax/etc/di.xml +++ b/app/code/Magento/Tax/etc/di.xml @@ -77,4 +77,7 @@ <argument name="resourcePrefix" xsi:type="string">sales</argument> </arguments> </type> + <type name="Magento\Sales\Api\OrderRepositoryInterface"> + <plugin name="save_order_tax" type="Magento\Tax\Model\Plugin\OrderSave"/> + </type> </config> diff --git a/app/code/Magento/Tax/etc/events.xml b/app/code/Magento/Tax/etc/events.xml index 4d2c2d3ba60da356a557d9c5f87e6fc33df14cd2..b523cf565dd000c1b1e0877911973ca364534642 100644 --- a/app/code/Magento/Tax/etc/events.xml +++ b/app/code/Magento/Tax/etc/events.xml @@ -6,9 +6,6 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Event/etc/events.xsd"> - <event name="sales_order_save_after"> - <observer name="tax" instance="Magento\Tax\Model\Observer" method="salesEventOrderAfterSave" /> - </event> <event name="sales_quote_collect_totals_before"> <observer name="tax" instance="Magento\Tax\Model\Observer" method="quoteCollectTotalsBefore" /> </event> diff --git a/app/code/Magento/Tax/etc/extension_attributes.xml b/app/code/Magento/Tax/etc/extension_attributes.xml index 0302ae2fcd16a12a2f270d5f7e04fda059c52af4..810c04b8c1917e9230db99faeac529445844b019 100644 --- a/app/code/Magento/Tax/etc/extension_attributes.xml +++ b/app/code/Magento/Tax/etc/extension_attributes.xml @@ -9,4 +9,9 @@ <extension_attributes for="Magento\Quote\Api\Data\TotalsInterface"> <attribute code="tax_grandtotal_details" type="Magento\Tax\Api\Data\GrandTotalDetailsInterface[]" /> </extension_attributes> + <extension_attributes for="Magento\Sales\Api\Data\OrderInterface"> + <attribute code="applied_taxes" type="Magento\Tax\Api\Data\OrderTaxDetailsAppliedTaxInterface[]" /> + <attribute code="item_applied_taxes" type="Magento\Tax\Api\Data\OrderTaxDetailsItemInterface[]" /> + <attribute code="converting_from_quote" type="boolean" /> + </extension_attributes> </config> diff --git a/lib/internal/Magento/Framework/Api/DataObjectHelper.php b/lib/internal/Magento/Framework/Api/DataObjectHelper.php index 2fb75d5f841613fb0523cc88e7062112d1fc6729..70a7b79c71b8b9b011aae799e4e3b76f6c7027f2 100644 --- a/lib/internal/Magento/Framework/Api/DataObjectHelper.php +++ b/lib/internal/Magento/Framework/Api/DataObjectHelper.php @@ -176,7 +176,7 @@ class DataObjectHelper $object = $this->objectFactory->create($returnType, []); $this->populateWithArray($object, $value, $returnType); } else if (is_subclass_of($returnType, '\Magento\Framework\Api\ExtensionAttributesInterface')) { - $object = $this->extensionFactory->create(get_class($dataObject), $value); + $object = $this->extensionFactory->create(get_class($dataObject), ['data' => $value]); } else { $object = $this->objectFactory->create($returnType, $value); }