Skip to content

Commit ea79f7d

Browse files
authored
Merge pull request #8770 from adobe-commerce-tier-4/T4-02-16-2024
[Support T4] 02-16-2024 Regular delivery of bugfixes and improvements
2 parents f89a447 + 4a27ba0 commit ea79f7d

File tree

40 files changed

+1230
-233
lines changed

40 files changed

+1230
-233
lines changed

app/code/Magento/CatalogImportExport/Model/Export/Product.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,11 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity
368368
*/
369369
private $stockConfiguration;
370370

371+
/**
372+
* @var array
373+
*/
374+
private array $attributeFrontendTypes = [];
375+
371376
/**
372377
* Product constructor.
373378
*
@@ -1062,7 +1067,7 @@ protected function collectRawData()
10621067

10631068
if ($this->_attributeTypes[$code] == 'datetime') {
10641069
if (in_array($code, $this->dateAttrCodes)
1065-
|| in_array($code, $this->userDefinedAttributes)
1070+
|| $this->attributeFrontendTypes[$code] === 'date'
10661071
) {
10671072
$attrValue = $this->_localeDate->formatDateTime(
10681073
new \DateTime($attrValue),
@@ -1657,6 +1662,7 @@ protected function initAttributes()
16571662
$this->_attributeValues[$attribute->getAttributeCode()] = $this->getAttributeOptions($attribute);
16581663
$this->_attributeTypes[$attribute->getAttributeCode()] =
16591664
\Magento\ImportExport\Model\Import::getAttributeType($attribute);
1665+
$this->attributeFrontendTypes[$attribute->getAttributeCode()] = $attribute->getFrontendInput();
16601666
if ($attribute->getIsUserDefined()) {
16611667
$this->userDefinedAttributes[] = $attribute->getAttributeCode();
16621668
}

app/code/Magento/CatalogImportExport/Model/Import/Product.php

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2055,18 +2055,26 @@ private function saveProductAttributesPhase(
20552055
$backModel = $attribute->getBackendModel();
20562056
$attrTable = $attribute->getBackend()->getTable();
20572057
$storeIds = [0];
2058-
if ('datetime' == $attribute->getBackendType()
2059-
&& (
2060-
in_array($attribute->getAttributeCode(), $this->dateAttrCodes)
2061-
|| $attribute->getIsUserDefined()
2062-
)
2063-
) {
2064-
$attrValue = $this->dateTime->formatDate($attrValue, false);
2065-
} elseif ('datetime' == $attribute->getBackendType() && strtotime($attrValue)) {
2066-
$attrValue = gmdate(
2067-
'Y-m-d H:i:s',
2068-
$this->_localeDate->date($attrValue)->getTimestamp()
2069-
);
2058+
if ('datetime' == $attribute->getBackendType()) {
2059+
$attrValue = trim((string) $attrValue);
2060+
if (!empty($attrValue)) {
2061+
$timezone = new \DateTimeZone($this->_localeDate->getConfigTimezone());
2062+
// Parse date from format Y-m-d[ H:i:s]
2063+
$date = date_create_from_format(DateTime::DATETIME_PHP_FORMAT, $attrValue, $timezone)
2064+
?: date_create_from_format(DateTime::DATE_PHP_FORMAT, $attrValue, $timezone);
2065+
// Perhaps, date is formatted according to user locale. For example, dates in exported csv file
2066+
$date = $date ?: $this->_localeDate->date($attrValue);
2067+
if ($attribute->getFrontendInput() === 'date'
2068+
|| in_array($attribute->getAttributeCode(), $this->dateAttrCodes)
2069+
) {
2070+
$date->setTime(0, 0);
2071+
} else {
2072+
$date->setTimezone(new \DateTimeZone($this->_localeDate->getDefaultTimezone()));
2073+
}
2074+
$attrValue = $date->format(DateTime::DATETIME_PHP_FORMAT);
2075+
} else {
2076+
$attrValue = null;
2077+
}
20702078
} elseif ($backModel) {
20712079
$attribute->getBackend()->beforeSave($product);
20722080
$attrValue = $product->getData($attribute->getAttributeCode());

app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
namespace Magento\CatalogImportExport\Model\Import\Product;
77

88
use Magento\CatalogImportExport\Model\Import\Product;
9+
use Magento\Framework\App\ObjectManager;
10+
use Magento\Framework\Stdlib\DateTime;
11+
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
912
use Magento\Framework\Validator\AbstractValidator;
1013
use Magento\Catalog\Model\Product\Attribute\Backend\Sku;
1114

@@ -49,15 +52,24 @@ class Validator extends AbstractValidator implements RowValidatorInterface
4952
protected $invalidAttribute;
5053

5154
/**
52-
* @param \Magento\Framework\Stdlib\StringUtils $string
55+
* @var TimezoneInterface
56+
*/
57+
private $localeDate;
58+
59+
/**
60+
* @param StringUtils $string
5361
* @param RowValidatorInterface[] $validators
62+
* @param TimezoneInterface|null $localeDate
5463
*/
5564
public function __construct(
5665
\Magento\Framework\Stdlib\StringUtils $string,
57-
$validators = []
66+
$validators = [],
67+
?TimezoneInterface $localeDate = null
5868
) {
5969
$this->string = $string;
6070
$this->validators = $validators;
71+
$this->localeDate = $localeDate ?: ObjectManager::getInstance()
72+
->get(TimezoneInterface::class);
6173
}
6274

6375
/**
@@ -302,7 +314,16 @@ private function validateMultiselect(string $attrCode, array $options, array|str
302314
private function validateDateTime(string $rowData): bool
303315
{
304316
$val = trim($rowData);
305-
$valid = strtotime($val) !== false;
317+
try {
318+
if (!date_create_from_format(DateTime::DATETIME_PHP_FORMAT, $val)
319+
&& !date_create_from_format(DateTime::DATE_PHP_FORMAT, $val)
320+
) {
321+
$this->localeDate->date($val);
322+
}
323+
$valid = true;
324+
} catch (\Exception $e) {
325+
$valid = false;
326+
}
306327
if (!$valid) {
307328
$this->_addMessages([RowValidatorInterface::ERROR_INVALID_ATTRIBUTE_TYPE]);
308329
}

app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/ValidatorTest.php

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
use PHPUnit\Framework\MockObject\MockObject;
1919
use PHPUnit\Framework\TestCase;
2020

21+
/**
22+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
23+
*/
2124
class ValidatorTest extends TestCase
2225
{
2326
/** @var Validator */
@@ -62,13 +65,19 @@ protected function setUp(): void
6265
);
6366

6467
$this->validators = [$this->validatorOne, $this->validatorTwo];
65-
$this->objectManagerHelper = new ObjectManagerHelper($this);
66-
$this->validator = $this->objectManagerHelper->getObject(
67-
Validator::class,
68-
[
69-
'string' => new StringUtils(),
70-
'validators' => $this->validators
71-
]
68+
$timezone = $this->createMock(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class);
69+
$timezone->expects($this->any())
70+
->method('date')
71+
->willReturnCallback(
72+
function ($date = null) {
73+
return new \DateTime($date);
74+
}
75+
);
76+
77+
$this->validator = new Validator(
78+
new StringUtils(),
79+
$this->validators,
80+
$timezone
7281
);
7382
$this->validator->init($this->context);
7483
}

app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
namespace Magento\CatalogUrlRewrite\Model;
77

88
use Magento\Catalog\Api\CategoryRepositoryInterface;
9+
use Magento\Catalog\Api\Data\CategoryInterface;
910
use Magento\Catalog\Model\Category;
1011

1112
/**
@@ -16,12 +17,12 @@ class CategoryUrlPathGenerator
1617
/**
1718
* Minimal category level that can be considered for generate path
1819
*/
19-
const MINIMAL_CATEGORY_LEVEL_FOR_PROCESSING = 3;
20+
public const MINIMAL_CATEGORY_LEVEL_FOR_PROCESSING = 3;
2021

2122
/**
2223
* XML path for category url suffix
2324
*/
24-
const XML_PATH_CATEGORY_URL_SUFFIX = 'catalog/seo/category_url_suffix';
25+
public const XML_PATH_CATEGORY_URL_SUFFIX = 'catalog/seo/category_url_suffix';
2526

2627
/**
2728
* Cache for category rewrite suffix
@@ -73,14 +74,12 @@ public function getUrlPath($category, $parentCategory = null)
7374
if (in_array($category->getParentId(), [Category::ROOT_CATEGORY_ID, Category::TREE_ROOT_ID])) {
7475
return '';
7576
}
76-
$path = $category->getUrlPath();
77-
if ($path !== null && !$category->dataHasChangedFor('url_key') && !$category->dataHasChangedFor('parent_id')) {
78-
return $path;
79-
}
80-
$path = $category->getUrlKey();
81-
if ($path === false) {
77+
78+
if ($this->shouldReturnCurrentUrlPath($category)) {
8279
return $category->getUrlPath();
8380
}
81+
82+
$path = $category->getUrlKey();
8483
if ($this->isNeedToGenerateUrlPathForParent($category)) {
8584
$parentCategory = $parentCategory === null ?
8685
$this->categoryRepository->get($category->getParentId(), $category->getStoreId()) : $parentCategory;
@@ -90,6 +89,27 @@ public function getUrlPath($category, $parentCategory = null)
9089
return $path;
9190
}
9291

92+
/**
93+
* Check if current category url path is valid to be returned
94+
*
95+
* @param CategoryInterface $category
96+
* @return bool
97+
*/
98+
private function shouldReturnCurrentUrlPath(CategoryInterface $category): bool
99+
{
100+
$path = $category->getUrlPath();
101+
if ($path !== null && !$category->dataHasChangedFor('url_key') && !$category->dataHasChangedFor('parent_id')) {
102+
$parentPath = $this->generateParentUrlPathFromUrlKeys($category);
103+
if (strlen($parentPath) && str_contains($path, $parentPath) !== false) {
104+
return true;
105+
}
106+
}
107+
if (empty($category->getUrlKey())) {
108+
return true;
109+
}
110+
return false;
111+
}
112+
93113
/**
94114
* Define whether we should generate URL path for parent
95115
*
@@ -159,4 +179,27 @@ public function getUrlKey($category)
159179
$urlKey = $category->getUrlKey();
160180
return $category->formatUrlKey($urlKey === '' || $urlKey === null ? $category->getName() : $urlKey);
161181
}
182+
183+
/**
184+
* Generate a parent url path based on custom scoped url keys
185+
*
186+
* @param CategoryInterface $category
187+
* @return string
188+
*/
189+
private function generateParentUrlPathFromUrlKeys(CategoryInterface $category): string
190+
{
191+
$storeId = $category->getStoreId();
192+
$currentStore = $this->storeManager->getStore();
193+
$this->storeManager->setCurrentStore($storeId);
194+
195+
$parentPath = [];
196+
foreach ($category->getParentCategories() as $parentCategory) {
197+
if ($parentCategory->getUrlKey() && (int)$category->getId() !== (int)$parentCategory->getId()) {
198+
$parentPath[] = $parentCategory->getUrlKey();
199+
}
200+
}
201+
202+
$this->storeManager->setCurrentStore($currentStore);
203+
return implode('/', $parentPath);
204+
}
162205
}

app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ public function execute(Observer $observer)
125125
}
126126
}
127127
}
128-
$category->setUrlKey(null)->setUrlPath(null);
129128
}
130129
}
131130

@@ -210,7 +209,7 @@ protected function updateUrlPathForChildren(Category $category)
210209
Category::ENTITY
211210
)) {
212211
$child = $this->categoryRepository->get($childId, $storeId);
213-
$this->updateUrlPathForCategory($child);
212+
$this->updateUrlPathForCategory($child, $category);
214213
}
215214
}
216215
}

app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ protected function setUp(): void
5151
'getId',
5252
'formatUrlKey',
5353
'getName',
54-
'isObjectNew'
54+
'isObjectNew',
55+
'getParentCategories'
5556
]
5657
)
5758
->disableOriginalConstructor()
@@ -60,6 +61,10 @@ protected function setUp(): void
6061
$this->scopeConfig = $this->getMockForAbstractClass(ScopeConfigInterface::class);
6162
$this->categoryRepository = $this->getMockForAbstractClass(CategoryRepositoryInterface::class);
6263

64+
$this->category->expects($this->any())
65+
->method('getParentCategories')
66+
->willReturn([]);
67+
6368
$this->categoryUrlPathGenerator = (new ObjectManager($this))->getObject(
6469
CategoryUrlPathGenerator::class,
6570
[
@@ -154,10 +159,21 @@ public function testGetUrlPathWithParent(
154159
$this->category->expects($this->any())->method('getUrlPath')->willReturn($urlPath);
155160
$this->category->expects($this->any())->method('getUrlKey')->willReturn($urlKey);
156161
$this->category->expects($this->any())->method('isObjectNew')->willReturn($isCategoryNew);
162+
$this->category->expects($this->any())->method('getStoreId')->willReturn(Store::DEFAULT_STORE_ID);
157163

158164
$parentCategory = $this->getMockBuilder(Category::class)
159165
->addMethods(['getUrlPath'])
160-
->onlyMethods(['__wakeup', 'getParentId', 'getLevel', 'dataHasChangedFor', 'load'])
166+
->onlyMethods(
167+
[
168+
'__wakeup',
169+
'getParentId',
170+
'getLevel',
171+
'dataHasChangedFor',
172+
'load',
173+
'getStoreId',
174+
'getParentCategories'
175+
]
176+
)
161177
->disableOriginalConstructor()
162178
->getMock();
163179
$parentCategory->expects($this->any())->method('getParentId')
@@ -166,10 +182,16 @@ public function testGetUrlPathWithParent(
166182
$parentCategory->expects($this->any())->method('getUrlPath')->willReturn($parentUrlPath);
167183
$parentCategory->expects($this->any())->method('dataHasChangedFor')
168184
->willReturnMap([['url_key', false], ['path_ids', false]]);
185+
$parentCategory->expects($this->any())->method('getStoreId')->willReturn(Store::DEFAULT_STORE_ID);
186+
$parentCategory->expects($this->any())->method('getParentCategories')->willReturn([]);
169187

170188
$this->categoryRepository->expects($this->any())->method('get')->with(13)
171189
->willReturn($parentCategory);
172190

191+
$store = $this->createMock(Store::class);
192+
$store->expects($this->any())->method('getId')->willReturn(0);
193+
$this->storeManager->expects($this->any())->method('getStore')->willReturn($store);
194+
173195
$this->assertEquals($result, $this->categoryUrlPathGenerator->getUrlPath($this->category));
174196
}
175197

@@ -196,7 +218,7 @@ public function testGetUrlPathWithSuffixAndStore($urlPath, $storeId, $categorySt
196218
{
197219
$this->category->expects($this->any())->method('getStoreId')->willReturn($categoryStoreId);
198220
$this->category->expects($this->once())->method('getParentId')->willReturn(123);
199-
$this->category->expects($this->once())->method('getUrlPath')->willReturn($urlPath);
221+
$this->category->expects($this->exactly(2))->method('getUrlPath')->willReturn($urlPath);
200222
$this->category->expects($this->exactly(2))->method('dataHasChangedFor')
201223
->willReturnMap([['url_key', false], ['path_ids', false]]);
202224

@@ -221,13 +243,13 @@ public function testGetUrlPathWithSuffixWithoutStore()
221243

222244
$this->category->expects($this->any())->method('getStoreId')->willReturn($storeId);
223245
$this->category->expects($this->once())->method('getParentId')->willReturn(2);
224-
$this->category->expects($this->once())->method('getUrlPath')->willReturn($urlPath);
246+
$this->category->expects($this->exactly(2))->method('getUrlPath')->willReturn($urlPath);
225247
$this->category->expects($this->exactly(2))->method('dataHasChangedFor')
226248
->willReturnMap([['url_key', false], ['path_ids', false]]);
227249

228250
$store = $this->createMock(Store::class);
229251
$store->expects($this->once())->method('getId')->willReturn($currentStoreId);
230-
$this->storeManager->expects($this->once())->method('getStore')->willReturn($store);
252+
$this->storeManager->expects($this->exactly(2))->method('getStore')->willReturn($store);
231253
$this->scopeConfig->expects($this->once())->method('getValue')
232254
->with(CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, ScopeInterface::SCOPE_STORE, $currentStoreId)
233255
->willReturn($suffix);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/************************************************************************
4+
* Copyright 2023 Adobe
5+
* All Rights Reserved.
6+
*
7+
* NOTICE: All information contained herein is, and remains
8+
* the property of Adobe and its suppliers, if any. The intellectual
9+
* and technical concepts contained herein are proprietary to Adobe
10+
* and its suppliers and are protected by all applicable intellectual
11+
* property laws, including trade secret and copyright laws.
12+
* Dissemination of this information or reproduction of this material
13+
* is strictly forbidden unless prior written permission is obtained
14+
* from Adobe.
15+
* ************************************************************************
16+
*/
17+
-->
18+
19+
<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd">
21+
<actionGroup name="AdminOrderStatusToStateAssignActionGroup" extends="AdminGoToOrderStatusPageActionGroup">
22+
<annotations>
23+
<description>Order Default Status assignation to Order State.</description>
24+
</annotations>
25+
<arguments>
26+
<argument name="orderStatus" type="string"/>
27+
<argument name="orderState" type="string" />
28+
</arguments>
29+
<click selector="{{AdminOrderStatusGridSection.assignStatusToStateBtn}}" stepKey="clickAssignStatusBtn" after="waitForPageLoaded"/>
30+
<selectOption selector="{{AdminAssignOrderStatusToStateSection.orderStatus}}" userInput="{{orderStatus}}" stepKey="selectOrderStatus"/>
31+
<selectOption selector="{{AdminAssignOrderStatusToStateSection.orderState}}" userInput="{{orderState}}" stepKey="selectOrderState"/>
32+
<checkOption selector="{{AdminAssignOrderStatusToStateSection.orderStatusAsDefault}}" stepKey="orderStatusAsDefault"/>
33+
<checkOption selector="{{AdminAssignOrderStatusToStateSection.visibleOnStorefront}}" stepKey="visibleOnStorefront"/>
34+
<click selector="{{AdminAssignOrderStatusToStateSection.saveStatusAssignment}}" stepKey="clickSaveStatus"/>
35+
<waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageVisible"/>
36+
<see userInput="You assigned the order status." selector="{{AdminMessagesSection.success}}" stepKey="assertSaveSuccessMessage"/>
37+
</actionGroup>
38+
</actionGroups>

0 commit comments

Comments
 (0)