diff --git a/CHANGELOG-2.0.md b/CHANGELOG-2.0.md index c68dbd6f..ebe388c4 100644 --- a/CHANGELOG-2.0.md +++ b/CHANGELOG-2.0.md @@ -1,5 +1,9 @@ # CHANGELOG +### v2.0.3 (2025-10-31) + +- [#400](https://github.com/Sylius/InvoicingPlugin/pull/400) Fix InvoiceLineItem net price calculator ([@tomkalon](https://github.com/tomkalon)) + ### v2.0.2 (2025-07-03) - [#373](https://github.com/Sylius/InvoicingPlugin/pull/373) Add sylius/test-application ([@Wojdylak](https://github.com/Wojdylak)) diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index 052c0a91..a980437a 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -1,3 +1,31 @@ +# UPGRADE FROM 2.0.2 TO 2.0.3 + +### Deprecations +- `Sylius\InvoicingPlugin\Provider\UnitNetPriceProvider` — **deprecated since 2.0** and will be removed in 3.0. +- The `orderItemUnitsToLineItemsConverter` argument and property in `Sylius\InvoicingPlugin\Generator\InvoiceGenerator` — **deprecated since 2.0.3**, to be removed in 3.0. +- Using `InvoiceGenerator` without providing `orderItemsToLineItemsConverter` is **deprecated since 2.0** and will become an error in 3.0. + +```diff + + + + + + + + + ++ + +``` + +### Changed +- `InvoiceGenerator` now prefers `orderItemsToLineItemsConverter`; if it is not provided, it falls back to the (deprecated) `orderItemUnitsToLineItemsConverter` and emits deprecation warnings. + +### Removed (since 3.0) +- `UnitNetPriceProvider` +- `orderItemUnitsToLineItemsConverter` from `InvoiceGenerator` (argument and property) + # UPGRADE FROM 1.X TO 2.0 1. Support for Sylius 2.0 has been added, it is now the recommended Sylius version to use with InvoicingPlugin. diff --git a/config/services.xml b/config/services.xml index 47ea616a..ae39c664 100644 --- a/config/services.xml +++ b/config/services.xml @@ -73,5 +73,10 @@ class="Sylius\InvoicingPlugin\Provider\UnitNetPriceProvider" /> + + + + + diff --git a/config/services/converters.xml b/config/services/converters.xml index e28df52e..9fb60ab2 100644 --- a/config/services/converters.xml +++ b/config/services/converters.xml @@ -23,6 +23,12 @@ + + + + + + diff --git a/config/services/generators.xml b/config/services/generators.xml index cfca0642..533baa54 100644 --- a/config/services/generators.xml +++ b/config/services/generators.xml @@ -35,6 +35,7 @@ + diff --git a/features/managing_invoices/seeing_invoice_with_taxes_included_in_price_and_promotions_applied.feature b/features/managing_invoices/seeing_invoice_with_taxes_included_in_price_and_promotions_applied.feature index 4662f1c9..6eb153ac 100644 --- a/features/managing_invoices/seeing_invoice_with_taxes_included_in_price_and_promotions_applied.feature +++ b/features/managing_invoices/seeing_invoice_with_taxes_included_in_price_and_promotions_applied.feature @@ -22,6 +22,6 @@ Feature: Seeing included in price taxes and promotions applied on an invoice Scenario: Seeing proper taxes and promotions on an invoice When I view the summary of the invoice for order "#00000666" - Then it should have 2 "PHP T-Shirt" items with unit net price "50.65", discounted unit net price "40.65", net value "81.30", tax total "18.70" and total "100.00" in "USD" currency + Then it should have 2 "PHP T-Shirt" items with unit net price "48.78", discounted unit net price "40.65", net value "81.30", tax total "18.70" and total "100.00" in "USD" currency And it should have a tax item "23%" with amount "18.70" in "USD" currency And its total should be "110.00" in "USD" currency diff --git a/src/Converter/OrderItemsToLineItemsConverter.php b/src/Converter/OrderItemsToLineItemsConverter.php new file mode 100644 index 00000000..e5e700f9 --- /dev/null +++ b/src/Converter/OrderItemsToLineItemsConverter.php @@ -0,0 +1,110 @@ +getItems() as $item) { + foreach ($this->convertOrderItemToLineItems($item) as $lineItem) { + $lineItems = $this->addLineItem($lineItem, $lineItems); + } + } + + return $lineItems; + } + + private function convertOrderItemToLineItems(OrderItemInterface $item): array + { + $lineItems = []; + $units = $item->getUnits()->getValues(); + $unitNetPrices = $this->unitNetPriceProvider->getItemNetPrices($item); + + /** @var OrderItemUnitInterface $unit */ + foreach ($units as $index => $unit) { + $lineItems = $this->addLineItem($this->convertOrderItemUnitToLineItem($unit, (int) $unitNetPrices[$index]), $lineItems); + } + + return $lineItems; + } + + private function convertOrderItemUnitToLineItem(OrderItemUnitInterface $unit, int $unitNetPrice): LineItemInterface + { + /** @var OrderItemInterface $item */ + $item = $unit->getOrderItem(); + + $grossValue = $unit->getTotal(); + $taxAmount = $unit->getTaxTotal(); + $discountedUnitNetPrice = $grossValue - $taxAmount; + + /** @var string|null $productName */ + $productName = $item->getProductName(); + Assert::notNull($productName); + + $variant = $item->getVariant(); + + return $this->lineItemFactory->createWithData( + $productName, + 1, + $unitNetPrice, + $discountedUnitNetPrice, + $discountedUnitNetPrice, + $taxAmount, + $grossValue, + $item->getVariantName(), + $variant !== null ? $variant->getCode() : null, + $this->taxRatePercentageProvider->provideFromAdjustable($unit), + ); + } + + /** + * @param LineItemInterface[] $lineItems + * + * @return LineItemInterface[] + */ + private function addLineItem(LineItemInterface $newLineItem, array $lineItems): array + { + foreach ($lineItems as $lineItem) { + if ($lineItem->compare($newLineItem)) { + $lineItem->merge($newLineItem); + + return $lineItems; + } + } + + $lineItems[] = $newLineItem; + + return $lineItems; + } +} diff --git a/src/Generator/InvoiceGenerator.php b/src/Generator/InvoiceGenerator.php index 8241ca4e..c425265a 100644 --- a/src/Generator/InvoiceGenerator.php +++ b/src/Generator/InvoiceGenerator.php @@ -36,7 +36,23 @@ public function __construct( private readonly LineItemsConverterInterface $orderItemUnitsToLineItemsConverter, private readonly LineItemsConverterInterface $shippingAdjustmentsToLineItemsConverter, private readonly TaxItemsConverterInterface $taxItemsConverter, + private readonly ?LineItemsConverterInterface $orderItemsToLineItemsConverter = null, ) { + if (null === $this->orderItemsToLineItemsConverter) { + trigger_deprecation( + 'sylius/invoicing-plugin', + '2.0', + 'Not passing a "%s" to "%s" is deprecated and will be required in Sylius Invoicing Plugin 3.0.', + LineItemsConverterInterface::class, + self::class, + ); + trigger_deprecation( + 'sylius/invoicing-plugin', + '2.0', + 'Deprecated constructor argument "$orderItemUnitsToLineItemsConverter" passed to %s. Use "$orderItemsToLineItemsConverter" instead.', + self::class, + ); + } } public function generateForOrder(OrderInterface $order, \DateTimeInterface $date): InvoiceInterface @@ -50,6 +66,10 @@ public function generateForOrder(OrderInterface $order, \DateTimeInterface $date $paymentState = $order->getPaymentState() === OrderPaymentStates::STATE_PAID ? InvoiceInterface::PAYMENT_STATE_COMPLETED : InvoiceInterface::PAYMENT_STATE_PENDING; + $lineItemsFromOrder = $this->orderItemsToLineItemsConverter !== null + ? $this->orderItemsToLineItemsConverter->convert($order) + : $this->orderItemUnitsToLineItemsConverter->convert($order); + return $this->invoiceFactory->createForData( $this->uuidInvoiceIdentifierGenerator->generate(), $this->sequentialInvoiceNumberGenerator->generate(), @@ -60,7 +80,7 @@ public function generateForOrder(OrderInterface $order, \DateTimeInterface $date $order->getLocaleCode(), $order->getTotal(), new ArrayCollection(array_merge( - $this->orderItemUnitsToLineItemsConverter->convert($order), + $lineItemsFromOrder, $this->shippingAdjustmentsToLineItemsConverter->convert($order), )), $this->taxItemsConverter->convert($order), diff --git a/src/Provider/ItemNetPricesProvider.php b/src/Provider/ItemNetPricesProvider.php new file mode 100644 index 00000000..52213ebf --- /dev/null +++ b/src/Provider/ItemNetPricesProvider.php @@ -0,0 +1,67 @@ +getUnits()->first(); + + if (null === $orderItemUnit) { + return []; + } + + $taxRate = $this->getTaxRate($orderItemUnit); + $grossTotal = $orderItem->getQuantity() * $orderItem->getUnitPrice(); + + $itemNetPrice = ($grossTotal / (100 + ($taxRate))) * 100; + + return array_reverse($this->distributor->distribute(round($itemNetPrice, 1), $orderItem->getQuantity())); + } + + private function getTaxRate(OrderItemUnitInterface $orderItemUnit): int + { + $taxRate = 0; + + /** @var AdjustmentInterface $adjustment */ + foreach ($orderItemUnit->getAdjustments(AdjustmentInterface::TAX_ADJUSTMENT) as $adjustment) { + if (!$adjustment->isNeutral()) { + continue; + } + + try { + $details = $adjustment->getDetails(); + if (is_array($details) && array_key_exists('taxRateAmount', $details) && is_numeric($details['taxRateAmount'])) { + $taxRate = $details['taxRateAmount'] * 100; + } + } catch (\Throwable $e) { + throw new \RuntimeException('Tax rate amount is not valid', 0, $e); + } + } + + return (int) round($taxRate); + } +} diff --git a/src/Provider/ItemNetPricesProviderInterface.php b/src/Provider/ItemNetPricesProviderInterface.php new file mode 100644 index 00000000..0f26e10c --- /dev/null +++ b/src/Provider/ItemNetPricesProviderInterface.php @@ -0,0 +1,21 @@ +