Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG-2.0.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
28 changes: 28 additions & 0 deletions UPGRADE-2.0.md
Original file line number Diff line number Diff line change
@@ -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
<service id="sylius_invoicing.generator.invoice" class="Sylius\InvoicingPlugin\Generator\InvoiceGenerator">
<argument type="service" id="sylius_invoicing.generator.invoice_identifier" />
<argument type="service" id="sylius_invoicing.generator.invoice_number" />
<argument type="service" id="sylius_invoicing.custom_factory.invoice" />
<argument type="service" id="sylius_invoicing.factory.billing_data" />
<argument type="service" id="sylius_invoicing.factory.shop_billing_data" />
<argument type="service" id="sylius_invoicing.converter.order_item_units_to_line_items" />
<argument type="service" id="sylius_invoicing.converter.shipping_adjustments_to_line_items" />
<argument type="service" id="sylius_invoicing.converter.tax_items" />
+ <argument type="service" id="sylius_invoicing.converter.order_item_to_line_items" />
</service>
```

### 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.
Expand Down
5 changes: 5 additions & 0 deletions config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,10 @@
class="Sylius\InvoicingPlugin\Provider\UnitNetPriceProvider"
/>
<service id="Sylius\InvoicingPlugin\Provider\UnitNetPriceProviderInterface" alias="sylius_invoicing.provider.unit_net_price" />

<service id="sylius_invoicing.provider.item_net_prices" class="Sylius\InvoicingPlugin\Provider\ItemNetPricesProvider">
<argument type="service" id="sylius.distributor.integer" />
</service>
<service id="Sylius\InvoicingPlugin\Provider\ItemNetPricesProviderInterface" alias="sylius_invoicing.provider.item_net_prices" />
</services>
</container>
6 changes: 6 additions & 0 deletions config/services/converters.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
<argument type="service" id="sylius_invoicing.provider.unit_net_price" />
</service>

<service id="sylius_invoicing.converter.order_item_to_line_items" class="Sylius\InvoicingPlugin\Converter\OrderItemsToLineItemsConverter">
<argument type="service" id="sylius_invoicing.provider.tax_rate_percentage" />
<argument type="service" id="sylius_invoicing.factory.line_item" />
<argument type="service" id="sylius_invoicing.provider.item_net_prices" />
</service>

<service id="sylius_invoicing.converter.shipping_adjustments_to_line_items" class="Sylius\InvoicingPlugin\Converter\ShippingAdjustmentsToLineItemsConverter">
<argument type="service" id="sylius_invoicing.provider.tax_rate_percentage" />
<argument type="service" id="sylius_invoicing.factory.line_item" />
Expand Down
1 change: 1 addition & 0 deletions config/services/generators.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<argument type="service" id="sylius_invoicing.converter.order_item_units_to_line_items" />
<argument type="service" id="sylius_invoicing.converter.shipping_adjustments_to_line_items" />
<argument type="service" id="sylius_invoicing.converter.tax_items" />
<argument type="service" id="sylius_invoicing.converter.order_item_to_line_items" />
</service>
<service id="Sylius\InvoicingPlugin\Generator\InvoiceGeneratorInterface" alias="sylius_invoicing.generator.invoice" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
110 changes: 110 additions & 0 deletions src/Converter/OrderItemsToLineItemsConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\InvoicingPlugin\Converter;

use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\OrderItemInterface;
use Sylius\Component\Core\Model\OrderItemUnitInterface;
use Sylius\InvoicingPlugin\Entity\LineItemInterface;
use Sylius\InvoicingPlugin\Factory\LineItemFactoryInterface;
use Sylius\InvoicingPlugin\Provider\ItemNetPricesProviderInterface;
use Sylius\InvoicingPlugin\Provider\TaxRatePercentageProviderInterface;
use Webmozart\Assert\Assert;

final class OrderItemsToLineItemsConverter implements LineItemsConverterInterface
{
public function __construct(
private readonly TaxRatePercentageProviderInterface $taxRatePercentageProvider,
private readonly LineItemFactoryInterface $lineItemFactory,
private readonly ItemNetPricesProviderInterface $unitNetPriceProvider,
) {
}

public function convert(OrderInterface $order): array
{
$lineItems = [];

/** @var OrderItemInterface $item */
foreach ($order->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;
}
}
22 changes: 21 additions & 1 deletion src/Generator/InvoiceGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(),
Expand All @@ -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),
Expand Down
67 changes: 67 additions & 0 deletions src/Provider/ItemNetPricesProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\InvoicingPlugin\Provider;

use Sylius\Component\Core\Distributor\IntegerDistributorInterface;
use Sylius\Component\Core\Model\AdjustmentInterface;
use Sylius\Component\Core\Model\OrderItemInterface;
use Sylius\Component\Core\Model\OrderItemUnitInterface;

final class ItemNetPricesProvider implements ItemNetPricesProviderInterface
{
public function __construct(
private IntegerDistributorInterface $distributor,
) {
}

public function getItemNetPrices(OrderItemInterface $orderItem): array
{
/** @var OrderItemUnitInterface|null $orderItemUnit */
$orderItemUnit = $orderItem->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);
}
}
21 changes: 21 additions & 0 deletions src/Provider/ItemNetPricesProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\InvoicingPlugin\Provider;

use Sylius\Component\Core\Model\OrderItemInterface;

interface ItemNetPricesProviderInterface
{
public function getItemNetPrices(OrderItemInterface $orderItem): array;
}
1 change: 1 addition & 0 deletions src/Provider/UnitNetPriceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Sylius\Component\Core\Model\AdjustmentInterface;
use Sylius\Component\Core\Model\OrderItemUnitInterface;

/** @deprecated since Sylius Invoicing Plugin 2.0 and will be removed in Sylius Invoicing Plugin 3.0. */
final class UnitNetPriceProvider implements UnitNetPriceProviderInterface
{
public function getUnitNetPrice(OrderItemUnitInterface $orderItemUnit): int
Expand Down