From 4b1986bce52f656670ab45822e6f39b2aad21d9a Mon Sep 17 00:00:00 2001 From: Thilo <40621547+ThLind@users.noreply.github.com> Date: Tue, 16 Apr 2024 13:02:41 +0200 Subject: [PATCH] Mol 1299/refund tax at net orders (#734) * MOL-1299: Refund Tax at Net Orders * MOL-1299: Correct VAT Promotion Calculation --------- Co-authored-by: Thilo Lindner --- .../Builder/RefundDataBuilder.php | 103 +++++++++++++++++- .../RefundData/OrderItem/AbstractItem.php | 36 +++++- .../RefundData/OrderItem/DeliveryItem.php | 8 +- .../RefundData/OrderItem/ProductItem.php | 15 ++- .../RefundData/OrderItem/PromotionItem.php | 23 +++- .../RefundManager/RefundData/RefundData.php | 17 ++- .../grids/ShopwareOrderGrid.js | 5 + .../components/mollie-refund-manager/index.js | 18 +++ .../mollie-refund-manager.html.twig | 11 ++ .../mollie-refund-manager.scss | 3 +- .../services/RefundItemService.js | 39 ++++++- .../app/administration/src/snippet/de-DE.json | 3 +- .../app/administration/src/snippet/en-GB.json | 3 +- .../app/administration/src/snippet/nl-NL.json | 3 +- .../RefundData/OrderItem/ProductItemTest.php | 8 +- .../RefundData/RefundDataTest.php | 15 ++- 16 files changed, 283 insertions(+), 27 deletions(-) diff --git a/src/Components/RefundManager/Builder/RefundDataBuilder.php b/src/Components/RefundManager/Builder/RefundDataBuilder.php index e31a60041..e7e9f362a 100644 --- a/src/Components/RefundManager/Builder/RefundDataBuilder.php +++ b/src/Components/RefundManager/Builder/RefundDataBuilder.php @@ -19,7 +19,9 @@ use Kiener\MolliePayments\Struct\OrderLineItemEntity\OrderLineItemEntityAttributes; use Mollie\Api\Resources\OrderLine; use Mollie\Api\Resources\Refund; +use Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice; use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryEntity; +use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity; use Shopware\Core\Checkout\Order\OrderEntity; use Shopware\Core\Framework\Context; @@ -112,15 +114,19 @@ public function buildRefundData(OrderEntity $order, Context $context): RefundDat $alreadyRefundedQty = $this->getRefundedQuantity($mollieOrderLineId, $mollieOrder, $refunds); } + $taxTotal = round($this->calculateLineItemTaxTotal($item), 2); + $taxPerItem = floor($taxTotal / $item->getQuantity() * 100) / 100; + $taxDiff = round($taxTotal - ($taxPerItem * $item->getQuantity()), 2); + # this is just a way to move the promotions to the last positions of our array. # also, shipping-free promotions have their discount item in the deliveries,...so here would just # be a 0,00 value line item, that we want to skip. if ($lineItemAttribute->isPromotion()) { if ($item->getTotalPrice() !== 0.0) { - $refundPromotionItems[] = PromotionItem::fromOrderLineItem($item, $alreadyRefundedQty); + $refundPromotionItems[] = PromotionItem::fromOrderLineItem($item, $alreadyRefundedQty, $taxTotal, $taxPerItem, $taxDiff); } } else { - $refundItems[] = new ProductItem($item, $promotionCompositions, $alreadyRefundedQty); + $refundItems[] = new ProductItem($item, $promotionCompositions, $alreadyRefundedQty, $taxTotal, $taxPerItem, $taxDiff); } } } @@ -151,10 +157,14 @@ public function buildRefundData(OrderEntity $order, Context $context): RefundDat $alreadyRefundedQty = $this->getRefundedQuantity($mollieLineID, $mollieOrder, $refunds); } + $taxTotal = round($this->calculateDeliveryEntityTaxTotal($delivery), 2); + $taxPerItem = floor($taxTotal / $delivery->getShippingCosts()->getQuantity() * 100) / 100; + $taxDiff = round($taxTotal - ($taxPerItem * $delivery->getShippingCosts()->getQuantity()), 2); + if ($delivery->getShippingCosts()->getTotalPrice() < 0) { - $refundPromotionItems[] = PromotionItem::fromOrderDeliveryItem($delivery, $alreadyRefundedQty); + $refundPromotionItems[] = PromotionItem::fromOrderDeliveryItem($delivery, $alreadyRefundedQty, $taxTotal, $taxPerItem, $taxDiff); } else { - $refundDeliveryItems[] = new DeliveryItem($delivery, $alreadyRefundedQty); + $refundDeliveryItems[] = new DeliveryItem($delivery, $alreadyRefundedQty, $taxTotal, $taxPerItem, $taxDiff); } } } @@ -178,6 +188,8 @@ public function buildRefundData(OrderEntity $order, Context $context): RefundDat # we first need products, then promotions and as last type we add the deliveries $refundItems = array_merge($refundItems, $refundPromotionItems, $refundDeliveryItems); + // get the tax status of the order + $taxStatus = $order->getTaxStatus(); # now fetch some basic values from the API # TODO: these API calls should be removed one day, once I have more time (this refund manager is indeed huge) for now it's fine @@ -205,7 +217,8 @@ public function buildRefundData(OrderEntity $order, Context $context): RefundDat $pendingRefundAmount, $refundedTotal, $remaining, - $roundingDiffTotal + $roundingDiffTotal, + $taxStatus ); } @@ -242,13 +255,47 @@ private function getAllPromotionCompositions(OrderEntity $order): array foreach ($order->getLineItems() as $item) { if (isset($item->getPayload()['composition'])) { - $promotionCompositions[] = $item->getPayload()['composition']; + $promotionComposition = $item->getPayload()['composition']; + + $promotionComposition = $this->calculatePromotionCompositionTax($item, $promotionComposition); + + $promotionCompositions[] = $promotionComposition; } } return $promotionCompositions; } + /** + * @param OrderLineItemEntity $item + * @param array $promotionComposition + * @return array + */ + private function calculatePromotionCompositionTax(OrderLineItemEntity $item, array $promotionComposition): array + { + $lineItemAttribute = new OrderLineItemEntityAttributes($item); + if ($lineItemAttribute->isPromotion()) { + $taxTotal = round($this->calculateLineItemTaxTotal($item), 2); + $lineItemTotal = $item->getTotalPrice(); + $lastIndex = array_keys($promotionComposition)[count($promotionComposition) - 1]; + + $taxSum = 0; + + foreach ($promotionComposition as $i => &$composition) { + $partialTax = round($taxTotal * $composition['discount'] / $lineItemTotal, 2); + + if ($i === $lastIndex) { + $partialTax = -$taxTotal - $taxSum; + } + + $composition['taxValue'] = $partialTax; + $taxSum += $partialTax; + } + } + + return $promotionComposition; + } + /** * @param string $mollieLineItemId * @param \Mollie\Api\Resources\Order $mollieOrder @@ -312,4 +359,48 @@ private function getRefundedQuantity(string $mollieLineItemId, \Mollie\Api\Resou return $refundedQty; } + + /** + * @param OrderLineItemEntity $item + * @return float + */ + private function calculateLineItemTaxTotal(OrderLineItemEntity $item): float + { + $taxTotal = 0; + + $price = $item->getPrice(); + + if (!$price instanceof CalculatedPrice) { + return $taxTotal; + } + + return $this->calculateTax($price); + } + + /** + * @param OrderDeliveryEntity $delivery + * @return float + */ + private function calculateDeliveryEntityTaxTotal(OrderDeliveryEntity $delivery): float + { + $shippingCosts = $delivery->getShippingCosts(); + + return $this->calculateTax($shippingCosts); + } + + /** + * @param CalculatedPrice $price + * @return float + */ + private function calculateTax(CalculatedPrice $price): float + { + $calculatedTaxes = $price->getCalculatedTaxes(); + $taxTotal = 0; + + foreach ($calculatedTaxes as $calculatedTax) { + $taxTotal += $calculatedTax->getTax(); + } + + return $taxTotal; + } } diff --git a/src/Components/RefundManager/RefundData/OrderItem/AbstractItem.php b/src/Components/RefundManager/RefundData/OrderItem/AbstractItem.php index 0c8d4ed35..4cc537782 100644 --- a/src/Components/RefundManager/RefundData/OrderItem/AbstractItem.php +++ b/src/Components/RefundManager/RefundData/OrderItem/AbstractItem.php @@ -4,6 +4,33 @@ abstract class AbstractItem { + /** + * @var float + */ + private $taxTotal; + + /** + * @var float + */ + private $taxPerItem; + + /** + * @var float + */ + private $taxDiff; + + /** + * @param float $taxTotal + * @param float $taxPerItem + * @param float $taxDiff + */ + public function __construct(float $taxTotal, float $taxPerItem, float $taxDiff) + { + $this->taxTotal = $taxTotal; + $this->taxPerItem = $taxPerItem; + $this->taxDiff = $taxDiff; + } + /** * @param string $id * @param string $label @@ -15,10 +42,11 @@ abstract class AbstractItem * @param float $totalPrice * @param float $promotionDiscount * @param int $promotionAffectedQty + * @param float $promotionTaxValue * @param int $refundedQty * @return array */ - protected function buildArray(string $id, string $label, string $referenceNumber, bool $isPromotion, bool $isDelivery, float $unitPrice, int $quantity, float $totalPrice, float $promotionDiscount, int $promotionAffectedQty, int $refundedQty): array + protected function buildArray(string $id, string $label, string $referenceNumber, bool $isPromotion, bool $isDelivery, float $unitPrice, int $quantity, float $totalPrice, float $promotionDiscount, int $promotionAffectedQty, float $promotionTaxValue, int $refundedQty): array { return [ 'refunded' => $refundedQty, @@ -33,9 +61,15 @@ protected function buildArray(string $id, string $label, string $referenceNumber 'promotion' => [ 'discount' => $promotionDiscount, 'quantity' => $promotionAffectedQty, + 'taxValue' => $promotionTaxValue, ], 'isPromotion' => $isPromotion, 'isDelivery' => $isDelivery, + 'tax' => [ + 'totalItemTax' => round($this->taxTotal, 2), + 'perItemTax' => round($this->taxPerItem, 2), + 'totalToPerItemRoundingDiff' => round($this->taxDiff, 2), + ], ], ]; } diff --git a/src/Components/RefundManager/RefundData/OrderItem/DeliveryItem.php b/src/Components/RefundManager/RefundData/OrderItem/DeliveryItem.php index 66e8f741f..394f873c3 100644 --- a/src/Components/RefundManager/RefundData/OrderItem/DeliveryItem.php +++ b/src/Components/RefundManager/RefundData/OrderItem/DeliveryItem.php @@ -23,11 +23,16 @@ class DeliveryItem extends AbstractItem /** * @param OrderDeliveryEntity $delivery * @param int $alreadyRefundedQuantity + * @param float $taxTotal + * @param float $taxPerItem + * @param float $taxDiff */ - public function __construct(OrderDeliveryEntity $delivery, int $alreadyRefundedQuantity) + public function __construct(OrderDeliveryEntity $delivery, int $alreadyRefundedQuantity, float $taxTotal, float $taxPerItem, float $taxDiff) { $this->delivery = $delivery; $this->alreadyRefundedQty = $alreadyRefundedQuantity; + + parent::__construct($taxTotal, $taxPerItem, $taxDiff); } @@ -49,6 +54,7 @@ public function toArray(): array $this->delivery->getShippingCosts()->getTotalPrice(), 0, 0, + 0, $this->alreadyRefundedQty ); } diff --git a/src/Components/RefundManager/RefundData/OrderItem/ProductItem.php b/src/Components/RefundManager/RefundData/OrderItem/ProductItem.php index 88e566bf8..34dc546ab 100644 --- a/src/Components/RefundManager/RefundData/OrderItem/ProductItem.php +++ b/src/Components/RefundManager/RefundData/OrderItem/ProductItem.php @@ -21,6 +21,11 @@ class ProductItem extends AbstractItem */ private $promotionAffectedQuantity; + /** + * @var float + */ + private $promotionTaxValue; + /** * @var int */ @@ -31,13 +36,18 @@ class ProductItem extends AbstractItem * @param OrderLineItemEntity $lineItem * @param array $promotionCompositions * @param int $alreadyRefundedQuantity + * @param float $taxTotal + * @param float $taxPerItem + * @param float $taxDiff */ - public function __construct(OrderLineItemEntity $lineItem, array $promotionCompositions, int $alreadyRefundedQuantity) + public function __construct(OrderLineItemEntity $lineItem, array $promotionCompositions, int $alreadyRefundedQuantity, float $taxTotal, float $taxPerItem, float $taxDiff) { $this->lineItem = $lineItem; $this->alreadyRefundedQty = $alreadyRefundedQuantity; $this->extractPromotionDiscounts($promotionCompositions); + + parent::__construct($taxTotal, $taxPerItem, $taxDiff); } /** @@ -48,6 +58,7 @@ private function extractPromotionDiscounts(array $promotionCompositions) { $this->promotionDiscount = 0; $this->promotionAffectedQuantity = 0; + $this->promotionTaxValue = 0; foreach ($promotionCompositions as $composition) { foreach ($composition as $compItem) { @@ -56,6 +67,7 @@ private function extractPromotionDiscounts(array $promotionCompositions) if ($compItem['id'] === $this->lineItem->getReferencedId()) { $this->promotionDiscount += round((float)$compItem['discount'], 2); $this->promotionAffectedQuantity += (int)$compItem['quantity']; + $this->promotionTaxValue += round((float)$compItem['taxValue'], 2); } } } @@ -77,6 +89,7 @@ public function toArray(): array $this->lineItem->getTotalPrice(), $this->promotionDiscount, $this->promotionAffectedQuantity, + $this->promotionTaxValue, $this->alreadyRefundedQty ); } diff --git a/src/Components/RefundManager/RefundData/OrderItem/PromotionItem.php b/src/Components/RefundManager/RefundData/OrderItem/PromotionItem.php index 63cf3fac5..a7818f08e 100644 --- a/src/Components/RefundManager/RefundData/OrderItem/PromotionItem.php +++ b/src/Components/RefundManager/RefundData/OrderItem/PromotionItem.php @@ -27,8 +27,11 @@ class PromotionItem extends AbstractItem /** * @param OrderDeliveryEntity|OrderLineItemEntity $lineItem * @param int $alreadyRefundedQuantity + * @param float $taxTotal + * @param float $taxPerItem + * @param float $taxDiff */ - private function __construct($lineItem, int $alreadyRefundedQuantity) + private function __construct($lineItem, int $alreadyRefundedQuantity, float $taxTotal, float $taxPerItem, float $taxDiff) { if ($lineItem instanceof OrderDeliveryEntity) { $this->orderDeliveryItem = $lineItem; @@ -39,26 +42,34 @@ private function __construct($lineItem, int $alreadyRefundedQuantity) } $this->alreadyRefundedQty = $alreadyRefundedQuantity; + + parent::__construct($taxTotal, $taxPerItem, $taxDiff); } /** * @param OrderLineItemEntity $lineItem * @param int $alreadyRefundedQuantity + * @param float $taxTotal + * @param float $taxPerItem + * @param float $taxDiff * @return PromotionItem */ - public static function fromOrderLineItem(OrderLineItemEntity $lineItem, int $alreadyRefundedQuantity) + public static function fromOrderLineItem(OrderLineItemEntity $lineItem, int $alreadyRefundedQuantity, float $taxTotal, float $taxPerItem, float $taxDiff) { - return new PromotionItem($lineItem, $alreadyRefundedQuantity); + return new PromotionItem($lineItem, $alreadyRefundedQuantity, $taxTotal, $taxPerItem, $taxDiff); } /** * @param OrderDeliveryEntity $lineItem * @param int $alreadyRefundedQuantity + * @param float $taxTotal + * @param float $taxPerItem + * @param float $taxDiff * @return PromotionItem */ - public static function fromOrderDeliveryItem(OrderDeliveryEntity $lineItem, int $alreadyRefundedQuantity) + public static function fromOrderDeliveryItem(OrderDeliveryEntity $lineItem, int $alreadyRefundedQuantity, float $taxTotal, float $taxPerItem, float $taxDiff) { - return new PromotionItem($lineItem, $alreadyRefundedQuantity); + return new PromotionItem($lineItem, $alreadyRefundedQuantity, $taxTotal, $taxPerItem, $taxDiff); } /** @@ -78,6 +89,7 @@ public function toArray(): array $this->orderLineItem->getTotalPrice(), 0, 0, + 0, $this->alreadyRefundedQty ); } else { @@ -103,6 +115,7 @@ public function toArray(): array $this->orderDeliveryItem->getShippingCosts()->getTotalPrice(), 0, 0, + 0, $this->alreadyRefundedQty ); } diff --git a/src/Components/RefundManager/RefundData/RefundData.php b/src/Components/RefundManager/RefundData/RefundData.php index 2b95550ed..57b972c7d 100644 --- a/src/Components/RefundManager/RefundData/RefundData.php +++ b/src/Components/RefundManager/RefundData/RefundData.php @@ -44,6 +44,11 @@ class RefundData */ private $roundingItemTotal; + /** + * @var string + */ + private $taxStatus; + /** * @param AbstractItem[] $cartItems * @param Refund[] $refunds @@ -53,7 +58,7 @@ class RefundData * @param float $amountRemaining * @param float $roundingItemTotal */ - public function __construct(array $cartItems, array $refunds, float $amountVouchers, float $amountPendingRefunds, float $amountCompletedRefunds, float $amountRemaining, float $roundingItemTotal) + public function __construct(array $cartItems, array $refunds, float $amountVouchers, float $amountPendingRefunds, float $amountCompletedRefunds, float $amountRemaining, float $roundingItemTotal, string $taxStatus) { $this->orderItems = $cartItems; $this->refunds = $refunds; @@ -62,6 +67,7 @@ public function __construct(array $cartItems, array $refunds, float $amountVouch $this->amountCompletedRefunds = $amountCompletedRefunds; $this->amountRemaining = $amountRemaining; $this->roundingItemTotal = $roundingItemTotal; + $this->taxStatus = $taxStatus; } /** @@ -120,6 +126,14 @@ public function getRoundingItemTotal(): float return $this->roundingItemTotal; } + /** + * @return string + */ + public function getTaxStatus(): string + { + return $this->taxStatus; + } + /** * @return array @@ -155,6 +169,7 @@ public function toArray() ], 'cart' => $hydratedOrderItems, 'refunds' => $refundsArray, + 'taxStatus' => $this->taxStatus, ]; } } diff --git a/src/Resources/app/administration/src/module/mollie-payments/components/mollie-refund-manager/grids/ShopwareOrderGrid.js b/src/Resources/app/administration/src/module/mollie-payments/components/mollie-refund-manager/grids/ShopwareOrderGrid.js index eb316cfd1..2f87ef979 100644 --- a/src/Resources/app/administration/src/module/mollie-payments/components/mollie-refund-manager/grids/ShopwareOrderGrid.js +++ b/src/Resources/app/administration/src/module/mollie-payments/components/mollie-refund-manager/grids/ShopwareOrderGrid.js @@ -59,6 +59,11 @@ export default class ShopwareOrderGrid { width: '150px', align: 'center', }, + { + label: '', + property: 'inputConsiderTax', + align: 'center', + }, { label: '', property: 'inputConsiderPromotion', diff --git a/src/Resources/app/administration/src/module/mollie-payments/components/mollie-refund-manager/index.js b/src/Resources/app/administration/src/module/mollie-payments/components/mollie-refund-manager/index.js index 33a22ed5e..5f83fd8ba 100644 --- a/src/Resources/app/administration/src/module/mollie-payments/components/mollie-refund-manager/index.js +++ b/src/Resources/app/administration/src/module/mollie-payments/components/mollie-refund-manager/index.js @@ -200,6 +200,13 @@ Component.register('mollie-refund-manager', { return this.itemService.isRefundable(item); }, + /** + * Gets if the order tax status is gross + */ + isTaxStatusGross() { + return this.order.taxStatus === 'gross'; + }, + /** * This automatically selects all items by * assigning their maximum quantity to be refunded. @@ -260,6 +267,17 @@ Component.register('mollie-refund-manager', { this._calculateFinalAmount(); }, + /** + * This will be executed if the user changes the + * configuration to either activate or deactivate the + * Tax Refund in case of Net Orders. + * @param item + */ + onItemRefundTaxChanged(item) { + this.itemService.onRefundTaxChanged(item); + this._calculateFinalAmount(); + }, + /** * This will be executed if the user changes the * configuration to either allow or forbid the deduction diff --git a/src/Resources/app/administration/src/module/mollie-payments/components/mollie-refund-manager/mollie-refund-manager.html.twig b/src/Resources/app/administration/src/module/mollie-payments/components/mollie-refund-manager/mollie-refund-manager.html.twig index 56ea743f3..f7f718e42 100644 --- a/src/Resources/app/administration/src/module/mollie-payments/components/mollie-refund-manager/mollie-refund-manager.html.twig +++ b/src/Resources/app/administration/src/module/mollie-payments/components/mollie-refund-manager/mollie-refund-manager.html.twig @@ -85,6 +85,17 @@ {{ item.refundAmount | currency(order.currency.shortName) }} +