├── .github ├── CODE_OF_CONDUCT.md └── FUNDING.yml ├── Controller └── Adminhtml │ ├── Export │ ├── Category.php │ ├── CategoryDownload.php │ └── CategoryPost.php │ └── Import │ ├── Category.php │ └── CategoryPost.php ├── LICENSE ├── Model ├── Config │ ├── ExcludedFields.php │ └── Source │ │ └── Category │ │ ├── Attributes.php │ │ └── VisibleAttributes.php ├── Csv │ └── Options.php ├── DataProvider.php ├── Export │ ├── Categories.php │ └── ToCsv.php ├── Import │ ├── Categories.php │ └── FromCsv.php ├── Session │ └── DownloadContext.php └── Utils.php ├── README.md ├── Setup └── Patch │ └── Data │ ├── AddCategoryCodeAttributeV1.php │ └── PopulateCategoryCodeV1.php ├── Ui └── Component │ └── Form │ └── Button │ └── Download.php ├── composer.json ├── etc ├── acl.xml ├── adminhtml │ ├── menu.xml │ ├── routes.xml │ └── system.xml ├── config.xml └── module.xml ├── registration.php └── view └── adminhtml ├── layout ├── adminhtml_export_category.xml └── adminhtml_import_category.xml └── ui_component ├── category_form.xml ├── export_category_form.xml └── import_category_form.xml /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opengento@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | custom: ['https://www.helloasso.com/associations/opengento/formulaires/1'] 13 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Export/Category.php: -------------------------------------------------------------------------------- 1 | resultFactory->create(ResultFactory::TYPE_PAGE); 23 | $page->getConfig()->getTitle()->set(new Phrase('Export Categories')); 24 | 25 | return $page; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Export/CategoryDownload.php: -------------------------------------------------------------------------------- 1 | downloadContext->getFile(); 35 | if ($file) { 36 | $this->downloadContext->clearFile(); 37 | try { 38 | return $this->fileFactory->create( 39 | basename($file), 40 | ['type' => 'filename', 'value' => $file], 41 | DirectoryList::VAR_IMPORT_EXPORT 42 | ); 43 | } catch (Exception $e) { 44 | $this->messageManager->addExceptionMessage($e, new Phrase('Something went wrong while downloading the file.')); 45 | } 46 | } 47 | 48 | return $this->resultRedirectFactory->create()->setPath('*/*/category'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Export/CategoryPost.php: -------------------------------------------------------------------------------- 1 | downloadContext->setFile( 38 | $this->toCsv->execute( 39 | $this->resolveStoreIds(), 40 | $this->resolveAttributes(), 41 | Options::createFromRequest($this->getRequest()) 42 | ) 43 | ); 44 | 45 | $this->messageManager->addSuccessMessage(new Phrase('The export file is now ready for download.')); 46 | } catch (LocalizedException $e) { 47 | $this->messageManager->addErrorMessage($e, $e->getMessage()); 48 | } catch (Exception $e) { 49 | $this->messageManager->addExceptionMessage($e, new Phrase('Something went wrong while exporting the data.')); 50 | } 51 | 52 | return $this->resultRedirectFactory->create()->setPath('*/*/category'); 53 | } 54 | 55 | private function resolveStoreIds(): array 56 | { 57 | return (array)$this->getRequest()->getParam('store_ids', [Store::DEFAULT_STORE_ID]); 58 | } 59 | 60 | /** 61 | * @throws InputException 62 | */ 63 | private function resolveAttributes(): array 64 | { 65 | return (array)$this->getRequest()->getParam('attributes') ?: throw InputException::requiredField('attributes'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Import/Category.php: -------------------------------------------------------------------------------- 1 | resultFactory->create(ResultFactory::TYPE_PAGE); 23 | $page->getConfig()->getTitle()->set(new Phrase('Import Categories')); 24 | 25 | return $page; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Import/CategoryPost.php: -------------------------------------------------------------------------------- 1 | fromCsv->execute( 39 | $this->uploadFile('import-' . time() . '-categories.csv'), 40 | Options::createFromRequest($this->getRequest()) 41 | ); 42 | $this->messageManager->addSuccessMessage(new Phrase('Import successful!')); 43 | } catch (LocalizedException $e) { 44 | $this->messageManager->addErrorMessage($e->getMessage()); 45 | } catch (Exception $e) { 46 | $this->messageManager->addExceptionMessage($e, new Phrase('Something went wrong while uploading the file.')); 47 | } 48 | 49 | return $this->resultRedirectFactory->create()->setPath('*/*/category'); 50 | } 51 | 52 | /** 53 | * @throws Exception 54 | */ 55 | private function uploadFile(string $fileName): string 56 | { 57 | $directoryRead = $this->filesystem->getDirectoryRead(DirectoryList::VAR_IMPORT_EXPORT); 58 | $uploader = $this->uploaderFactory->create(['fileId' => 'file']); 59 | $uploader->setAllowCreateFolders(true); 60 | $uploader->setAllowRenameFiles(true); 61 | $uploader->setFilesDispersion(false); 62 | $uploader->setFilenamesCaseSensitivity(true); 63 | $uploader->setAllowedExtensions(['csv']); 64 | $uploader->setAllowRenameFiles(true); 65 | $result = $uploader->save($directoryRead->getAbsolutePath('import'), $fileName); 66 | if ($result === false) { 67 | throw new LocalizedException(new Phrase('The uploaded file could not be saved.')); 68 | } 69 | 70 | return $directoryRead->getAbsolutePath($result['path'] . '/' . $result['file']); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) OpenGento 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Model/Config/ExcludedFields.php: -------------------------------------------------------------------------------- 1 | excludedAttributes ??= array_unique(array_merge( 35 | ['category_code'], 36 | $this->resolveInvisibleAttributes(), 37 | explode(',', $this->scopeConfig->getValue(self::CONFIG_PATH_EXCLUDE_FIELDS) ?? ''), 38 | explode(',', $this->scopeConfig->getValue(self::CONFIG_PATH_EXCLUDE_ATTRIBUTES) ?? '') 39 | )); 40 | } 41 | 42 | private function resolveInvisibleAttributes(): array 43 | { 44 | $collection = $this->collectionFactory->create(); 45 | $collection->setEntityTypeFilter($this->config->getEntityType(Category::ENTITY)); 46 | $collection->addFieldToSelect('attribute_code'); 47 | $collection->joinLeft( 48 | ['cea' => 'catalog_eav_attribute'], 49 | 'main_table.attribute_id = cea.attribute_id', 50 | [''] 51 | ); 52 | $collection->addFieldToFilter('cea.is_visible', 0); 53 | 54 | return $collection->getColumnValues('attribute_code'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Model/Config/Source/Category/Attributes.php: -------------------------------------------------------------------------------- 1 | options ??= $this->resolveAttributes(); 37 | } 38 | 39 | /** 40 | * @throws LocalizedException 41 | */ 42 | private function resolveAttributes(): array 43 | { 44 | $options = []; 45 | /** @var Attribute $attribute */ 46 | foreach ($this->createAttributeCollection()->getItems() as $attribute) { 47 | $options[] = [ 48 | 'label' => sprintf('%s (%s)', $attribute->getDefaultFrontendLabel(), $attribute->getAttributeCode()), 49 | 'value' => $attribute->getAttributeCode() 50 | ]; 51 | } 52 | 53 | return $options; 54 | } 55 | 56 | /** 57 | * @throws LocalizedException 58 | */ 59 | private function createAttributeCollection(): Collection 60 | { 61 | $collection = $this->collectionFactory->create(); 62 | $collection->setEntityTypeFilter($this->config->getEntityType(Category::ENTITY)); 63 | $collection->addFieldToSelect(['attribute_code', 'frontend_label']); 64 | $collection->addFieldToFilter('attribute_code', ['nin' => $this->excludedFields->get()]); 65 | $collection->setOrder('frontend_label', 'ASC'); 66 | $collection->setOrder('attribute_code', 'ASC'); 67 | 68 | return $collection; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Model/Config/Source/Category/VisibleAttributes.php: -------------------------------------------------------------------------------- 1 | options ??= $this->resolveAttributes(); 35 | } 36 | 37 | /** 38 | * @throws LocalizedException 39 | */ 40 | private function resolveAttributes(): array 41 | { 42 | $options = []; 43 | /** @var Attribute $attribute */ 44 | foreach ($this->createVisibleAttributeCollection()->getItems() as $attribute) { 45 | $options[] = [ 46 | 'label' => sprintf('%s (%s)', $attribute->getDefaultFrontendLabel(), $attribute->getAttributeCode()), 47 | 'value' => $attribute->getAttributeCode() 48 | ]; 49 | } 50 | 51 | return $options; 52 | } 53 | 54 | /** 55 | * @throws LocalizedException 56 | */ 57 | private function createVisibleAttributeCollection(): Collection 58 | { 59 | $collection = $this->collectionFactory->create(); 60 | $collection->setEntityTypeFilter($this->config->getEntityType(Category::ENTITY)); 61 | $collection->addFieldToSelect(['attribute_code', 'frontend_label']); 62 | $collection->joinLeft( 63 | ['cea' => 'catalog_eav_attribute'], 64 | 'main_table.attribute_id = cea.attribute_id', 65 | [''] 66 | ); 67 | $collection->addFieldToFilter('cea.is_visible', 1); 68 | $collection->addFieldToFilter('attribute_code', 'category_code'); 69 | $collection->setOrder('frontend_label', 'ASC'); 70 | $collection->setOrder('attribute_code', 'ASC'); 71 | 72 | return $collection; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Model/Csv/Options.php: -------------------------------------------------------------------------------- 1 | delimiter) !== 1) { 26 | throw new InputException( 27 | new Phrase( 28 | 'Invalid value of "%1" provided for the %2 field. Expected a single valid character.', 29 | ['delimiter', $this->delimiter] 30 | ) 31 | ); 32 | } 33 | if (strlen($this->enclosure) !== 1) { 34 | throw new InputException( 35 | new Phrase( 36 | 'Invalid value of "%1" provided for the %2 field. Expected a single valid character.', 37 | ['enclosure', $this->enclosure] 38 | ) 39 | ); 40 | } 41 | } 42 | 43 | /** 44 | * @throws InputException 45 | */ 46 | public static function createFromRequest(RequestInterface $request): Options 47 | { 48 | return new Options((string)$request->getParam('delimiter'), (string)$request->getParam('enclosure')); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Model/DataProvider.php: -------------------------------------------------------------------------------- 1 | collectionFactory->create(); 37 | $collection->setStoreId($storeId); 38 | $collection->setProductStoreId($storeId); 39 | $collection->setLoadProductCount(false); 40 | $collection->addAttributeToSelect($attributes); 41 | $collection->addAttributeToFilter('parent_id', ['neq' => 0]); 42 | 43 | $export = []; 44 | /** @var Category $category */ 45 | foreach ($collection->getItems() as $category) { 46 | $row = $this->utils->sanitizeData($category->toArray($attributes)); 47 | $row['store'] = $this->storeManager->getStore($storeId)->getCode(); 48 | $row['parent_code'] = $category->getParentCategory()->getData('category_code'); 49 | $export[] = $row; 50 | } 51 | 52 | return $export; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Model/Export/ToCsv.php: -------------------------------------------------------------------------------- 1 | filesystem->getDirectoryWrite(DirectoryList::VAR_IMPORT_EXPORT); 41 | $directoryWrite->create('export'); 42 | $fileName = 'export/' . time() . '-categories.csv'; 43 | 44 | $batch = []; 45 | foreach ($storeIds as $storeId) { 46 | $batch[] = $this->categories->execute((int)$storeId, $attributes); 47 | } 48 | $data = array_merge([], ...$batch); 49 | array_unshift($data, array_keys($data[0] ?? [])); 50 | 51 | $this->csv->setDelimiter($options->delimiter); 52 | if ($options->enclosure !== null) { 53 | $this->csv->setEnclosure($options->enclosure); 54 | } 55 | $this->csv->appendData($directoryWrite->getAbsolutePath($fileName), $data); 56 | 57 | return $directoryWrite->getRelativePath($fileName); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Model/Import/Categories.php: -------------------------------------------------------------------------------- 1 | batchByStore($data) as $storeId => $rows) { 42 | $this->processStore($storeId, $rows); 43 | } 44 | } 45 | 46 | /** 47 | * @throws NoSuchEntityException 48 | * @throws CouldNotSaveException 49 | * @throws LocalizedException 50 | */ 51 | private function processStore(int $storeId, array $rows): void 52 | { 53 | $currentStore = $this->storeManager->getStore(); 54 | $this->storeManager->setCurrentStore($storeId); 55 | $collection = $this->createCollection($storeId, array_column($rows, 'category_code')); 56 | 57 | try { 58 | foreach ($rows as $row) { 59 | $category = $collection->getItemByColumnValue('category_code', $row['category_code']) 60 | ?? $this->categoryFactory->create(); 61 | $category->addData($row); 62 | $category->setStoreId($storeId); 63 | if (isset($row['parent_code'])) { 64 | $category = $this->updateParent($category, $row['parent_code']); 65 | } 66 | $this->categoryRepository->save($category); 67 | } 68 | } catch (LocalizedException $e) { 69 | throw new LocalizedException( 70 | new Phrase( 71 | 'An error occurred while processing the category with code "%1" with the store "%2". The error is: %3', 72 | [ 73 | $row['category_code'], 74 | $this->storeManager->getStore($storeId)->getName(), 75 | $e->getMessage()] 76 | ) 77 | ); 78 | } finally { 79 | $this->storeManager->setCurrentStore($currentStore); 80 | } 81 | } 82 | 83 | /** 84 | * @throws NoSuchEntityException 85 | * @throws InputException 86 | */ 87 | private function batchByStore(array $data): array 88 | { 89 | $batch = []; 90 | foreach ($data as $row) { 91 | $batch[$this->storeManager->getStore($row['store'] ?? 'admin')->getId()][] = $this->utils->sanitizeData($row); 92 | } 93 | 94 | return $batch; 95 | } 96 | 97 | private function createCollection(int $storeId, array $codes): Collection 98 | { 99 | $collection = $this->collectionFactory->create(); 100 | $collection->setStoreId($storeId); 101 | $collection->setProductStoreId($storeId); 102 | $collection->setLoadProductCount(false); 103 | $collection->addFieldToFilter('category_code', ['in' => $codes]); 104 | 105 | return $collection; 106 | } 107 | 108 | /** 109 | * @throws LocalizedException 110 | */ 111 | private function updateParent(Category $category, string $parentCode): Category 112 | { 113 | $collection = $this->collectionFactory->create(); 114 | $collection->addAttributeToSelect('path'); 115 | $collection->setLoadProductCount(false); 116 | $collection->addAttributeToFilter('category_code', $parentCode); 117 | 118 | /** @var Category $parentCategory */ 119 | $parentCategory = $collection->getFirstItem(); 120 | $parentId = (int)$parentCategory->getId(); 121 | if ($parentId) { 122 | if ($category->getId()) { 123 | if ($parentId !== (int)$category->getParentId()) { 124 | $category->move($parentId, null); 125 | } 126 | } else { 127 | $category->setParentId($parentId); 128 | } 129 | } 130 | 131 | return $category; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Model/Import/FromCsv.php: -------------------------------------------------------------------------------- 1 | csv->setDelimiter($options->delimiter); 39 | if ($options->enclosure !== null) { 40 | $this->csv->setEnclosure($options->enclosure); 41 | } 42 | 43 | $rows = $this->csv->getData($filePath); 44 | $keys = array_map(static fn (string $key): string => preg_replace('/[^\w]/', '', $key), array_shift($rows)); 45 | $keysCount = count($keys); 46 | 47 | $data = []; 48 | foreach ($rows as $row) { 49 | if ($keysCount !== count($row)) { 50 | throw new InputException( 51 | new Phrase('The number of column does not match the keys. Please verify the field separator.') 52 | ); 53 | } 54 | $data[] = array_combine($keys, $row); 55 | } 56 | 57 | $this->categories->execute($data); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Model/Session/DownloadContext.php: -------------------------------------------------------------------------------- 1 | dataPersistor->get(self::FILE_KEY); 21 | } 22 | 23 | public function setFile(string $file): void 24 | { 25 | $this->dataPersistor->set(self::FILE_KEY, $file); 26 | } 27 | 28 | public function clearFile(): void 29 | { 30 | $this->dataPersistor->clear(self::FILE_KEY); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Model/Utils.php: -------------------------------------------------------------------------------- 1 | excludedFields->get())); 27 | 28 | return ['category_code' => $categoryCode] + $data; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Category Import Export Module for Magento 2 2 | 3 | [![Latest Stable Version](https://img.shields.io/packagist/v/opengento/module-category-import-export.svg?style=flat-square)](https://packagist.org/packages/opengento/module-category-import-export) 4 | [![License: MIT](https://img.shields.io/github/license/opengento/magento2-category-import-export.svg?style=flat-square)](./LICENSE) 5 | [![Packagist](https://img.shields.io/packagist/dt/opengento/module-category-import-export.svg?style=flat-square)](https://packagist.org/packages/opengento/module-category-import-export/stats) 6 | [![Packagist](https://img.shields.io/packagist/dm/opengento/module-category-import-export.svg?style=flat-square)](https://packagist.org/packages/opengento/module-category-import-export/stats) 7 | 8 | This module add the many countries to many stores relation and make it available to the storefront. 9 | 10 | - [Setup](#setup) 11 | - [Composer installation](#composer-installation) 12 | - [Setup the module](#setup-the-module) 13 | - [Features](#features) 14 | - [Settings](#settings) 15 | - [Documentation](#documentation) 16 | - [Support](#support) 17 | - [Authors](#authors) 18 | - [License](#license) 19 | 20 | ## Setup 21 | 22 | Magento 2 Open Source or Commerce edition is required. 23 | 24 | ### Composer installation 25 | 26 | Run the following composer command: 27 | 28 | ``` 29 | composer require opengento/module-category-import-export 30 | ``` 31 | 32 | ### Setup the module 33 | 34 | Run the following magento command: 35 | 36 | ``` 37 | bin/magento setup:upgrade 38 | ``` 39 | 40 | **If you are in production mode, do not forget to recompile and redeploy the static resources.** 41 | 42 | ## Features 43 | 44 | - You can import categories from System > Data Transfer > Import Categories 45 | - You can export categories from System > Data Transfer > Export Categories 46 | 47 | ## Documentation 48 | 49 | - The available format for import/export is CSV 50 | - A new attribute category attribute is added with the module: `category_code` which allows to identify the categories. 51 | 52 | ## Support 53 | 54 | Raise a new [request](https://github.com/opengento/magento2-category-import-export/issues) to the issue tracker. 55 | 56 | ## Authors 57 | 58 | - **Opengento Community** - *Lead* - [![Twitter Follow](https://img.shields.io/twitter/follow/opengento.svg?style=social)](https://twitter.com/opengento) 59 | - **Thomas Klein** - *Maintainer* - [![GitHub followers](https://img.shields.io/github/followers/thomas-kl1.svg?style=social)](https://github.com/thomas-kl1) 60 | - **Contributors** - *Contributor* - [![GitHub contributors](https://img.shields.io/github/contributors/opengento/magento2-category-import-export.svg?style=flat-square)](https://github.com/opengento/magento2-category-import-export/graphs/contributors) 61 | 62 | ## License 63 | 64 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) details. 65 | 66 | ***That's all folks!*** 67 | -------------------------------------------------------------------------------- /Setup/Patch/Data/AddCategoryCodeAttributeV1.php: -------------------------------------------------------------------------------- 1 | eavSetupFactory->create(['setup' => $this->setup]); 38 | 39 | $eavSetup->addAttribute( 40 | Category::ENTITY, 41 | 'category_code', 42 | [ 43 | 'type' => 'varchar', 44 | 'label' => 'Category Code', 45 | 'input' => 'text', 46 | 'source' => '', 47 | 'visible' => true, 48 | 'default' => null, 49 | 'required' => true, 50 | 'global' => ScopedAttributeInterface::SCOPE_GLOBAL, 51 | 'user_defined' => false, 52 | 'group' => '', 53 | 'backend' => '' 54 | ] 55 | ); 56 | 57 | return $this; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Setup/Patch/Data/PopulateCategoryCodeV1.php: -------------------------------------------------------------------------------- 1 | collecionFactory->create(); 45 | $categories = $collection->addAttributeToSelect(['entity_id', 'name', 'category_code', 'path'])->getItems(); 46 | 47 | $idsToName = []; 48 | /** @var Category $category */ 49 | foreach ($categories as $category) { 50 | $idsToName[$category->getId()] = $category->getName(); 51 | } 52 | 53 | /** @var Category $category */ 54 | foreach ($categories as $category) { 55 | if (!$category->getData('category_code')) { 56 | $code = []; 57 | foreach ($category->getPathIds() as $pathId) { 58 | if ($pathId === '') { 59 | throw new LocalizedException( 60 | new Phrase( 61 | 'Category "%1" has an invalid path: %2.', 62 | [$category->getName(), $category->getPath()] 63 | ) 64 | ); 65 | } 66 | $code[] = $idsToName[$pathId]; 67 | } 68 | 69 | $category->setCustomAttribute('category_code', strtolower(str_replace(' ', '_', implode('_', $code)))); 70 | $this->resourceCategory->saveAttribute($category, 'category_code'); 71 | } 72 | } 73 | 74 | return $this; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Ui/Component/Form/Button/Download.php: -------------------------------------------------------------------------------- 1 | downloadContext->getFile() 27 | ? [ 28 | 'label' => new Phrase('Download'), 29 | 'on_click' => sprintf("location.href = '%s';", $this->urlBuilder->getUrl('*/*/categoryDownload')), 30 | 'class' => 'download primary' 31 | ] 32 | : []; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opengento/module-category-import-export", 3 | "description": "This module add the capability to import and export the categories from the back-office.", 4 | "keywords": [ 5 | "php", 6 | "magento", 7 | "magento2", 8 | "import", 9 | "export", 10 | "category" 11 | ], 12 | "require": { 13 | "php": "^8.1", 14 | "magento/framework": "*", 15 | "magento/module-backend": "*", 16 | "magento/module-ui": "*", 17 | "magento/module-eav": "*", 18 | "magento/module-store": "*", 19 | "magento/module-catalog": "*" 20 | }, 21 | "require-dev": { 22 | "magento/magento-coding-standard": "^33", 23 | "roave/security-advisories": "dev-latest" 24 | }, 25 | "suggest": {}, 26 | "type": "magento2-module", 27 | "license": [ 28 | "MIT" 29 | ], 30 | "homepage": "https://github.com/opengento/magento2-category-import-export", 31 | "authors": [ 32 | { 33 | "name": "Opengento Team", 34 | "email": "opengento@gmail.com", 35 | "homepage": "https://opengento.fr/", 36 | "role": "lead" 37 | }, 38 | { 39 | "name": "Thomas Klein", 40 | "email": "thomaskein876@gmail.com", 41 | "homepage": "https://www.linkedin.com/in/thomas-klein/", 42 | "role": "maintainer" 43 | } 44 | ], 45 | "support": { 46 | "source": "https://github.com/opengento/magento2-category-import-export", 47 | "issues": "https://github.com/opengento/magento2-category-import-export/issues" 48 | }, 49 | "autoload": { 50 | "files": [ 51 | "registration.php" 52 | ], 53 | "psr-4": { 54 | "Opengento\\CategoryImportExport\\": "" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /etc/acl.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /etc/adminhtml/menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /etc/adminhtml/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | Comma-separated. 16 | 17 | 18 | 19 | Opengento\CategoryImportExport\Model\Config\Source\Category\VisibleAttributes 20 | These attributes won't be imported or exported. 21 | 22 | 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | created_at,created_in,parent_id,store_id,store,updated_at,updated_in 13 | image 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /view/adminhtml/layout/adminhtml_import_category.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /view/adminhtml/ui_component/category_form.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 |
9 |
10 | 11 | 12 | text 13 | 14 | 15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /view/adminhtml/ui_component/export_category_form.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 |
9 | 10 | 11 | export_category_form.export_category_form_data_source 12 | 13 | Export Categories 14 | templates/form/collapsible 15 | 16 | 17 | 18 | 22 | 22 | 23 | import_category_form 24 | data 25 | 26 | import_category_form.import_category_form_data_source 27 | 28 |
29 | 30 | 31 | 32 | Magento_Ui/js/form/provider 33 | 34 | 35 | 36 | 37 | 38 | Opengento_CategoryImportExport::import 39 | 40 | 41 | id 42 | id 43 | 44 | 45 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | file 54 | 59 | 60 | 61 | 62 | 63 | 64 | , 65 | 66 | 67 | 68 | 69 | ui/form/element/input 70 | delimiter 71 | 72 | true 73 | 74 | 75 | 76 | 77 | 78 | 79 | " 80 | 81 | 82 | 83 | 84 | ui/form/element/input 85 | enclosure 86 | 87 | true 88 | 89 | 90 | 91 |
92 | 93 | --------------------------------------------------------------------------------