Skip to content

Commit 339feb1

Browse files
committed
MAGETWO-99371: Wrong calculation of invoiced items
- Code refactoring
1 parent f774303 commit 339feb1

File tree

1 file changed

+82
-104
lines changed

1 file changed

+82
-104
lines changed

app/code/Magento/Sales/Model/Service/InvoiceService.php

Lines changed: 82 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@
33
* Copyright © Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
6+
declare(strict_types=1);
7+
68
namespace Magento\Sales\Model\Service;
79

10+
use Magento\Framework\Exception\LocalizedException;
11+
use Magento\Framework\Serialize\Serializer\Json as JsonSerializer;
12+
use Magento\Sales\Api\Data\InvoiceInterface;
13+
use Magento\Sales\Api\Data\InvoiceItemInterface;
14+
use Magento\Sales\Api\Data\OrderItemInterface;
815
use Magento\Sales\Api\InvoiceManagementInterface;
916
use Magento\Sales\Model\Order;
10-
use Magento\Framework\App\ObjectManager;
11-
use Magento\Framework\Serialize\Serializer\Json;
12-
use Magento\Catalog\Model\Product\Type;
17+
use Magento\Sales\Model\Order\Invoice;
1318

1419
/**
1520
* Class InvoiceService
@@ -19,36 +24,26 @@
1924
class InvoiceService implements InvoiceManagementInterface
2025
{
2126
/**
22-
* Repository
23-
*
2427
* @var \Magento\Sales\Api\InvoiceRepositoryInterface
2528
*/
2629
protected $repository;
2730

2831
/**
29-
* Repository
30-
*
3132
* @var \Magento\Sales\Api\InvoiceCommentRepositoryInterface
3233
*/
3334
protected $commentRepository;
3435

3536
/**
36-
* Search Criteria Builder
37-
*
3837
* @var \Magento\Framework\Api\SearchCriteriaBuilder
3938
*/
4039
protected $criteriaBuilder;
4140

4241
/**
43-
* Filter Builder
44-
*
4542
* @var \Magento\Framework\Api\FilterBuilder
4643
*/
4744
protected $filterBuilder;
4845

4946
/**
50-
* Invoice Notifier
51-
*
5247
* @var \Magento\Sales\Model\Order\InvoiceNotifier
5348
*/
5449
protected $invoiceNotifier;
@@ -64,23 +59,19 @@ class InvoiceService implements InvoiceManagementInterface
6459
protected $orderConverter;
6560

6661
/**
67-
* Serializer interface instance.
68-
*
69-
* @var Json
62+
* @var JsonSerializer
7063
*/
7164
private $serializer;
7265

7366
/**
74-
* Constructor
75-
*
7667
* @param \Magento\Sales\Api\InvoiceRepositoryInterface $repository
7768
* @param \Magento\Sales\Api\InvoiceCommentRepositoryInterface $commentRepository
7869
* @param \Magento\Framework\Api\SearchCriteriaBuilder $criteriaBuilder
7970
* @param \Magento\Framework\Api\FilterBuilder $filterBuilder
8071
* @param \Magento\Sales\Model\Order\InvoiceNotifier $notifier
8172
* @param \Magento\Sales\Api\OrderRepositoryInterface $orderRepository
8273
* @param \Magento\Sales\Model\Convert\Order $orderConverter
83-
* @param Json|null $serializer
74+
* @param JsonSerializer $serializer
8475
*/
8576
public function __construct(
8677
\Magento\Sales\Api\InvoiceRepositoryInterface $repository,
@@ -90,7 +81,7 @@ public function __construct(
9081
\Magento\Sales\Model\Order\InvoiceNotifier $notifier,
9182
\Magento\Sales\Api\OrderRepositoryInterface $orderRepository,
9283
\Magento\Sales\Model\Convert\Order $orderConverter,
93-
Json $serializer = null
84+
JsonSerializer $serializer
9485
) {
9586
$this->repository = $repository;
9687
$this->commentRepository = $commentRepository;
@@ -99,7 +90,7 @@ public function __construct(
9990
$this->invoiceNotifier = $notifier;
10091
$this->orderRepository = $orderRepository;
10192
$this->orderConverter = $orderConverter;
102-
$this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
93+
$this->serializer = $serializer;
10394
}
10495

10596
/**
@@ -140,133 +131,120 @@ public function setVoid($id)
140131
}
141132

142133
/**
143-
* Creates an invoice based on the order and quantities provided
134+
* Creates an invoice based on the order and quantities provided.
135+
*
136+
* Explanation for `if` statements:
137+
* - using qty defined in `$preparedItemsQty` is prioritized
138+
* - if qty is not defined and item is dummy, get ordered qty
139+
* - if qty is not defined, get qty to invoice
140+
* - else qty is 0
144141
*
145142
* @param Order $order
146-
* @param array $qtys
147-
* @return \Magento\Sales\Model\Order\Invoice
148-
* @throws \Magento\Framework\Exception\LocalizedException
143+
* @param array $orderItemsQtyToInvoice
144+
* @return Invoice
145+
* @throws LocalizedException
146+
* @throws \Exception
149147
*/
150-
public function prepareInvoice(Order $order, array $qtys = [])
151-
{
152-
$isQtysEmpty = empty($qtys);
153-
$invoice = $this->orderConverter->toInvoice($order);
148+
public function prepareInvoice(
149+
Order $order,
150+
array $orderItemsQtyToInvoice = []
151+
): InvoiceInterface {
154152
$totalQty = 0;
155-
$qtys = $this->prepareItemsQty($order, $qtys);
153+
$invoice = $this->orderConverter->toInvoice($order);
154+
$preparedItemsQty = $this->prepareItemsQty($order, $orderItemsQtyToInvoice);
155+
156156
foreach ($order->getAllItems() as $orderItem) {
157-
if (!$this->_canInvoiceItem($orderItem, $qtys)) {
157+
if (!$this->canInvoiceItem($orderItem, $preparedItemsQty)) {
158158
continue;
159159
}
160-
$item = $this->orderConverter->itemToInvoiceItem($orderItem);
161-
if (isset($qtys[$orderItem->getId()])) {
162-
$qty = (double) $qtys[$orderItem->getId()];
160+
161+
if (isset($preparedItemsQty[$orderItem->getId()])) {
162+
$qty = $preparedItemsQty[$orderItem->getId()];
163163
} elseif ($orderItem->isDummy()) {
164164
$qty = $orderItem->getQtyOrdered() ? $orderItem->getQtyOrdered() : 1;
165-
} elseif ($isQtysEmpty) {
165+
} elseif (empty($orderItemsQtyToInvoice)) {
166166
$qty = $orderItem->getQtyToInvoice();
167167
} else {
168168
$qty = 0;
169169
}
170+
171+
$invoiceItem = $this->orderConverter->itemToInvoiceItem($orderItem);
172+
$this->setInvoiceItemQuantity($invoiceItem, (float) $qty);
173+
$invoice->addItem($invoiceItem);
170174
$totalQty += $qty;
171-
$this->setInvoiceItemQuantity($item, $qty);
172-
$invoice->addItem($item);
173175
}
176+
174177
$invoice->setTotalQty($totalQty);
175178
$invoice->collectTotals();
176179
$order->getInvoiceCollection()->addItem($invoice);
180+
177181
return $invoice;
178182
}
179183

180184
/**
181-
* Prepare qty to invoice for parent and child products if theirs qty is not specified in initial request.
185+
* Prepare qty to invoice for parent and child products
186+
* if theirs qty is not specified in initial request.
182187
*
183188
* @param Order $order
184-
* @param array $qtys
189+
* @param array $orderItemsQtyToInvoice
185190
* @return array
186191
*/
187-
private function prepareItemsQty(Order $order, array $qtys = [])
188-
{
192+
private function prepareItemsQty(
193+
Order $order,
194+
array $orderItemsQtyToInvoice
195+
): array {
189196
foreach ($order->getAllItems() as $orderItem) {
190-
if (empty($qtys[$orderItem->getId()])) {
191-
if ($orderItem->getProductType() == Type::TYPE_BUNDLE && !$orderItem->isShipSeparately()) {
192-
$qtys[$orderItem->getId()] = $orderItem->getQtyOrdered() - $orderItem->getQtyInvoiced();
193-
} else {
194-
$parentItem = $orderItem->getParentItem();
195-
$parentItemId = $parentItem ? $parentItem->getId() : null;
196-
if ($parentItemId && isset($qtys[$parentItemId])) {
197-
$qtys[$orderItem->getId()] = $qtys[$parentItemId];
198-
}
199-
continue;
197+
if (isset($orderItemsQtyToInvoice[$orderItem->getId()])) {
198+
if ($orderItem->isDummy() && $orderItem->getHasChildren()) {
199+
$orderItemsQtyToInvoice = $this->setChildItemsQtyToInvoice($orderItem, $orderItemsQtyToInvoice);
200+
}
201+
} else {
202+
if (isset($orderItemsQtyToInvoice[$orderItem->getParentItemId()])) {
203+
$orderItemsQtyToInvoice[$orderItem->getId()] = $orderItemsQtyToInvoice[$orderItem->getParentItemId()];
200204
}
201205
}
202-
203-
$this->prepareItemQty($orderItem, $qtys);
204206
}
205207

206-
return $qtys;
208+
return $orderItemsQtyToInvoice;
207209
}
208210

209211
/**
210-
* Prepare qty_invoiced for order item
212+
* Sets qty to invoice for children order items, if not set.
211213
*
212-
* @param \Magento\Sales\Api\Data\OrderItemInterface $orderItem
213-
* @param array $qtys
214+
* @param OrderItemInterface $parentOrderItem
215+
* @param array $orderItemsQtyToInvoice
216+
* @return array
214217
*/
215-
private function prepareItemQty(\Magento\Sales\Api\Data\OrderItemInterface $orderItem, &$qtys)
216-
{
217-
$this->prepareBundleQty($orderItem, $qtys);
218+
private function setChildItemsQtyToInvoice(
219+
OrderItemInterface $parentOrderItem,
220+
array $orderItemsQtyToInvoice
221+
): array {
222+
/** @var OrderItemInterface $childOrderItem */
223+
foreach ($parentOrderItem->getChildrenItems() as $childOrderItem) {
224+
if (!isset($orderItemsQtyToInvoice[$childOrderItem->getItemId()])) {
225+
$productOptions = $childOrderItem->getProductOptions();
218226

219-
if ($orderItem->isDummy()) {
220-
if ($orderItem->getHasChildren()) {
221-
foreach ($orderItem->getChildrenItems() as $child) {
222-
if (!isset($qtys[$child->getId()])) {
223-
$qtys[$child->getId()] = $child->getQtyToInvoice();
224-
}
225-
$parentId = $orderItem->getParentItemId();
226-
if ($parentId && array_key_exists($parentId, $qtys)) {
227-
$qtys[$orderItem->getId()] = $qtys[$parentId];
228-
} else {
229-
continue;
230-
}
231-
}
232-
} elseif ($orderItem->getParentItem()) {
233-
$parent = $orderItem->getParentItem();
234-
if (!isset($qtys[$parent->getId()])) {
235-
$qtys[$parent->getId()] = $parent->getQtyToInvoice();
227+
if (isset($productOptions['bundle_selection_attributes'])) {
228+
$bundleSelectionAttributes = $this->serializer
229+
->unserialize($productOptions['bundle_selection_attributes']);
230+
$orderItemsQtyToInvoice[$childOrderItem->getItemId()] =
231+
$bundleSelectionAttributes['qty'] * $orderItemsQtyToInvoice[$parentOrderItem->getItemId()];
236232
}
237233
}
238234
}
239-
}
240-
241-
/**
242-
* Prepare qty to invoice for bundle products
243-
*
244-
* @param \Magento\Sales\Api\Data\OrderItemInterface $orderItem
245-
* @param array $qtys
246-
*/
247-
private function prepareBundleQty(\Magento\Sales\Api\Data\OrderItemInterface $orderItem, &$qtys)
248-
{
249-
if ($orderItem->getProductType() == Type::TYPE_BUNDLE && !$orderItem->isShipSeparately()) {
250-
foreach ($orderItem->getChildrenItems() as $childItem) {
251-
$bundleSelectionAttributes = $childItem->getProductOptionByCode('bundle_selection_attributes');
252-
if (is_string($bundleSelectionAttributes)) {
253-
$bundleSelectionAttributes = $this->serializer->unserialize($bundleSelectionAttributes);
254-
}
255235

256-
$qtys[$childItem->getId()] = $qtys[$orderItem->getId()] * $bundleSelectionAttributes['qty'];
257-
}
258-
}
236+
return $orderItemsQtyToInvoice;
259237
}
260238

261239
/**
262240
* Check if order item can be invoiced.
263241
*
264-
* @param \Magento\Sales\Api\Data\OrderItemInterface $item
242+
* @param OrderItemInterface $item
265243
* @param array $qtys
266244
* @return bool
267245
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
268246
*/
269-
protected function _canInvoiceItem(\Magento\Sales\Api\Data\OrderItemInterface $item, array $qtys = [])
247+
private function canInvoiceItem(OrderItemInterface $item, array $qtys): bool
270248
{
271249
if ($item->getLockedDoInvoice()) {
272250
return false;
@@ -299,14 +277,14 @@ protected function _canInvoiceItem(\Magento\Sales\Api\Data\OrderItemInterface $i
299277
}
300278

301279
/**
302-
* Set quantity to invoice item
280+
* Set quantity to invoice item.
303281
*
304-
* @param \Magento\Sales\Api\Data\InvoiceItemInterface $item
282+
* @param InvoiceItemInterface $item
305283
* @param float $qty
306-
* @return $this
307-
* @throws \Magento\Framework\Exception\LocalizedException
284+
* @return InvoiceManagementInterface
285+
* @throws LocalizedException
308286
*/
309-
protected function setInvoiceItemQuantity(\Magento\Sales\Api\Data\InvoiceItemInterface $item, $qty)
287+
private function setInvoiceItemQuantity(InvoiceItemInterface $item, float $qty): InvoiceManagementInterface
310288
{
311289
$qty = ($item->getOrderItem()->getIsQtyDecimal()) ? (double) $qty : (int) $qty;
312290
$qty = $qty > 0 ? $qty : 0;
@@ -317,7 +295,7 @@ protected function setInvoiceItemQuantity(\Magento\Sales\Api\Data\InvoiceItemInt
317295
$qtyToInvoice = sprintf("%F", $item->getOrderItem()->getQtyToInvoice());
318296
$qty = sprintf("%F", $qty);
319297
if ($qty > $qtyToInvoice && !$item->getOrderItem()->isDummy()) {
320-
throw new \Magento\Framework\Exception\LocalizedException(
298+
throw new LocalizedException(
321299
__('We found an invalid quantity to invoice item "%1".', $item->getName())
322300
);
323301
}

0 commit comments

Comments
 (0)