diff --git a/fs_import_image_advanced/README.rst b/fs_import_image_advanced/README.rst new file mode 100644 index 0000000000..1cc2689a92 --- /dev/null +++ b/fs_import_image_advanced/README.rst @@ -0,0 +1,87 @@ +============================ +Import Storage product image +============================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6b8c3532008323c4855704ee06cb4519b25bf2ff635698948a373b218cc331cf + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github + :target: https://github.com/OCA/storage/tree/18.0/fs_import_image_advanced + :alt: OCA/storage +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/storage-18-0/storage-18-0-fs_import_image_advanced + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Import product image from a CVS file from URLs or ZIP file', Idea based +on an idea of the 'image_product_import' from Cybrosys Techno Solutions + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Akretion +* Camptocamp + +Contributors +------------ + +- Sylvain Calador +- Saritha +- Simone Orsi +- Héctor Villarreal +- Do Anh Duy + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fs_import_image_advanced/__init__.py b/fs_import_image_advanced/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/fs_import_image_advanced/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/fs_import_image_advanced/__manifest__.py b/fs_import_image_advanced/__manifest__.py new file mode 100644 index 0000000000..47b40b993e --- /dev/null +++ b/fs_import_image_advanced/__manifest__.py @@ -0,0 +1,32 @@ +# Copyright 2018 Akretion (http://www.akretion.com). +# Author: Sylvain Calador () +# Author: Saritha Sahadevan () +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Import Storage product image", + "version": "18.0.1.0.0", + "summary": "Import product images using CSV", + "author": "Akretion, Camptocamp, Odoo Community Association (OCA)", + "company": "Akretion", + "maintainer": "Akretion", + "website": "https://github.com/OCA/storage", + "category": "Product", + "development_status": "Alpha", + "depends": ["fs_product_multi_image", "sale", "queue_job"], + "external_dependencies": { + "python": ["python-magic", "validators"], + "deb": ["libmagic1"], + }, + "data": [ + "data/ir_cron.xml", + "data/queue_job_channel_data.xml", + "data/queue_job_function_data.xml", + "security/ir_model_access.xml", + "views/import_product_image_view.xml", + "views/report_html.xml", + ], + "license": "AGPL-3", + "installable": True, +} diff --git a/fs_import_image_advanced/data/ir_cron.xml b/fs_import_image_advanced/data/ir_cron.xml new file mode 100644 index 0000000000..ffe6c7c4bf --- /dev/null +++ b/fs_import_image_advanced/data/ir_cron.xml @@ -0,0 +1,13 @@ + + + + Storage image imports cleanup + + + 1 + days + + code + model._cron_cleanup_obsolete() + + diff --git a/fs_import_image_advanced/data/queue_job_channel_data.xml b/fs_import_image_advanced/data/queue_job_channel_data.xml new file mode 100644 index 0000000000..d27ad6f172 --- /dev/null +++ b/fs_import_image_advanced/data/queue_job_channel_data.xml @@ -0,0 +1,6 @@ + + + import_image + + + diff --git a/fs_import_image_advanced/data/queue_job_function_data.xml b/fs_import_image_advanced/data/queue_job_function_data.xml new file mode 100644 index 0000000000..6b647b5d58 --- /dev/null +++ b/fs_import_image_advanced/data/queue_job_function_data.xml @@ -0,0 +1,10 @@ + + + + do_import + + + diff --git a/fs_import_image_advanced/i18n/shopinvader_import_image.pot b/fs_import_image_advanced/i18n/shopinvader_import_image.pot new file mode 100644 index 0000000000..006ea3e8d8 --- /dev/null +++ b/fs_import_image_advanced/i18n/shopinvader_import_image.pot @@ -0,0 +1,266 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * shopinvader_import_image +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__file_csv +msgid "CSV file" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__csv_delimiter +msgid "CSV file delimiter" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__csv_header +msgid "CSV file header" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__chunk_size +msgid "Chunk Size" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__create_missing_tags +msgid "Create Missing Tags" +msgstr "" + +#. module: shopinvader_import_image +#: code:addons/shopinvader_import_image/models/import_image.py:0 +#, python-format +msgid "Created" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__create_uid +msgid "Created by" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__create_date +msgid "Created on" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__display_name +msgid "Display Name" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields.selection,name:shopinvader_import_image.selection__shopinvader_import_product_image__state__done +msgid "Done" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__done_on +msgid "Done On" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields.selection,name:shopinvader_import_image.selection__shopinvader_import_product_image__source_type__external_storage +msgid "External storage" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model,name:shopinvader_import_image.model_shopinvader_import_product_image +msgid "Handle import of shopinvader product images" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,help:shopinvader_import_image.field_shopinvader_import_product_image__chunk_size +msgid "How many lines will be handled in each job." +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__id +msgid "ID" +msgstr "" + +#. module: shopinvader_import_image +#: code:addons/shopinvader_import_image/models/import_image.py:0 +#, python-format +msgid "Image file not found" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.actions.act_window,name:shopinvader_import_image.import_image_action +msgid "Import Product Image" +msgstr "" + +#. module: shopinvader_import_image +#: model_terms:ir.ui.view,arch_db:shopinvader_import_image.product_import_image_form_view +msgid "Import images" +msgstr "" + +#. module: shopinvader_import_image +#: model_terms:ir.ui.view,arch_db:shopinvader_import_image.product_import_image_form_view +msgid "Import images again" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.ui.menu,name:shopinvader_import_image.menu_backend_shopinvader_image_import +#: model:ir.ui.menu,name:shopinvader_import_image.menu_sale_shopinvader_image_import +msgid "Import product images" +msgstr "" + +#. module: shopinvader_import_image +#: model_terms:ir.ui.view,arch_db:shopinvader_import_image.product_import_image_form_view +#: model_terms:ir.ui.view,arch_db:shopinvader_import_image.product_import_image_tree_view +msgid "Import shopinvader product images" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__state +msgid "Import state" +msgstr "" + +#. module: shopinvader_import_image +#: code:addons/shopinvader_import_image/models/import_image.py:0 +#, python-format +msgid "Invalid CSV file headers found! Expected: %s" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image____last_update +msgid "Last Modified on" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__write_date +msgid "Last Updated on" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields.selection,name:shopinvader_import_image.selection__shopinvader_import_product_image__state__new +msgid "New" +msgstr "" + +#. module: shopinvader_import_image +#: code:addons/shopinvader_import_image/models/import_image.py:0 +#, python-format +msgid "No storage backend provided!" +msgstr "" + +#. module: shopinvader_import_image +#: code:addons/shopinvader_import_image/models/import_image.py:0 +#, python-format +msgid "No zip file provided!" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__options +msgid "Options" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__overwrite +msgid "Overwrite image with same name" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__external_csv_path +msgid "Path to CSV file" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__product_model +msgid "Product Model" +msgstr "" + +#. module: shopinvader_import_image +#: code:addons/shopinvader_import_image/models/import_image.py:0 +#, python-format +msgid "Product not found" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields.selection,name:shopinvader_import_image.selection__shopinvader_import_product_image__product_model__product_template +msgid "Product template" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields.selection,name:shopinvader_import_image.selection__shopinvader_import_product_image__product_model__product_product +msgid "Product variants" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,help:shopinvader_import_image.field_shopinvader_import_product_image__external_csv_path +msgid "Relative path of the CSV file located in the external storage" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__report +#: model_terms:ir.ui.view,arch_db:shopinvader_import_image.product_import_image_form_view +msgid "Report" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__report_html +msgid "Report Html" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields.selection,name:shopinvader_import_image.selection__shopinvader_import_product_image__state__scheduled +msgid "Scheduled" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.actions.server,name:shopinvader_import_image.ir_cron_import_images_cleanup_ir_actions_server +#: model:ir.cron,cron_name:shopinvader_import_image.ir_cron_import_images_cleanup +#: model:ir.cron,name:shopinvader_import_image.ir_cron_import_images_cleanup +msgid "Shopinvader image imports cleanup" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__source_type +msgid "Source type" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__fs_storage_id +msgid "Storage Backend" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__source_fs_storage_id +msgid "Storage Backend with images" +msgstr "" + +#. module: shopinvader_import_image +#: code:addons/shopinvader_import_image/models/import_image.py:0 +#, python-format +msgid "Tags not found" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields.selection,name:shopinvader_import_image.selection__shopinvader_import_product_image__source_type__url +msgid "URL" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__source_zipfile +msgid "ZIP with images" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields.selection,name:shopinvader_import_image.selection__shopinvader_import_product_image__source_type__zip_file +msgid "Zip file" +msgstr "" diff --git a/fs_import_image_advanced/i18n/storage_import_image_advanced.pot b/fs_import_image_advanced/i18n/storage_import_image_advanced.pot new file mode 100644 index 0000000000..80d3f6f870 --- /dev/null +++ b/fs_import_image_advanced/i18n/storage_import_image_advanced.pot @@ -0,0 +1,330 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_import_image_advanced +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: storage_import_image_advanced +#: code:addons/storage_import_image_advanced/models/import_image.py:0 +#, python-format +msgid "CSV Schema Incompatible" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__file_csv +msgid "CSV file" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__csv_delimiter +msgid "CSV file delimiter" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__chunk_size +msgid "Chunk Size" +msgstr "" + +#. module: storage_import_image_advanced +#: model_terms:ir.ui.view,arch_db:storage_import_image_advanced.product_import_image_form_view +msgid "Column Mapping" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__create_missing_tags +msgid "Create Missing Tags" +msgstr "" + +#. module: storage_import_image_advanced +#: code:addons/storage_import_image_advanced/models/import_image.py:0 +#, python-format +msgid "Created" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__create_uid +msgid "Created by" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__create_date +msgid "Created on" +msgstr "" + +#. module: storage_import_image_advanced +#: model_terms:ir.ui.view,arch_db:storage_import_image_advanced.product_import_image_form_view +msgid "Destination" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__display_name +msgid "Display Name" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields.selection,name:storage_import_image_advanced.selection__storage_import_product_image__state__done +msgid "Done" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__done_on +msgid "Done On" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields.selection,name:storage_import_image_advanced.selection__storage_import_product_image__source_type__external_storage +msgid "External storage" +msgstr "" + +#. module: storage_import_image_advanced +#: model_terms:ir.ui.view,arch_db:storage_import_image_advanced.product_import_image_form_view +msgid "File Path" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__filename +msgid "Filename" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__filename_zip +msgid "Filename Zip" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model,name:storage_import_image_advanced.model_storage_import_product_image +msgid "Handle import of storage product images" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,help:storage_import_image_advanced.field_storage_import_product_image__chunk_size +msgid "How many lines will be handled in each job." +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__id +msgid "ID" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__csv_column_tag_name +msgid "Image Tag Name column" +msgstr "" + +#. module: storage_import_image_advanced +#: code:addons/storage_import_image_advanced/models/import_image.py:0 +#, python-format +msgid "Image file not found" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__csv_column_file_path +msgid "Image file path column" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.actions.act_window,name:storage_import_image_advanced.import_image_action +msgid "Import Product Image" +msgstr "" + +#. module: storage_import_image_advanced +#: model_terms:ir.ui.view,arch_db:storage_import_image_advanced.product_import_image_form_view +msgid "Import images" +msgstr "" + +#. module: storage_import_image_advanced +#: model_terms:ir.ui.view,arch_db:storage_import_image_advanced.product_import_image_form_view +msgid "Import images again" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.ui.menu,name:storage_import_image_advanced.menu_sale_storage_image_import +msgid "Import product images" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__state +msgid "Import state" +msgstr "" + +#. module: storage_import_image_advanced +#: model_terms:ir.ui.view,arch_db:storage_import_image_advanced.product_import_image_form_view +msgid "Import storage product images" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image____last_update +msgid "Last Modified on" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__write_date +msgid "Last Updated on" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields.selection,name:storage_import_image_advanced.selection__storage_import_product_image__state__new +msgid "New" +msgstr "" + +#. module: storage_import_image_advanced +#: code:addons/storage_import_image_advanced/models/import_image.py:0 +#, python-format +msgid "No storage backend provided!" +msgstr "" + +#. module: storage_import_image_advanced +#: code:addons/storage_import_image_advanced/models/import_image.py:0 +#, python-format +msgid "No zip file provided!" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__options +#: model_terms:ir.ui.view,arch_db:storage_import_image_advanced.product_import_image_form_view +msgid "Options" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__overwrite +msgid "Overwrite image with same name" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__external_csv_path +msgid "Path to CSV file" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__product_model +msgid "Product Model" +msgstr "" + +#. module: storage_import_image_advanced +#: model_terms:ir.ui.view,arch_db:storage_import_image_advanced.product_import_image_form_view +msgid "Product Reference" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__csv_column_default_code +msgid "Product Reference column" +msgstr "" + +#. module: storage_import_image_advanced +#: code:addons/storage_import_image_advanced/models/import_image.py:0 +#, python-format +msgid "Product not found" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields.selection,name:storage_import_image_advanced.selection__storage_import_product_image__product_model__product_template +msgid "Product template" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields.selection,name:storage_import_image_advanced.selection__storage_import_product_image__product_model__product_product +msgid "Product variants" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,help:storage_import_image_advanced.field_storage_import_product_image__external_csv_path +msgid "Relative path of the CSV file located in the external storage" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__report +#: model_terms:ir.ui.view,arch_db:storage_import_image_advanced.product_import_image_form_view +msgid "Report" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__report_html +msgid "Report Html" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields.selection,name:storage_import_image_advanced.selection__storage_import_product_image__state__scheduled +msgid "Scheduled" +msgstr "" + +#. module: storage_import_image_advanced +#: model_terms:ir.ui.view,arch_db:storage_import_image_advanced.product_import_image_form_view +msgid "Source" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__source_type +msgid "Source Type" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__fs_storage_id +msgid "Storage Backend" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__source_fs_storage_id +msgid "Storage Backend with images" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.actions.server,name:storage_import_image_advanced.ir_cron_import_images_cleanup_ir_actions_server +#: model:ir.cron,cron_name:storage_import_image_advanced.ir_cron_import_images_cleanup +#: model:ir.cron,name:storage_import_image_advanced.ir_cron_import_images_cleanup +msgid "Storage image imports cleanup" +msgstr "" + +#. module: storage_import_image_advanced +#: model_terms:ir.ui.view,arch_db:storage_import_image_advanced.product_import_image_form_view +msgid "Tag Name" +msgstr "" + +#. module: storage_import_image_advanced +#: code:addons/storage_import_image_advanced/models/import_image.py:0 +#, python-format +msgid "Tags not found" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,help:storage_import_image_advanced.field_storage_import_product_image__csv_column_file_path +msgid "The CSV File column name that holds the image file path or url." +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,help:storage_import_image_advanced.field_storage_import_product_image__csv_column_tag_name +msgid "The CSV File column name that holds the image tag name." +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,help:storage_import_image_advanced.field_storage_import_product_image__csv_column_default_code +msgid "The CSV File column name that holds the product reference." +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields.selection,name:storage_import_image_advanced.selection__storage_import_product_image__source_type__url +msgid "URL" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields,field_description:storage_import_image_advanced.field_storage_import_product_image__source_zipfile +msgid "ZIP with images" +msgstr "" + +#. module: storage_import_image_advanced +#: model:ir.model.fields.selection,name:storage_import_image_advanced.selection__storage_import_product_image__source_type__zip_file +msgid "Zip file" +msgstr "" diff --git a/fs_import_image_advanced/models/__init__.py b/fs_import_image_advanced/models/__init__.py new file mode 100644 index 0000000000..71872ae248 --- /dev/null +++ b/fs_import_image_advanced/models/__init__.py @@ -0,0 +1 @@ +from . import import_image diff --git a/fs_import_image_advanced/models/import_image.py b/fs_import_image_advanced/models/import_image.py new file mode 100644 index 0000000000..abf1617c1c --- /dev/null +++ b/fs_import_image_advanced/models/import_image.py @@ -0,0 +1,380 @@ +# Copyright 2018 Akretion (http://www.akretion.com). +# Author: Sylvain Calador () +# Author: Saritha Sahadevan () +# Copyright 2020 Camptocamp (http://www.camptocamp.com) +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +import base64 +import csv +import io +import logging +import os +import sys +from contextlib import closing +from urllib.request import urlopen +from zipfile import ZipFile + +from odoo import api, exceptions, fields, models +from odoo.tools import date_utils + +from odoo.addons.base_sparse_field.models.fields import Serialized + +_logger = logging.getLogger(__name__) + +try: + import magic + import validators +except (OSError, ImportError) as err: + _logger.debug(err) + + +def gen_chunks(iterable, chunksize=10): + """Chunk generator. + + Take an iterable and yield `chunksize` sized slices. + "Borrowed" from connector_importer. + """ + chunk = [] + last_chunk = False + for i, line in enumerate(iterable): + if i % chunksize == 0 and i > 0: + yield chunk, last_chunk + del chunk[:] + chunk.append(line) + last_chunk = True + yield chunk, last_chunk + + +class ProductImageImportWizard(models.Model): + _name = "storage.import.product_image" + _description = "Handle import of storage product images" + + @api.model + def _default_csv_header(self): + product_identifier = self._get_product_identifier_field() + flds = [product_identifier, "tag", "path"] + return ",".join(flds) + + product_model = fields.Selection( + [ + ("product.template", "Product template"), + ("product.product", "Product variants"), + ], + required=True, + ) + source_type = fields.Selection( + [ + ("url", "URL"), + ("zip_file", "Zip file"), + ("external_storage", "External storage"), + ], + required=True, + default="url", + ) + filename = fields.Char() + filename_zip = fields.Char() + file_csv = fields.Binary(string="CSV file", required=True) + csv_delimiter = fields.Char( + string="CSV file delimiter", + default=",", + required=True, + ) + csv_column_default_code = fields.Char( + string="Product Reference column", + help="The CSV File column name that holds the product reference.", + default="default_code", + required=True, + ) + csv_column_tag_name = fields.Char( + string="Image Tag Name column", + help="The CSV File column name that holds the image tag name.", + default="tag", + required=True, + ) + csv_column_file_path = fields.Char( + string="Image file path column", + help="The CSV File column name that holds the image file path or url.", + default="path", + required=True, + ) + source_zipfile = fields.Binary("ZIP with images", required=False) + source_fs_storage_id = fields.Many2one("fs.storage", "FS Storage with images") + external_csv_path = fields.Char( + string="Path to CSV file", + help="Relative path of the CSV file located in the external storage", + ) + options = Serialized(readonly=True) + overwrite = fields.Boolean( + "Overwrite image with same name", sparse="options", default=False + ) + create_missing_tags = fields.Boolean(sparse="options", default=False) + chunk_size = fields.Integer( + sparse="options", + default=10, + help="How many lines will be handled in each job.", + ) + report = Serialized(readonly=True) + report_html = fields.Html(readonly=True, compute="_compute_report_html") + state = fields.Selection( + [("new", "New"), ("scheduled", "Scheduled"), ("done", "Done")], + string="Import state", + default="new", + ) + done_on = fields.Datetime() + + @api.depends("report") + def _compute_report_html(self): + # TODO: add tests + for record in self: + if not record.report: + record.report_html = "" + continue + report_html = self.env["ir.qweb"]._render( + "fs_import_image_advanced.report_html", {"record": record} + ) + record.report_html = report_html + + @api.model + def _get_base64(self, file_path): + res = {} + binary = None + mimetype = None + binary = getattr(self, "_read_from_" + self.source_type)(file_path) + if binary: + mimetype = magic.from_buffer(binary, mime=True) + res = {"mimetype": mimetype, "b64": base64.encodebytes(binary)} + return res + + def _read_from_url(self, file_path): + if validators.url(file_path): + return urlopen(file_path, timeout=120).read() + return None + + def _read_from_zip_file(self, file_path): + if not self.source_zipfile: + raise exceptions.UserError(self.env._("No zip file provided!")) + file_content = base64.b64decode(self.source_zipfile) + with closing(io.BytesIO(file_content)) as zip_file: + with ZipFile(zip_file, "r") as z: + try: + return z.read(file_path) + except KeyError: + # File missing + return None + + def _read_from_external_storage(self, file_path): + if not self.source_fs_storage_id: + raise exceptions.UserError(self.env._("No storage backend provided!")) + return self.source_fs_storage_id._get_filesystem().open(file_path) + + def _read_csv(self): + if self.file_csv: + return base64.b64decode(self.file_csv) + elif self.external_csv_path: + return self.source_fs_storage_id._get_bin_data(self.external_csv_path) + + def _get_lines(self): + lines = [] + product_identifier_field = self._get_product_identifier_field() + mapping = { + product_identifier_field: self.csv_column_default_code, + "tag_name": self.csv_column_tag_name, + "file_path": self.csv_column_file_path, + } + with closing(io.BytesIO(self._read_csv())) as binary_file: + csv_file = (line.decode("utf8") for line in binary_file) + reader = csv.DictReader(csv_file, delimiter=self.csv_delimiter) + csv.field_size_limit(sys.maxsize) + for row in reader: + try: + line = {key: row[column] for key, column in mapping.items()} + except KeyError as e: + _logger.error(e) + raise exceptions.UserError( + self.env._("CSV Schema Incompatible") + ) from e + lines.append(line) + return lines + + def _get_options(self): + return self.options or {} + + def action_import(self): + self.report = self.report_html = False + self.state = "scheduled" + # Generate N chunks to split in several jobs. + chunks = gen_chunks( + self._get_lines(), chunksize=self._get_options().get("chunk_size") + ) + for i, (chunk, is_last_chunk) in enumerate(chunks, 1): + self.with_delay().do_import(lines=chunk, last_chunk=is_last_chunk) + _logger.info( + "Generated job for chunk nr %d. Is last: %s.", + i, + "yes" if is_last_chunk else "no", + ) + + def do_import(self, lines=None, last_chunk=False): + lines = lines or self._get_lines() + report = self._do_import(lines, self.product_model, options=self._get_options()) + # Refresh report + extendable_keys = [ + "created", + "file_not_found", + "missing", + "missing_tags", + ] + prev_report = self.report or {} + for k, v in report.items(): + if k in extendable_keys and prev_report.get(k): + report[k] = sorted(set(prev_report[k] + v)) + + # Lock as writing can come from several jobs + sql = f"SELECT id FROM {self._table} WHERE ID IN %s FOR UPDATE" + self.env.cr.execute(sql, (tuple(self.ids),), log_exceptions=False) + self.write( + { + "report": report, + "state": "done" if last_chunk else self.state, + "done_on": fields.Datetime.now() if last_chunk else False, + } + ) + return report + + def _get_product_identifier_field(self): + """Override if you want to use another field as product identifier""" + return "default_code" + + def _assign_product_tmpl_attr_values(self, product): + prod_tmpl_attr_value_obj = self.env["product.template.attribute.value"] + return prod_tmpl_attr_value_obj.browse( + product["product_template_attribute_value_ids"] + ) + + @api.model + def _post_process_image(self, image): + pass + + def _do_import(self, lines, product_model, options=None): + tag_obj = self.env["image.tag"] + image_obj = self.env["fs.image"] + relation_obj = self.env["fs.product.image"] + product_identifier_field = self._get_product_identifier_field() + report = { + "created": set(), + "file_not_found": set(), + "missing": [], + "missing_tags": [], + } + options = options or {} + + # do all query at once + lines_by_code = {x[product_identifier_field]: x for x in lines} + all_codes = list(lines_by_code.keys()) + _fields = [product_identifier_field, "product_tmpl_id"] + if product_model == "product.template": + # exclude template id + _fields = _fields[:1] + else: + _fields.append("product_template_attribute_value_ids") + product_identifier_field = self._get_product_identifier_field() + products = self.env[product_model].search_read( + [(product_identifier_field, "in", all_codes)], _fields + ) + existing_by_code = {x[product_identifier_field]: x for x in products} + report["missing"] = sorted( + code for code in all_codes if not existing_by_code.get(code) + ) + + all_tags = [x["tag_name"] for x in lines if x["tag_name"]] + tags = tag_obj.search_read([("name", "in", all_tags)], ["name"]) + tag_by_name = {x["name"]: x["id"] for x in tags} + missing_tags = set(all_tags).difference(set(tag_by_name.keys())) + if missing_tags: + if options.get("create_missing_tags"): + for tag_name in missing_tags: + tag_by_name[tag_name] = tag_obj.create({"name": tag_name}).id + else: + report["missing_tags"] = sorted(missing_tags) + + for prod in products: + line = lines_by_code[prod[product_identifier_field]] + file_path = line["file_path"] + file_vals = self._prepare_file_values(file_path) + if not file_vals: + report["file_not_found"].add(prod[product_identifier_field]) + continue + tag_id = tag_by_name.get(line["tag_name"]) + + if product_model == "product.template": + tmpl_id = prod["id"] + elif product_model == "product.product": + # TODO: test product.product import + tmpl_id = prod["product_tmpl_id"][0] + + image = image_obj.create(file_vals) + self._post_process_image(image) + if options.get("overwrite"): + domain = [ + ("image_id.name", "=", image.name), + ("tag_id", "=", tag_id), + ("product_tmpl_id", "=", tmpl_id), + ] + relation_obj.search(domain).unlink() + + img_relation_values = { + "image_id": image.id, + "tag_id": tag_id, + "product_tmpl_id": tmpl_id, + } + # Assign specific product attribute values + if ( + product_model == "product.product" + and prod["product_template_attribute_value_ids"] + ): + img_relation_values["attribute_value_ids"] = [ + ( + 6, + 0, + self._assign_product_tmpl_attr_values(prod) + .mapped("product_attribute_value_id") + .ids, + ) + ] + relation_obj.create(img_relation_values) + report["created"].add(prod[product_identifier_field]) + report["created"] = sorted(report["created"]) + report["file_not_found"] = sorted(report["file_not_found"]) + return report + + def _prepare_file_values(self, file_path, filetype="image"): + name = os.path.basename(file_path) + file_data = self._get_base64(file_path) + if not file_data: + return {} + vals = { + "image": { + "filename": name, + "content": file_data["b64"], + }, + } + return vals + + @api.model + def _cron_cleanup_obsolete(self, days=7): + from_date = fields.Datetime.now().replace(hour=23, minute=59, second=59) + limit_date = date_utils.subtract(from_date, days) + records = self.search([("state", "=", "done"), ("done_on", "<=", limit_date)]) + records.unlink() + _logger.info("Cleanup obsolete images import. %d records found.", len(records)) + + def _report_label_for(self, key): + labels = { + "created": self.env._("Created"), + "file_not_found": self.env._("Image file not found"), + "missing": self.env._("Product not found"), + "missing_tags": self.env._("Tags not found"), + } + return labels.get(key, key) diff --git a/fs_import_image_advanced/pyproject.toml b/fs_import_image_advanced/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/fs_import_image_advanced/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fs_import_image_advanced/readme/CONTRIBUTORS.md b/fs_import_image_advanced/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..89cd99ee93 --- /dev/null +++ b/fs_import_image_advanced/readme/CONTRIBUTORS.md @@ -0,0 +1,5 @@ +- Sylvain Calador \<\> +- Saritha \<\> +- Simone Orsi \<\> +- Héctor Villarreal \<\> +- Do Anh Duy \<\> diff --git a/fs_import_image_advanced/readme/DESCRIPTION.md b/fs_import_image_advanced/readme/DESCRIPTION.md new file mode 100644 index 0000000000..2d9deaae7f --- /dev/null +++ b/fs_import_image_advanced/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +Import product image from a CVS file from URLs or ZIP file', Idea based +on an idea of the 'image_product_import' from Cybrosys Techno Solutions diff --git a/fs_import_image_advanced/security/ir_model_access.xml b/fs_import_image_advanced/security/ir_model_access.xml new file mode 100644 index 0000000000..a8e59e8653 --- /dev/null +++ b/fs_import_image_advanced/security/ir_model_access.xml @@ -0,0 +1,13 @@ + + + + storage_import_product_image user + + + + + + + + + diff --git a/fs_import_image_advanced/static/description/icon.png b/fs_import_image_advanced/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/fs_import_image_advanced/static/description/icon.png differ diff --git a/fs_import_image_advanced/static/description/index.html b/fs_import_image_advanced/static/description/index.html new file mode 100644 index 0000000000..cd3ce8ff3d --- /dev/null +++ b/fs_import_image_advanced/static/description/index.html @@ -0,0 +1,435 @@ + + + + + +Import Storage product image + + + +
+

Import Storage product image

+ + +

Alpha License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

Import product image from a CVS file from URLs or ZIP file’, Idea based +on an idea of the ‘image_product_import’ from Cybrosys Techno Solutions

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/fs_import_image_advanced/tests/__init__.py b/fs_import_image_advanced/tests/__init__.py new file mode 100644 index 0000000000..069e16d855 --- /dev/null +++ b/fs_import_image_advanced/tests/__init__.py @@ -0,0 +1 @@ +from . import test_import_image diff --git a/fs_import_image_advanced/tests/fixture/A001.jpg b/fs_import_image_advanced/tests/fixture/A001.jpg new file mode 100644 index 0000000000..7f0b01ed13 Binary files /dev/null and b/fs_import_image_advanced/tests/fixture/A001.jpg differ diff --git a/fs_import_image_advanced/tests/fixture/A002.jpg b/fs_import_image_advanced/tests/fixture/A002.jpg new file mode 100644 index 0000000000..3a16ed9dd0 Binary files /dev/null and b/fs_import_image_advanced/tests/fixture/A002.jpg differ diff --git a/fs_import_image_advanced/tests/fixture/A003.jpg b/fs_import_image_advanced/tests/fixture/A003.jpg new file mode 100644 index 0000000000..798db4f5fb Binary files /dev/null and b/fs_import_image_advanced/tests/fixture/A003.jpg differ diff --git a/fs_import_image_advanced/tests/fixture/image_import_test.csv b/fs_import_image_advanced/tests/fixture/image_import_test.csv new file mode 100644 index 0000000000..77e08e331d --- /dev/null +++ b/fs_import_image_advanced/tests/fixture/image_import_test.csv @@ -0,0 +1,5 @@ +default_code,tag,path +A001,A001 tag,A001.jpg +A002,A002 tag,A002.jpg +A003,A003 tag,A003.jpg +A004,,A004-MISSING.jpg diff --git a/fs_import_image_advanced/tests/fixture/image_import_test.zip b/fs_import_image_advanced/tests/fixture/image_import_test.zip new file mode 100644 index 0000000000..985b690ef1 Binary files /dev/null and b/fs_import_image_advanced/tests/fixture/image_import_test.zip differ diff --git a/fs_import_image_advanced/tests/fixture/image_import_test_1.csv b/fs_import_image_advanced/tests/fixture/image_import_test_1.csv new file mode 100644 index 0000000000..8f020c02e8 --- /dev/null +++ b/fs_import_image_advanced/tests/fixture/image_import_test_1.csv @@ -0,0 +1,2 @@ +column,tag,path +A001,A001 tag,A001.jpg diff --git a/fs_import_image_advanced/tests/test_import_image.py b/fs_import_image_advanced/tests/test_import_image.py new file mode 100644 index 0000000000..e2f2a5dcc2 --- /dev/null +++ b/fs_import_image_advanced/tests/test_import_image.py @@ -0,0 +1,203 @@ +# Copyright 2020 Camptocamp (http://www.camptocamp.com) +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import base64 +import os +from unittest import mock + +import fsspec + +from odoo.exceptions import UserError +from odoo.tools import mute_logger + +from odoo.addons.fs_product_multi_image.tests.test_fs_product_multi_image import ( + TestFsProductMultiImage, +) + + +class TestStorageImportImageCase(TestFsProductMultiImage): + @staticmethod + def _get_file_content(name, base_path=None, as_binary=False): + path = base_path or os.path.dirname(os.path.abspath(__file__)) + with open(os.path.join(path, "fixture", name), "rb") as f: + data = f.read() + if as_binary: + return data + return base64.b64encode(data) + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.temp_storage = cls.env.ref("fs_storage.fs_storage_demo") + cls.base_path = os.path.dirname(os.path.abspath(__file__)) + cls.file_csv_content = cls._get_file_content( + "image_import_test.csv", base_path=cls.base_path + ) + cls.file_zip_content = cls._get_file_content( + "image_import_test.zip", base_path=cls.base_path + ) + + cls.wiz = cls._get_wizard(cls) + cls.products = cls.env["product.template"].search([], limit=3) + cls.products[0].write({"default_code": "A001", "image_ids": False}) + cls.products[1].write({"default_code": "A002", "image_ids": False}) + cls.products[2].write({"default_code": "A004", "image_ids": False}) + + def _get_wizard(self, **kw): + vals = { + "product_model": "product.template", + "file_csv": self.file_csv_content, + "source_zipfile": self.file_zip_content, + "source_type": "zip_file", + } + vals.update(kw) + return self.env["storage.import.product_image"].create(vals) + + +class TestStorageImportImage(TestStorageImportImageCase): + def test_get_lines(self): + lines = self.wiz._get_lines() + expected = [ + { + "default_code": "A00%d" % x, + "tag_name": "A00%d tag" % x, + "file_path": "A00%d.jpg" % x, + } + for x in range(1, 4) + ] + [{"default_code": "A004", "file_path": "A004-MISSING.jpg", "tag_name": ""}] + self.assertEqual(lines, expected) + + def test_get_lines_failed(self): + file_csv_content = self._get_file_content( + "image_import_test_1.csv", base_path=self.base_path + ) + self.wiz.file_csv = file_csv_content + with mute_logger("odoo.addons.fs_import_image_advanced.models.import_image"): + with self.assertRaises(UserError): + self.wiz._get_lines() + + def test_read_from_zip(self): + img_content = self._get_file_content( + "A001.jpg", base_path=self.base_path, as_binary=True + ) + self.assertEqual(self.wiz._read_from_zip_file("A001.jpg"), img_content) + + def test_get_b64(self): + img_content = self._get_file_content( + "A001.jpg", base_path=self.base_path, as_binary=True + ) + self.assertEqual( + self.wiz._get_base64("A001.jpg"), + {"mimetype": "image/jpeg", "b64": base64.encodebytes(img_content)}, + ) + + def test_import_errors(self): + self.products[0].default_code = "NONE" + self.products[1].default_code = "NONE" + self.wiz.do_import() + self.assertEqual( + self.wiz.report, + { + "created": [], + "missing": ["A001", "A002", "A003"], + "missing_tags": ["A001 tag", "A002 tag", "A003 tag"], + "file_not_found": ["A004"], + }, + ) + + def test_import_no_overwrite(self): + self.wiz.do_import() + self.assertEqual( + self.wiz.report, + { + "created": ["A001", "A002"], + "missing": ["A003"], + "missing_tags": ["A001 tag", "A002 tag", "A003 tag"], + "file_not_found": ["A004"], + }, + ) + self.assertEqual(len(self.products[0].image_ids), 1) + self.assertEqual(len(self.products[1].image_ids), 1) + self.assertFalse(self.products[0].image_ids[0].tag_id) + self.assertFalse(self.products[1].image_ids[0].tag_id) + + self.wiz.do_import() + self.assertEqual( + self.wiz.report, + { + "created": ["A001", "A002"], + "missing": ["A003"], + "missing_tags": ["A001 tag", "A002 tag", "A003 tag"], + "file_not_found": ["A004"], + }, + ) + self.assertEqual(len(self.products[0].image_ids), 2) + self.assertEqual(len(self.products[1].image_ids), 2) + + def test_import_overwrite(self): + self.wiz.overwrite = True + self.wiz.do_import() + self.assertEqual( + self.wiz.report, + { + "created": ["A001", "A002"], + "missing": ["A003"], + "missing_tags": ["A001 tag", "A002 tag", "A003 tag"], + "file_not_found": ["A004"], + }, + ) + self.assertEqual(len(self.products[0].image_ids), 1) + self.assertEqual(len(self.products[1].image_ids), 1) + self.wiz.do_import() + self.assertEqual( + self.wiz.report, + { + "created": ["A001", "A002"], + "missing": ["A003"], + "missing_tags": ["A001 tag", "A002 tag", "A003 tag"], + "file_not_found": ["A004"], + }, + ) + self.assertEqual(len(self.products[0].image_ids), 1) + self.assertEqual(len(self.products[1].image_ids), 1) + + def test_import_create_missing_tags(self): + self.wiz.overwrite = True + self.wiz.create_missing_tags = True + self.wiz.do_import() + self.assertEqual( + self.wiz.report, + { + "created": ["A001", "A002"], + "missing": ["A003"], + "missing_tags": [], + "file_not_found": ["A004"], + }, + ) + self.assertEqual(len(self.products[0].image_ids), 1) + self.assertEqual(len(self.products[1].image_ids), 1) + self.assertEqual(self.products[0].image_ids[0].tag_id.name, "A001 tag") + self.assertEqual(self.products[1].image_ids[0].tag_id.name, "A002 tag") + + def test_import_with_storage(self): + wiz = self._get_wizard( + source_type="external_storage", + source_fs_storage_id=self.temp_storage.id, + create_missing_tags=True, + ) + fake_image_binary = self._get_file_content( + "A001.jpg", base_path=self.base_path, as_binary=True + ) + with mock.patch.object(fsspec.AbstractFileSystem, "open") as mocked: + mocked.return_value = fake_image_binary + wiz.do_import() + self.assertEqual( + wiz.report, + { + "created": ["A001", "A002", "A004"], + "missing": ["A003"], + "missing_tags": [], + "file_not_found": [], + }, + ) diff --git a/fs_import_image_advanced/views/import_product_image_view.xml b/fs_import_image_advanced/views/import_product_image_view.xml new file mode 100644 index 0000000000..8d6b2fa845 --- /dev/null +++ b/fs_import_image_advanced/views/import_product_image_view.xml @@ -0,0 +1,122 @@ + + + + product.import.image.form + storage.import.product_image + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +