Skip to content

Commit d96bb9d

Browse files
committed
Merge remote-tracking branch 'origin/AC-15286' into spartans_pr_02122025
2 parents 91d0a2d + 2d5e04a commit d96bb9d

File tree

2 files changed

+161
-4
lines changed
  • app/code/Magento/Sales/Controller/Adminhtml/Order
  • dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create

2 files changed

+161
-4
lines changed

app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ protected function _processData()
168168
* @SuppressWarnings(PHPMD.NPathComplexity)
169169
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
170170
*/
171+
// phpcs:disable Generic.Metrics.NestingLevel
171172
protected function _processActionData($action = null)
172173
{
173174
$eventData = [
@@ -253,7 +254,62 @@ protected function _processActionData($action = null)
253254
) {
254255
$items = $this->getRequest()->getPost('item');
255256
$items = $this->_processFiles($items);
256-
$this->_getOrderCreateModel()->addProducts($items);
257+
/**
258+
* Filter out products that are already in the quote with required options
259+
* when the current bulk payload does not contain any options for them.
260+
* This avoids accidental re-adding of previously configured products with qty=1.
261+
*/
262+
if (is_array($items)) {
263+
$filtered = [];
264+
foreach ($items as $productId => $config) {
265+
$config = is_array($config) ? $config : [];
266+
$hasOptionsInConfig = false;
267+
if (isset($config['options']) && is_array($config['options'])) {
268+
$opts = $config['options'];
269+
unset($opts['files_prefix']);
270+
$opts = array_filter(
271+
$opts,
272+
function ($v) {
273+
if (is_array($v)) {
274+
return !empty($v);
275+
}
276+
277+
return $v !== '' && $v !== null;
278+
}
279+
);
280+
$hasOptionsInConfig = !empty($opts);
281+
}
282+
if ($this->_getQuote()->hasProductId((int)$productId) && !$hasOptionsInConfig) {
283+
try {
284+
/** @var \Magento\Catalog\Model\Product $product */
285+
$product = $this
286+
->_objectManager
287+
->create(\Magento\Catalog\Model\Product::class)
288+
->load($productId);
289+
if ($product->getId() && $product->getHasOptions()) {
290+
$hasRequired = false;
291+
foreach ($product->getOptions() as $option) {
292+
if ($option->getIsRequire()) {
293+
$hasRequired = true;
294+
break;
295+
}
296+
}
297+
if ($hasRequired) {
298+
continue;
299+
}
300+
}
301+
//phpcs:ignore Magento2.CodeAnalysis.EmptyBlock
302+
} catch (\Throwable $e) {
303+
// Intentionally swallow any exception during pre-check to allow normal add flow.
304+
}
305+
}
306+
$filtered[$productId] = $config;
307+
}
308+
$items = $filtered;
309+
}
310+
if (!empty($items)) {
311+
$this->_getOrderCreateModel()->addProducts($items);
312+
}
257313
}
258314

259315
/**

dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlockTest.php

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,16 +221,16 @@ public function testJsonResponseWithJsVarnameUsesCompressionToPreventSessionBloa
221221
$backendSession->hasUpdateResult(),
222222
'Session should contain compressed updateResult'
223223
);
224-
224+
225225
$sessionData = $backendSession->getUpdateResult();
226226
$this->assertIsArray($sessionData, 'Session data should be array for compressed format');
227227
$this->assertTrue($sessionData['compressed'], 'Data should be marked as compressed');
228228
$this->assertArrayHasKey('data', $sessionData, 'Compressed data should exist');
229-
229+
230230
// Verify compression actually reduces size
231231
$decompressed = gzdecode($sessionData['data']);
232232
$this->assertNotEmpty($decompressed, 'Decompressed data should not be empty');
233-
233+
234234
$originalSize = strlen($decompressed);
235235
$compressedSize = strlen($sessionData['data']);
236236
$this->assertLessThan(
@@ -346,6 +346,107 @@ public function testAddProductToOrderFromWishList(): void
346346
$this->assertCount(1, $quoteItems);
347347
}
348348

349+
/**
350+
* Verify that when another product is added from the grid, a previously configured product
351+
* (with required options) is NOT re-added due to presence of options[files_prefix] only.
352+
*
353+
* Uses new DataFixture-based products to avoid URL rewrite collisions.
354+
*/
355+
public function testGridAddSkipsConfiguredProductWithoutOptions(): void
356+
{
357+
/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
358+
$productRepository = $this->_objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
359+
// Create two fresh products (unique skus) using Product fixture
360+
/** @var \Magento\Catalog\Test\Fixture\Product $productFixture */
361+
$productFixture = $this->_objectManager->get(\Magento\Catalog\Test\Fixture\Product::class);
362+
$productFixture->apply(['sku' => 'custom_options_p1', 'price' => 10]);
363+
$productFixture->apply(['sku' => 'plain_p1', 'price' => 10]);
364+
$customProduct = $productRepository->get('custom_options_p1');
365+
$simpleProduct = $productRepository->get('plain_p1');
366+
367+
// Programmatically add one required text custom option to the custom product
368+
/** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */
369+
$customOptionFactory = $this->_objectManager
370+
->get(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class);
371+
$requiredTextOption = $customOptionFactory->create(
372+
[
373+
'data' => [
374+
'title' => 'Req Text',
375+
'type' => 'field',
376+
'is_require' => 1,
377+
'sort_order' => 0,
378+
'price' => 1,
379+
'price_type' => 'fixed',
380+
'sku' => 'opt_text_1',
381+
'max_characters' => 100,
382+
],
383+
]
384+
);
385+
$requiredTextOption->setProductSku($customProduct->getSku());
386+
$customProduct->setCanSaveCustomOptions(true)->setHasOptions(true);
387+
$customProduct->setOptions([$requiredTextOption]);
388+
$productRepository->save($customProduct);
389+
390+
// Add the product with required custom options to the admin create quote with qty 10
391+
$options = [];
392+
foreach ($customProduct->getOptions() as $option) {
393+
$value = null;
394+
if ($option->getValues()) {
395+
$values = $option->getValues();
396+
$first = reset($values);
397+
$value = $first ? (int)$first->getOptionTypeId() : null;
398+
}
399+
if ($value === null) {
400+
$value = 'test';
401+
}
402+
$options[(int)$option->getId()] = $value;
403+
}
404+
/** @var \Magento\Sales\Model\AdminOrder\Create $orderCreate */
405+
$orderCreate = $this->_objectManager->get(\Magento\Sales\Model\AdminOrder\Create::class);
406+
$orderCreate->addProduct(
407+
(int)$customProduct->getId(),
408+
['qty' => 10, 'options' => $options]
409+
);
410+
411+
// Emulate grid submit where the first (already configured) product comes with only files_prefix.
412+
// Avoid rendering the sidebar (to prevent wishlist creation) and do not set a customer in session.
413+
$params = $this->hydrateParams([
414+
'block' => 'items,shipping_method,billing_method,totals,giftmessage'
415+
]);
416+
$post = $this->hydratePost([
417+
'customer_id' => 0,
418+
'item' => [
419+
(int)$customProduct->getId() => [
420+
'options' => ['files_prefix' => 'item_' . (int)$customProduct->getId() . '_'],
421+
],
422+
(int)$simpleProduct->getId() => ['qty' => 1],
423+
],
424+
]);
425+
$this->dispatchWitParams($params, $post);
426+
427+
// Assert custom product wasn't re-added and quantities are as expected
428+
$quote = $this->session->getQuote();
429+
$items = $quote->getItemsCollection(false)->getItems();
430+
$customCount = 0;
431+
$customQty = 0.0;
432+
$simpleCount = 0;
433+
$simpleQty = 0.0;
434+
foreach ($items as $item) {
435+
if ((int)$item->getProductId() === (int)$customProduct->getId()) {
436+
$customCount++;
437+
$customQty += (float)$item->getQty();
438+
}
439+
if ((int)$item->getProductId() === (int)$simpleProduct->getId()) {
440+
$simpleCount++;
441+
$simpleQty += (float)$item->getQty();
442+
}
443+
}
444+
$this->assertEquals(1, $customCount);
445+
$this->assertEquals(10.0, $customQty);
446+
$this->assertEquals(1, $simpleCount);
447+
$this->assertEquals(1.0, $simpleQty);
448+
}
449+
349450
/**
350451
* Check that customer notification is NOT disabled after comment is updated.
351452
*

0 commit comments

Comments
 (0)