Skip to content

Commit 8265c90

Browse files
committed
Fixing the problem with incorrect net price calculations on invoices
1 parent fe341c1 commit 8265c90

11 files changed

+265
-2
lines changed

CHANGELOG-2.0.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
### v2.0.3 (2025-10-31)
4+
5+
- [#400](https://github.com/Sylius/InvoicingPlugin/pull/400) Fix InvoiceLineItem net price calculator ([@tomkalon](https://github.com/tomkalon))
6+
37
### v2.0.2 (2025-07-03)
48

59
- [#373](https://github.com/Sylius/InvoicingPlugin/pull/373) Add sylius/test-application ([@Wojdylak](https://github.com/Wojdylak))

UPGRADE-2.0.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
# UPGRADE FROM 2.0.2 TO 2.0.3
2+
3+
### Deprecations
4+
- `Sylius\InvoicingPlugin\Provider\UnitNetPriceProvider`**deprecated since 2.0** and will be removed in 3.0.
5+
- The `orderItemUnitsToLineItemsConverter` argument and property in `Sylius\InvoicingPlugin\Generator\InvoiceGenerator`**deprecated since 2.0.3**, to be removed in 3.0.
6+
- Using `InvoiceGenerator` without providing `orderItemsToLineItemsConverter` is **deprecated since 2.0** and will become an error in 3.0.
7+
8+
```diff
9+
<service id="sylius_invoicing.generator.invoice" class="Sylius\InvoicingPlugin\Generator\InvoiceGenerator">
10+
<argument type="service" id="sylius_invoicing.generator.invoice_identifier" />
11+
<argument type="service" id="sylius_invoicing.generator.invoice_number" />
12+
<argument type="service" id="sylius_invoicing.custom_factory.invoice" />
13+
<argument type="service" id="sylius_invoicing.factory.billing_data" />
14+
<argument type="service" id="sylius_invoicing.factory.shop_billing_data" />
15+
<argument type="service" id="sylius_invoicing.converter.order_item_units_to_line_items" />
16+
<argument type="service" id="sylius_invoicing.converter.shipping_adjustments_to_line_items" />
17+
<argument type="service" id="sylius_invoicing.converter.tax_items" />
18+
+ <argument type="service" id="sylius_invoicing.converter.order_item_to_line_items" />
19+
</service>
20+
```
21+
22+
### Changed
23+
- `InvoiceGenerator` now prefers `orderItemsToLineItemsConverter`; if it is not provided, it falls back to the (deprecated) `orderItemUnitsToLineItemsConverter` and emits deprecation warnings.
24+
25+
### Removed (since 3.0)
26+
- `UnitNetPriceProvider`
27+
- `orderItemUnitsToLineItemsConverter` from `InvoiceGenerator` (argument and property)
28+
129
# UPGRADE FROM 1.X TO 2.0
230

331
1. Support for Sylius 2.0 has been added, it is now the recommended Sylius version to use with InvoicingPlugin.

config/services.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,10 @@
7373
class="Sylius\InvoicingPlugin\Provider\UnitNetPriceProvider"
7474
/>
7575
<service id="Sylius\InvoicingPlugin\Provider\UnitNetPriceProviderInterface" alias="sylius_invoicing.provider.unit_net_price" />
76+
77+
<service id="sylius_invoicing.provider.item_net_prices" class="Sylius\InvoicingPlugin\Provider\ItemNetPricesProvider">
78+
<argument type="service" id="sylius.distributor.integer" />
79+
</service>
80+
<service id="Sylius\InvoicingPlugin\Provider\ItemNetPricesProviderInterface" alias="sylius_invoicing.provider.item_net_prices" />
7681
</services>
7782
</container>

config/services/converters.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
<argument type="service" id="sylius_invoicing.provider.unit_net_price" />
2424
</service>
2525

26+
<service id="sylius_invoicing.converter.order_item_to_line_items" class="Sylius\InvoicingPlugin\Converter\OrderItemsToLineItemsConverter">
27+
<argument type="service" id="sylius_invoicing.provider.tax_rate_percentage" />
28+
<argument type="service" id="sylius_invoicing.factory.line_item" />
29+
<argument type="service" id="sylius_invoicing.provider.item_net_prices" />
30+
</service>
31+
2632
<service id="sylius_invoicing.converter.shipping_adjustments_to_line_items" class="Sylius\InvoicingPlugin\Converter\ShippingAdjustmentsToLineItemsConverter">
2733
<argument type="service" id="sylius_invoicing.provider.tax_rate_percentage" />
2834
<argument type="service" id="sylius_invoicing.factory.line_item" />

config/services/generators.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<argument type="service" id="sylius_invoicing.converter.order_item_units_to_line_items" />
3636
<argument type="service" id="sylius_invoicing.converter.shipping_adjustments_to_line_items" />
3737
<argument type="service" id="sylius_invoicing.converter.tax_items" />
38+
<argument type="service" id="sylius_invoicing.converter.order_item_to_line_items" />
3839
</service>
3940
<service id="Sylius\InvoicingPlugin\Generator\InvoiceGeneratorInterface" alias="sylius_invoicing.generator.invoice" />
4041

features/managing_invoices/seeing_invoice_with_taxes_included_in_price_and_promotions_applied.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ Feature: Seeing included in price taxes and promotions applied on an invoice
2222

2323
Scenario: Seeing proper taxes and promotions on an invoice
2424
When I view the summary of the invoice for order "#00000666"
25-
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
25+
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
2626
And it should have a tax item "23%" with amount "18.70" in "USD" currency
2727
And its total should be "110.00" in "USD" currency
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sylius\InvoicingPlugin\Converter;
15+
16+
use Sylius\Component\Core\Model\OrderInterface;
17+
use Sylius\Component\Core\Model\OrderItemInterface;
18+
use Sylius\Component\Core\Model\OrderItemUnitInterface;
19+
use Sylius\InvoicingPlugin\Entity\LineItemInterface;
20+
use Sylius\InvoicingPlugin\Factory\LineItemFactoryInterface;
21+
use Sylius\InvoicingPlugin\Provider\ItemNetPricesProviderInterface;
22+
use Sylius\InvoicingPlugin\Provider\TaxRatePercentageProviderInterface;
23+
use Webmozart\Assert\Assert;
24+
25+
final class OrderItemsToLineItemsConverter implements LineItemsConverterInterface
26+
{
27+
public function __construct(
28+
private readonly TaxRatePercentageProviderInterface $taxRatePercentageProvider,
29+
private readonly LineItemFactoryInterface $lineItemFactory,
30+
private readonly ItemNetPricesProviderInterface $unitNetPriceProvider,
31+
) {
32+
}
33+
34+
public function convert(OrderInterface $order): array
35+
{
36+
$lineItems = [];
37+
38+
/** @var OrderItemInterface $item */
39+
foreach ($order->getItems() as $item) {
40+
foreach ($this->convertOrderItemToLineItems($item) as $lineItem) {
41+
$lineItems = $this->addLineItem($lineItem, $lineItems);
42+
}
43+
}
44+
45+
return $lineItems;
46+
}
47+
48+
private function convertOrderItemToLineItems(OrderItemInterface $item): array
49+
{
50+
$lineItems = [];
51+
$units = $item->getUnits()->getValues();
52+
$unitNetPrices = $this->unitNetPriceProvider->getItemNetPrices($item);
53+
54+
/** @var OrderItemUnitInterface $unit */
55+
foreach ($units as $index => $unit) {
56+
$lineItems = $this->addLineItem($this->convertOrderItemUnitToLineItem($unit, (int) $unitNetPrices[$index]), $lineItems);
57+
}
58+
59+
return $lineItems;
60+
}
61+
62+
private function convertOrderItemUnitToLineItem(OrderItemUnitInterface $unit, int $unitNetPrice): LineItemInterface
63+
{
64+
/** @var OrderItemInterface $item */
65+
$item = $unit->getOrderItem();
66+
67+
$grossValue = $unit->getTotal();
68+
$taxAmount = $unit->getTaxTotal();
69+
$discountedUnitNetPrice = $grossValue - $taxAmount;
70+
71+
/** @var string|null $productName */
72+
$productName = $item->getProductName();
73+
Assert::notNull($productName);
74+
75+
$variant = $item->getVariant();
76+
77+
return $this->lineItemFactory->createWithData(
78+
$productName,
79+
1,
80+
$unitNetPrice,
81+
$discountedUnitNetPrice,
82+
$discountedUnitNetPrice,
83+
$taxAmount,
84+
$grossValue,
85+
$item->getVariantName(),
86+
$variant !== null ? $variant->getCode() : null,
87+
$this->taxRatePercentageProvider->provideFromAdjustable($unit),
88+
);
89+
}
90+
91+
/**
92+
* @param LineItemInterface[] $lineItems
93+
*
94+
* @return LineItemInterface[]
95+
*/
96+
private function addLineItem(LineItemInterface $newLineItem, array $lineItems): array
97+
{
98+
foreach ($lineItems as $lineItem) {
99+
if ($lineItem->compare($newLineItem)) {
100+
$lineItem->merge($newLineItem);
101+
102+
return $lineItems;
103+
}
104+
}
105+
106+
$lineItems[] = $newLineItem;
107+
108+
return $lineItems;
109+
}
110+
}

src/Generator/InvoiceGenerator.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,23 @@ public function __construct(
3636
private readonly LineItemsConverterInterface $orderItemUnitsToLineItemsConverter,
3737
private readonly LineItemsConverterInterface $shippingAdjustmentsToLineItemsConverter,
3838
private readonly TaxItemsConverterInterface $taxItemsConverter,
39+
private readonly ?LineItemsConverterInterface $orderItemsToLineItemsConverter = null,
3940
) {
41+
if (null === $this->orderItemsToLineItemsConverter) {
42+
trigger_deprecation(
43+
'sylius/invoicing-plugin',
44+
'2.0',
45+
'Not passing a "%s" to "%s" is deprecated and will be required in Sylius Invoicing Plugin 3.0.',
46+
LineItemsConverterInterface::class,
47+
self::class,
48+
);
49+
trigger_deprecation(
50+
'sylius/invoicing-plugin',
51+
'2.0',
52+
'Deprecated constructor argument "$orderItemUnitsToLineItemsConverter" passed to %s. Use "$orderItemsToLineItemsConverter" instead.',
53+
self::class,
54+
);
55+
}
4056
}
4157

4258
public function generateForOrder(OrderInterface $order, \DateTimeInterface $date): InvoiceInterface
@@ -50,6 +66,10 @@ public function generateForOrder(OrderInterface $order, \DateTimeInterface $date
5066
$paymentState = $order->getPaymentState() === OrderPaymentStates::STATE_PAID ?
5167
InvoiceInterface::PAYMENT_STATE_COMPLETED : InvoiceInterface::PAYMENT_STATE_PENDING;
5268

69+
$lineItemsFromOrder = $this->orderItemsToLineItemsConverter !== null
70+
? $this->orderItemsToLineItemsConverter->convert($order)
71+
: $this->orderItemUnitsToLineItemsConverter->convert($order);
72+
5373
return $this->invoiceFactory->createForData(
5474
$this->uuidInvoiceIdentifierGenerator->generate(),
5575
$this->sequentialInvoiceNumberGenerator->generate(),
@@ -60,7 +80,7 @@ public function generateForOrder(OrderInterface $order, \DateTimeInterface $date
6080
$order->getLocaleCode(),
6181
$order->getTotal(),
6282
new ArrayCollection(array_merge(
63-
$this->orderItemUnitsToLineItemsConverter->convert($order),
83+
$lineItemsFromOrder,
6484
$this->shippingAdjustmentsToLineItemsConverter->convert($order),
6585
)),
6686
$this->taxItemsConverter->convert($order),
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sylius\InvoicingPlugin\Provider;
15+
16+
use Sylius\Component\Core\Distributor\IntegerDistributorInterface;
17+
use Sylius\Component\Core\Model\AdjustmentInterface;
18+
use Sylius\Component\Core\Model\OrderItemInterface;
19+
use Sylius\Component\Core\Model\OrderItemUnitInterface;
20+
21+
final class ItemNetPricesProvider implements ItemNetPricesProviderInterface
22+
{
23+
public function __construct(
24+
private IntegerDistributorInterface $distributor,
25+
) {
26+
}
27+
28+
public function getItemNetPrices(OrderItemInterface $orderItem): array
29+
{
30+
/** @var OrderItemUnitInterface|null $orderItemUnit */
31+
$orderItemUnit = $orderItem->getUnits()->first();
32+
33+
if (null === $orderItemUnit) {
34+
return [];
35+
}
36+
37+
$taxRate = $this->getTaxRate($orderItemUnit);
38+
$grossTotal = $orderItem->getQuantity() * $orderItem->getUnitPrice();
39+
40+
$itemNetPrice = ($grossTotal / (100 + ($taxRate))) * 100;
41+
42+
return array_reverse($this->distributor->distribute(round($itemNetPrice, 1), $orderItem->getQuantity()));
43+
}
44+
45+
private function getTaxRate(OrderItemUnitInterface $orderItemUnit): int
46+
{
47+
$taxRate = 0;
48+
49+
/** @var AdjustmentInterface $adjustment */
50+
foreach ($orderItemUnit->getAdjustments(AdjustmentInterface::TAX_ADJUSTMENT) as $adjustment) {
51+
if (!$adjustment->isNeutral()) {
52+
continue;
53+
}
54+
55+
try {
56+
$details = $adjustment->getDetails();
57+
if (is_array($details) && array_key_exists('taxRateAmount', $details) && is_numeric($details['taxRateAmount'])) {
58+
$taxRate = $details['taxRateAmount'] * 100;
59+
}
60+
} catch (\Throwable $e) {
61+
throw new \RuntimeException('Tax rate amount is not valid', 0, $e);
62+
}
63+
}
64+
65+
return (int) round($taxRate);
66+
}
67+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sylius\InvoicingPlugin\Provider;
15+
16+
use Sylius\Component\Core\Model\OrderItemInterface;
17+
18+
interface ItemNetPricesProviderInterface
19+
{
20+
public function getItemNetPrices(OrderItemInterface $orderItem): array;
21+
}

0 commit comments

Comments
 (0)