From e314f055da0f3864c53f7653a6b75705403991a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Villarreal=20Ortega?= Date: Wed, 25 Aug 2021 15:57:07 +0200 Subject: [PATCH 01/29] [13.0][ADD] storage_import_image_advanced: Drop shopinvader dependencies + rename from shopinvader_import_image --- storage_import_image_advanced/README.rst | 77 ++++ storage_import_image_advanced/__init__.py | 1 + storage_import_image_advanced/__manifest__.py | 31 ++ .../data/ir_cron.xml | 15 + .../data/queue_job_channel_data.xml | 6 + .../data/queue_job_function_data.xml | 7 + .../i18n/shopinvader_import_image.pot | 266 +++++++++++ .../models/__init__.py | 1 + .../models/import_image.py | 369 +++++++++++++++ .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 2 + .../security/ir_model_access.xml | 13 + .../static/description/index.html | 423 ++++++++++++++++++ .../tests/__init__.py | 1 + .../tests/fixture/A001.jpg | Bin 0 -> 560 bytes .../tests/fixture/A002.jpg | Bin 0 -> 563 bytes .../tests/fixture/A003.jpg | Bin 0 -> 566 bytes .../tests/fixture/image_import_test.csv | 5 + .../tests/fixture/image_import_test.zip | Bin 0 -> 1413 bytes .../tests/test_import_image.py | 193 ++++++++ .../views/import_product_image_view.xml | 98 ++++ .../views/report_html.xml | 17 + 22 files changed, 1528 insertions(+) create mode 100644 storage_import_image_advanced/README.rst create mode 100644 storage_import_image_advanced/__init__.py create mode 100644 storage_import_image_advanced/__manifest__.py create mode 100644 storage_import_image_advanced/data/ir_cron.xml create mode 100644 storage_import_image_advanced/data/queue_job_channel_data.xml create mode 100644 storage_import_image_advanced/data/queue_job_function_data.xml create mode 100644 storage_import_image_advanced/i18n/shopinvader_import_image.pot create mode 100644 storage_import_image_advanced/models/__init__.py create mode 100644 storage_import_image_advanced/models/import_image.py create mode 100644 storage_import_image_advanced/readme/CONTRIBUTORS.rst create mode 100644 storage_import_image_advanced/readme/DESCRIPTION.rst create mode 100644 storage_import_image_advanced/security/ir_model_access.xml create mode 100644 storage_import_image_advanced/static/description/index.html create mode 100644 storage_import_image_advanced/tests/__init__.py create mode 100644 storage_import_image_advanced/tests/fixture/A001.jpg create mode 100644 storage_import_image_advanced/tests/fixture/A002.jpg create mode 100644 storage_import_image_advanced/tests/fixture/A003.jpg create mode 100644 storage_import_image_advanced/tests/fixture/image_import_test.csv create mode 100644 storage_import_image_advanced/tests/fixture/image_import_test.zip create mode 100644 storage_import_image_advanced/tests/test_import_image.py create mode 100644 storage_import_image_advanced/views/import_product_image_view.xml create mode 100644 storage_import_image_advanced/views/report_html.xml diff --git a/storage_import_image_advanced/README.rst b/storage_import_image_advanced/README.rst new file mode 100644 index 0000000000..1441a58eb1 --- /dev/null +++ b/storage_import_image_advanced/README.rst @@ -0,0 +1,77 @@ +================================ +Import Shopinvader product image +================================ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |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/13.0/storage_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-13-0/storage-13-0-storage_import_image_advanced + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/275/13.0 + :alt: Try me on Runbot + +|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 + +**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 smashing 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 + +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. diff --git a/storage_import_image_advanced/__init__.py b/storage_import_image_advanced/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/storage_import_image_advanced/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/storage_import_image_advanced/__manifest__.py b/storage_import_image_advanced/__manifest__.py new file mode 100644 index 0000000000..4b913fc343 --- /dev/null +++ b/storage_import_image_advanced/__manifest__.py @@ -0,0 +1,31 @@ +# 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": "13.0.1.0.0", + "summary": "Import product images", + "author": "Akretion, Camptocamp, Odoo Community Association (OCA)", + "company": "Akretion", + "maintainer": "Akretion", + "website": "https://github.com/OCA/storage", + "category": "Product", + "depends": ["storage_image_product", "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/storage_import_image_advanced/data/ir_cron.xml b/storage_import_image_advanced/data/ir_cron.xml new file mode 100644 index 0000000000..8c204ff442 --- /dev/null +++ b/storage_import_image_advanced/data/ir_cron.xml @@ -0,0 +1,15 @@ + + + + Storage image imports cleanup + + + 1 + days + -1 + + + code + model._cron_cleanup_obsolete() + + diff --git a/storage_import_image_advanced/data/queue_job_channel_data.xml b/storage_import_image_advanced/data/queue_job_channel_data.xml new file mode 100644 index 0000000000..d27ad6f172 --- /dev/null +++ b/storage_import_image_advanced/data/queue_job_channel_data.xml @@ -0,0 +1,6 @@ + + + import_image + + + diff --git a/storage_import_image_advanced/data/queue_job_function_data.xml b/storage_import_image_advanced/data/queue_job_function_data.xml new file mode 100644 index 0000000000..e6e4bb39ec --- /dev/null +++ b/storage_import_image_advanced/data/queue_job_function_data.xml @@ -0,0 +1,7 @@ + + + + do_import + + + diff --git a/storage_import_image_advanced/i18n/shopinvader_import_image.pot b/storage_import_image_advanced/i18n/shopinvader_import_image.pot new file mode 100644 index 0000000000..c514fa7a65 --- /dev/null +++ b/storage_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__storage_backend_id +msgid "Storage Backend" +msgstr "" + +#. module: shopinvader_import_image +#: model:ir.model.fields,field_description:shopinvader_import_image.field_shopinvader_import_product_image__source_storage_backend_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/storage_import_image_advanced/models/__init__.py b/storage_import_image_advanced/models/__init__.py new file mode 100644 index 0000000000..71872ae248 --- /dev/null +++ b/storage_import_image_advanced/models/__init__.py @@ -0,0 +1 @@ +from . import import_image diff --git a/storage_import_image_advanced/models/import_image.py b/storage_import_image_advanced/models/import_image.py new file mode 100644 index 0000000000..c9ecb97b50 --- /dev/null +++ b/storage_import_image_advanced/models/import_image.py @@ -0,0 +1,369 @@ +# 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.tools.pycompat import csv_reader + +_logger = logging.getLogger(__name__) + +try: + import magic + import validators +except (ImportError, IOError) 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" + + storage_backend_id = fields.Many2one( + "storage.backend", "Storage Backend", required=True + ) + product_model = fields.Selection( + [ + ("product.template", "Product template"), + ("product.product", "Product variants"), + ], + string="Product Model", + required=True, + ) + source_type = fields.Selection( + [ + ("url", "URL"), + ("zip_file", "Zip file"), + ("external_storage", "External storage"), + ], + string="Source type", + required=True, + default="url", + ) + file_csv = fields.Binary(string="CSV file", required=True) + csv_header = fields.Char( + string="CSV file header", + default="default_code,tag,path", + required=True, + ) + csv_delimiter = fields.Char( + string="CSV file delimiter", default=",", required=True + ) + source_zipfile = fields.Binary("ZIP with images", required=False) + source_storage_backend_id = fields.Many2one( + "storage.backend", "Storage Backend 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 = fields.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 = fields.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): + tmpl = self.env.ref("storage_import_image.report_html") + for record in self: + if not record.report: + record.report_html = "" + continue + report_html = tmpl.render({"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.encodestring(binary)} + return res + + def _read_from_url(self, file_path): + if validators.url(file_path): + return urlopen(file_path).read() + return None + + def _read_from_zip_file(self, file_path): + if not self.source_zipfile: + raise exceptions.UserError(_("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_storage_backend_id: + raise exceptions.UserError(_("No storage backend provided!")) + return self.source_storage_backend_id._get_bin_data(file_path) + + def _read_csv(self): + if self.file_csv: + return base64.b64decode(self.file_csv) + elif self.external_csv_path: + return self.source_storage_backend_id._get_bin_data( + self.external_csv_path + ) + + def _get_lines(self): + lines = [] + with closing(io.BytesIO(self._read_csv())) as file_csv: + reader = csv_reader(file_csv, delimiter=self.csv_delimiter) + headers = next(reader, None) + + if headers != self.csv_header.split(self.csv_delimiter): + raise exceptions.UserError( + _("Invalid CSV file headers found! Expected: %s") + % self.csv_header + ) + csv.field_size_limit(sys.maxsize) + + for row in reader: + if not row: + continue + default_code, tag_name, file_path = row + lines.append( + { + "default_code": default_code, + "tag_name": tag_name, + "file_path": file_path, + } + ) + 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 = "SELECT id FROM %s WHERE ID IN %%s FOR UPDATE" % self._table + 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 _do_import(self, lines, product_model, options=None): + tag_obj = self.env["image.tag"] + image_obj = self.env["storage.image"] + relation_obj = self.env["product.image.relation"] + prod_tmpl_attr_value_obj = self.env["product.template.attribute.value"] + + report = { + "created": set(), + "file_not_found": set(), + "missing": [], + "missing_tags": [], + } + options = options or {} + + # do all query at once + lines_by_code = {x["default_code"]: x for x in lines} + all_codes = list(lines_by_code.keys()) + _fields = ["default_code", "product_tmpl_id"] + if product_model == "product.template": + # exclude template id + _fields = _fields[:1] + else: + _fields.append("product_template_attribute_value_ids") + + products = self.env[product_model].search_read( + [("default_code", "in", all_codes)], _fields + ) + existing_by_code = {x["default_code"]: 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["default_code"]] + file_path = line["file_path"] + file_vals = self._prepare_file_values(file_path) + if not file_vals: + report["file_not_found"].add(prod["default_code"]) + continue + file_vals.update( + {"name": file_vals["name"], "alt_name": file_vals["name"]} + ) + # storage_file = file_obj.create(file_vals) + 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) + 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"] + ): + attr_values = prod_tmpl_attr_value_obj.browse( + prod["product_template_attribute_value_ids"] + ) + img_relation_values["attribute_value_ids"] = [ + ( + 6, + 0, + attr_values.mapped("product_attribute_value_id").ids, + ) + ] + relation_obj.create(img_relation_values) + report["created"].add(prod["default_code"]) + 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 = { + "data": file_data["b64"], + "name": name, + "file_type": filetype, + "mimetype": file_data["mimetype"], + "backend_id": self.storage_backend_id.id, + } + 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": _("Created"), + "file_not_found": _("Image file not found"), + "missing": _("Product not found"), + "missing_tags": _("Tags not found"), + } + return labels.get(key, key) diff --git a/storage_import_image_advanced/readme/CONTRIBUTORS.rst b/storage_import_image_advanced/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..f2b3e988bf --- /dev/null +++ b/storage_import_image_advanced/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Sylvain Calador +* Saritha +* Simone Orsi diff --git a/storage_import_image_advanced/readme/DESCRIPTION.rst b/storage_import_image_advanced/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..52bc10aa2e --- /dev/null +++ b/storage_import_image_advanced/readme/DESCRIPTION.rst @@ -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/storage_import_image_advanced/security/ir_model_access.xml b/storage_import_image_advanced/security/ir_model_access.xml new file mode 100644 index 0000000000..ccba9da685 --- /dev/null +++ b/storage_import_image_advanced/security/ir_model_access.xml @@ -0,0 +1,13 @@ + + + + storage_import_product_image user + + + + + + + + + diff --git a/storage_import_image_advanced/static/description/index.html b/storage_import_image_advanced/static/description/index.html new file mode 100644 index 0000000000..0785da897e --- /dev/null +++ b/storage_import_image_advanced/static/description/index.html @@ -0,0 +1,423 @@ + + + + + + +Import Shopinvader product image + + + +
+

Import Shopinvader product image

+ + +

Beta License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runbot

+

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

+

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 smashing 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/storage_import_image_advanced/tests/__init__.py b/storage_import_image_advanced/tests/__init__.py new file mode 100644 index 0000000000..069e16d855 --- /dev/null +++ b/storage_import_image_advanced/tests/__init__.py @@ -0,0 +1 @@ +from . import test_import_image diff --git a/storage_import_image_advanced/tests/fixture/A001.jpg b/storage_import_image_advanced/tests/fixture/A001.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7f0b01ed1388033b2b69434e170a5625bec30ba8 GIT binary patch literal 560 zcmex=LJ%Z3brs0d5{X z9xeesUOpi~0bvOZDG4z#2}5O7Sq&>=TN_Jb3v&mL7(WM>a94ASfZV|F_@wlVbUWX| z%7T=NnAG%SkRgnMLP8Rv68ch7`pHffPRS&L|A!a^IT%j%*bfZh))3% zTmeoi*zCbpNC*I}V@7r=&~u6lfsO^c!(NeNck~&U)4>V_LJ%Z3brs0d5{X z9xeesUOpi~0bvOZDG4z#2}5O7Sq&>=TN_Jb3v&mL7(WM>a94ASfZV|F_@wlVbUWX| z%7T=NnAG%SkRgnMLP8Rv68ch7`pHffPRS&L|A!a^IT%C)E8SC*VV1g+S6JTIQwgbp!G=$p1X3w++XoiFU0~4wmK}7~eN0?LW6${%= zp9J#2?gyJDAdkmtklO?Zxecs9925d59%K|WRD|0raGb00-WJ(udr$K@{J#kRx|VMT literal 0 HcmV?d00001 diff --git a/storage_import_image_advanced/tests/fixture/A003.jpg b/storage_import_image_advanced/tests/fixture/A003.jpg new file mode 100644 index 0000000000000000000000000000000000000000..798db4f5fb72195ec71f623ef027a2fbea3c4beb GIT binary patch literal 566 zcmex=LJ%Z3brs0d5{X z9xeesUOpi~0bvOZDG4z#2}5O7Sq&>=TN_Jb3v&mL7(WM>a94ASfZV|F_@wlVbUWX| z%7T=NnAG%SkRgnMLP8Rv68ch7`pHffPRS&L|A!a^IT%U0MFvNhTkI99wHAD?hk1a3 y8R#3c{b)n5>73YI@FPlI3pUalc z#+WB(C#T0~7iXvF$e75}hgx5?UnmD=6sKNk1{alPb;s+a*{mB_bpvW=gV3&R~(8 z$tw9@K2absA;p1pjY6PXNnqRm?GhC;B!QAMSS2JRvK_iuHfTO#$VyE~UB#{`aAaM> z%!wE03C?Fy+rcy^Y{SjwwG#vwHqD&)v~gZ=3`cSMUZvyjUKkrJZx-b`AzUz%L2-Jc zs7Iqtn$tGPl#L6MmF{FQPFGc&Brfvv>%HiI`#Bi`yxBR}PG4daWn^Fg1!w>~s81jT zwJ{>7jo?A80S#tQa1Z^U)^6xJ!Xj~TilGq))6_x+SINkk72le9gpQ;MuqzuYFSbp# zm1FfWU`=A;Ig`O9!MkwVhMUG~8MP(~BqTV6Dz7+k-~~r<*WRSW+b<^@d~fy@o}m7~ zjL|8NSzCa;m7z<@&C%I)$6lu6JEh*|PMo{#yUF~+|7U}Pzk1$lJxyTng8~^5{1=dd z-wYA_#_-^`7*@gW-O%O2dLh)1izyTm_!j?+8?c3bPsTM3BLk +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import base64 +import os + +import mock + +from odoo.addons.shopinvader_image.tests.common import TestShopinvaderImageCase + + +class TestShopinvaderImportImageCase(TestShopinvaderImageCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.storage_backend = cls.env.ref( + "storage_backend.default_storage_backend" + ) + 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 = { + "storage_backend_id": self.storage_backend.id, + "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["shopinvader.import.product_image"].create(vals) + + +class TestShopinvaderImportImage(TestShopinvaderImportImageCase): + 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_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.encodestring(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_storage_backend_id=self.storage_backend.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( + self.storage_backend.__class__, "_get_bin_data" + ) 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/storage_import_image_advanced/views/import_product_image_view.xml b/storage_import_image_advanced/views/import_product_image_view.xml new file mode 100644 index 0000000000..d819c2cbc6 --- /dev/null +++ b/storage_import_image_advanced/views/import_product_image_view.xml @@ -0,0 +1,98 @@ + + + + product.import.image.form + storage.import.product_image + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+ + + product.import.image.tree + storage.import.product_image + + + + + + + + + + + + + Import Product Image + ir.actions.act_window + storage.import.product_image + tree,form + + + + + +
diff --git a/storage_import_image_advanced/views/report_html.xml b/storage_import_image_advanced/views/report_html.xml new file mode 100644 index 0000000000..768511d63a --- /dev/null +++ b/storage_import_image_advanced/views/report_html.xml @@ -0,0 +1,17 @@ + + + + From f18b5b9459a63d8d3bbf0fda4bf8e5d3f6e0fa54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Villarreal=20Ortega?= Date: Thu, 26 Aug 2021 10:19:11 +0200 Subject: [PATCH 02/29] Adapt tests --- .../tests/test_import_image.py | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/storage_import_image_advanced/tests/test_import_image.py b/storage_import_image_advanced/tests/test_import_image.py index 25e413ea72..d909fa11fc 100644 --- a/storage_import_image_advanced/tests/test_import_image.py +++ b/storage_import_image_advanced/tests/test_import_image.py @@ -7,16 +7,14 @@ import mock -from odoo.addons.shopinvader_image.tests.common import TestShopinvaderImageCase +from odoo.addons.storage_image_product.tests.common import ProductImageCommonCase -class TestShopinvaderImportImageCase(TestShopinvaderImageCase): +class TestStorageImportImageCase(ProductImageCommonCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.storage_backend = cls.env.ref( - "storage_backend.default_storage_backend" - ) + cls.storage_backend = cls.env.ref("storage_backend.default_storage_backend") 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 @@ -40,10 +38,10 @@ def _get_wizard(self, **kw): "source_type": "zip_file", } vals.update(kw) - return self.env["shopinvader.import.product_image"].create(vals) + return self.env["storage.import.product_image"].create(vals) -class TestShopinvaderImportImage(TestShopinvaderImportImageCase): +class TestStorageImportImage(TestStorageImportImageCase): def test_get_lines(self): lines = self.wiz._get_lines() expected = [ @@ -53,13 +51,7 @@ def test_get_lines(self): "file_path": "A00%d.jpg" % x, } for x in range(1, 4) - ] + [ - { - "default_code": "A004", - "file_path": "A004-MISSING.jpg", - "tag_name": "", - } - ] + ] + [{"default_code": "A004", "file_path": "A004-MISSING.jpg", "tag_name": ""}] self.assertEqual(lines, expected) def test_read_from_zip(self): @@ -74,10 +66,7 @@ def test_get_b64(self): ) self.assertEqual( self.wiz._get_base64("A001.jpg"), - { - "mimetype": "image/jpeg", - "b64": base64.encodestring(img_content), - }, + {"mimetype": "image/jpeg", "b64": base64.encodestring(img_content)}, ) def test_import_errors(self): From d0a4067354a593bde7d34ce7f18afbcfb39a1cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Villarreal=20Ortega?= Date: Thu, 26 Aug 2021 10:25:37 +0200 Subject: [PATCH 03/29] [IMP] storage_import_image_advanced: black, isort, prettier --- storage_import_image_advanced/README.rst | 10 +- .../data/queue_job_function_data.xml | 5 +- .../models/import_image.py | 45 ++------ .../security/ir_model_access.xml | 14 +-- .../static/description/index.html | 8 +- .../views/import_product_image_view.xml | 100 +++++++++++------- .../views/report_html.xml | 2 +- 7 files changed, 91 insertions(+), 93 deletions(-) diff --git a/storage_import_image_advanced/README.rst b/storage_import_image_advanced/README.rst index 1441a58eb1..c3952648e2 100644 --- a/storage_import_image_advanced/README.rst +++ b/storage_import_image_advanced/README.rst @@ -1,6 +1,6 @@ -================================ -Import Shopinvader product image -================================ +============================ +Import Storage product image +============================ .. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! @@ -36,7 +36,7 @@ Idea based on an idea of the 'image_product_import' from Cybrosys Techno Solutio Bug Tracker =========== -Bugs are tracked on `GitHub Issues `_. +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 smashing it by providing a detailed and welcomed `feedback `_. @@ -74,4 +74,4 @@ promote its widespread use. This module is part of the `OCA/storage `_ project on GitHub. -You are welcome to contribute. +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/storage_import_image_advanced/data/queue_job_function_data.xml b/storage_import_image_advanced/data/queue_job_function_data.xml index e6e4bb39ec..6b647b5d58 100644 --- a/storage_import_image_advanced/data/queue_job_function_data.xml +++ b/storage_import_image_advanced/data/queue_job_function_data.xml @@ -1,5 +1,8 @@ - + do_import diff --git a/storage_import_image_advanced/models/import_image.py b/storage_import_image_advanced/models/import_image.py index c9ecb97b50..d0c0eb703d 100644 --- a/storage_import_image_advanced/models/import_image.py +++ b/storage_import_image_advanced/models/import_image.py @@ -74,13 +74,9 @@ class ProductImageImportWizard(models.Model): ) file_csv = fields.Binary(string="CSV file", required=True) csv_header = fields.Char( - string="CSV file header", - default="default_code,tag,path", - required=True, - ) - csv_delimiter = fields.Char( - string="CSV file delimiter", default=",", required=True + string="CSV file header", default="default_code,tag,path", required=True, ) + csv_delimiter = fields.Char(string="CSV file delimiter", default=",", required=True) source_zipfile = fields.Binary("ZIP with images", required=False) source_storage_backend_id = fields.Many2one( "storage.backend", "Storage Backend with images" @@ -155,9 +151,7 @@ def _read_csv(self): if self.file_csv: return base64.b64decode(self.file_csv) elif self.external_csv_path: - return self.source_storage_backend_id._get_bin_data( - self.external_csv_path - ) + return self.source_storage_backend_id._get_bin_data(self.external_csv_path) def _get_lines(self): lines = [] @@ -167,8 +161,7 @@ def _get_lines(self): if headers != self.csv_header.split(self.csv_delimiter): raise exceptions.UserError( - _("Invalid CSV file headers found! Expected: %s") - % self.csv_header + _("Invalid CSV file headers found! Expected: %s") % self.csv_header ) csv.field_size_limit(sys.maxsize) @@ -205,9 +198,7 @@ def action_import(self): 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() - ) + report = self._do_import(lines, self.product_model, options=self._get_options()) # Refresh report extendable_keys = [ "created", @@ -271,9 +262,7 @@ def _do_import(self, lines, product_model, options=None): 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 + tag_by_name[tag_name] = tag_obj.create({"name": tag_name}).id else: report["missing_tags"] = sorted(missing_tags) @@ -284,9 +273,7 @@ def _do_import(self, lines, product_model, options=None): if not file_vals: report["file_not_found"].add(prod["default_code"]) continue - file_vals.update( - {"name": file_vals["name"], "alt_name": file_vals["name"]} - ) + file_vals.update({"name": file_vals["name"], "alt_name": file_vals["name"]}) # storage_file = file_obj.create(file_vals) tag_id = tag_by_name.get(line["tag_name"]) @@ -319,11 +306,7 @@ def _do_import(self, lines, product_model, options=None): prod["product_template_attribute_value_ids"] ) img_relation_values["attribute_value_ids"] = [ - ( - 6, - 0, - attr_values.mapped("product_attribute_value_id").ids, - ) + (6, 0, attr_values.mapped("product_attribute_value_id").ids,) ] relation_obj.create(img_relation_values) report["created"].add(prod["default_code"]) @@ -347,17 +330,11 @@ def _prepare_file_values(self, file_path, filetype="image"): @api.model def _cron_cleanup_obsolete(self, days=7): - from_date = fields.Datetime.now().replace( - hour=23, minute=59, second=59 - ) + 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 = self.search([("state", "=", "done"), ("done_on", "<=", limit_date)]) records.unlink() - _logger.info( - "Cleanup obsolete images import. %d records found.", len(records) - ) + _logger.info("Cleanup obsolete images import. %d records found.", len(records)) def _report_label_for(self, key): labels = { diff --git a/storage_import_image_advanced/security/ir_model_access.xml b/storage_import_image_advanced/security/ir_model_access.xml index ccba9da685..dc20b1da9b 100644 --- a/storage_import_image_advanced/security/ir_model_access.xml +++ b/storage_import_image_advanced/security/ir_model_access.xml @@ -1,13 +1,13 @@ - + storage_import_product_image user - + - - - - - + + + + + diff --git a/storage_import_image_advanced/static/description/index.html b/storage_import_image_advanced/static/description/index.html index 0785da897e..3b9347bf35 100644 --- a/storage_import_image_advanced/static/description/index.html +++ b/storage_import_image_advanced/static/description/index.html @@ -4,7 +4,7 @@ -Import Shopinvader product image +Import Storage product image -
-

Import Shopinvader product image

+
+

Import Storage product image

-

Beta License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runbot

+

Beta License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runbot

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

Table of contents

@@ -387,7 +387,7 @@

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 smashing it by providing a detailed and welcomed -feedback.

+feedback.

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

@@ -415,7 +415,7 @@

Maintainers

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.

+

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/storage_import_image_advanced/tests/fixture/image_import_test_1.csv b/storage_import_image_advanced/tests/fixture/image_import_test_1.csv new file mode 100644 index 0000000000..8f020c02e8 --- /dev/null +++ b/storage_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/storage_import_image_advanced/tests/test_import_image.py b/storage_import_image_advanced/tests/test_import_image.py index d909fa11fc..1e5d32ea1e 100644 --- a/storage_import_image_advanced/tests/test_import_image.py +++ b/storage_import_image_advanced/tests/test_import_image.py @@ -7,6 +7,9 @@ import mock +from odoo.exceptions import UserError +from odoo.tools import mute_logger + from odoo.addons.storage_image_product.tests.common import ProductImageCommonCase @@ -54,6 +57,17 @@ def test_get_lines(self): ] + [{"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.storage_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 From 7d23b3d9989b6d4cc36e82ad3f3c3843199341dd Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 13 Dec 2021 10:51:34 +0000 Subject: [PATCH 16/29] storage_import_image_advanced 14.0.1.0.1 --- storage_import_image_advanced/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage_import_image_advanced/__manifest__.py b/storage_import_image_advanced/__manifest__.py index a99646b9e8..3f72da3319 100644 --- a/storage_import_image_advanced/__manifest__.py +++ b/storage_import_image_advanced/__manifest__.py @@ -6,7 +6,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { "name": "Import Storage product image", - "version": "14.0.1.0.0", + "version": "14.0.1.0.1", "summary": "Import product images using CSV", "author": "Akretion, Camptocamp, Odoo Community Association (OCA)", "company": "Akretion", From b472a1a53e1bf30a0a03c487e49a317bf8cd8062 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 23 Dec 2021 12:05:25 +0100 Subject: [PATCH 17/29] storage_import_image_advanced: fix report render --- storage_import_image_advanced/models/import_image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/storage_import_image_advanced/models/import_image.py b/storage_import_image_advanced/models/import_image.py index ff7fac5fb2..a150121198 100644 --- a/storage_import_image_advanced/models/import_image.py +++ b/storage_import_image_advanced/models/import_image.py @@ -135,12 +135,13 @@ def _default_csv_header(self): @api.depends("report") def _compute_report_html(self): + # TODO: add tests tmpl = self.env.ref("storage_import_image_advanced.report_html") for record in self: if not record.report: record.report_html = "" continue - report_html = tmpl.render({"record": record}) + report_html = tmpl._render({"record": record}) record.report_html = report_html @api.model From 1fe873af1063b83eb41d5cf41ae92a61a92db75c Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 23 Dec 2021 11:40:29 +0000 Subject: [PATCH 18/29] storage_import_image_advanced 14.0.1.0.2 --- storage_import_image_advanced/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage_import_image_advanced/__manifest__.py b/storage_import_image_advanced/__manifest__.py index 3f72da3319..8967e4f75c 100644 --- a/storage_import_image_advanced/__manifest__.py +++ b/storage_import_image_advanced/__manifest__.py @@ -6,7 +6,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { "name": "Import Storage product image", - "version": "14.0.1.0.1", + "version": "14.0.1.0.2", "summary": "Import product images using CSV", "author": "Akretion, Camptocamp, Odoo Community Association (OCA)", "company": "Akretion", From aa9bff4dae45a6b39eedb798af6d94e167175046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Villarreal=20Ortega?= Date: Tue, 21 Jun 2022 13:31:59 +0200 Subject: [PATCH 19/29] [IMP] storage_import_image_advanced: Add hook --- .../models/import_image.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/storage_import_image_advanced/models/import_image.py b/storage_import_image_advanced/models/import_image.py index a150121198..81fd882824 100644 --- a/storage_import_image_advanced/models/import_image.py +++ b/storage_import_image_advanced/models/import_image.py @@ -253,11 +253,16 @@ 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"] + ) + def _do_import(self, lines, product_model, options=None): tag_obj = self.env["image.tag"] image_obj = self.env["storage.image"] relation_obj = self.env["product.image.relation"] - prod_tmpl_attr_value_obj = self.env["product.template.attribute.value"] product_identifier_field = self._get_product_identifier_field() report = { "created": set(), @@ -332,14 +337,13 @@ def _do_import(self, lines, product_model, options=None): product_model == "product.product" and prod["product_template_attribute_value_ids"] ): - attr_values = prod_tmpl_attr_value_obj.browse( - prod["product_template_attribute_value_ids"] - ) img_relation_values["attribute_value_ids"] = [ ( 6, 0, - attr_values.mapped("product_attribute_value_id").ids, + self._assign_product_tmpl_attr_values(prod) + .mapped("product_attribute_value_id") + .ids, ) ] relation_obj.create(img_relation_values) From 3145b8d2a7dec53e78d8493bd8a59c2b48aefa4f Mon Sep 17 00:00:00 2001 From: Jasmin Solanki Date: Fri, 17 Jun 2022 14:26:08 +0530 Subject: [PATCH 20/29] [IMP] storage_import_image_advanced: black, isort, prettier --- storage_import_image_advanced/models/import_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage_import_image_advanced/models/import_image.py b/storage_import_image_advanced/models/import_image.py index 81fd882824..77fc76fc1c 100644 --- a/storage_import_image_advanced/models/import_image.py +++ b/storage_import_image_advanced/models/import_image.py @@ -287,7 +287,7 @@ def _do_import(self, lines, product_model, options=None): ) 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)] + 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"]] From ab3015055f3b9752e4f3dd9c7334968d02bfbe7b Mon Sep 17 00:00:00 2001 From: Jasmin Solanki Date: Fri, 17 Jun 2022 17:04:17 +0530 Subject: [PATCH 21/29] [MIG] storage_import_image_advanced: Migration to 15.0 --- storage_import_image_advanced/README.rst | 23 ++++++----- storage_import_image_advanced/__manifest__.py | 2 +- .../i18n/storage_import_image_advanced.pot | 5 +-- .../models/import_image.py | 7 +--- .../static/description/index.html | 38 ++++++++++--------- .../tests/test_import_image.py | 2 +- .../views/import_product_image_view.xml | 1 - 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/storage_import_image_advanced/README.rst b/storage_import_image_advanced/README.rst index 9b870b02df..2448e23b69 100644 --- a/storage_import_image_advanced/README.rst +++ b/storage_import_image_advanced/README.rst @@ -2,10 +2,13 @@ Import Storage product image ============================ -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:1d706ff34e4744344df7869c85d93483d4df078dd65b142064cbf75bc4f0e4eb + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status @@ -14,16 +17,16 @@ Import Storage product image :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/14.0/storage_import_image_advanced + :target: https://github.com/OCA/storage/tree/15.0/storage_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-14-0/storage-14-0-storage_import_image_advanced + :target: https://translation.odoo-community.org/projects/storage-15-0/storage-15-0-storage_import_image_advanced :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/275/14.0 - :alt: Try me on Runbot +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=15.0 + :alt: Try me on Runboat -|badge1| |badge2| |badge3| |badge4| |badge5| +|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 @@ -38,8 +41,8 @@ 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 smashing it by providing a detailed and welcomed -`feedback `_. +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. @@ -73,6 +76,6 @@ 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. +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/storage_import_image_advanced/__manifest__.py b/storage_import_image_advanced/__manifest__.py index 8967e4f75c..76d55aaea7 100644 --- a/storage_import_image_advanced/__manifest__.py +++ b/storage_import_image_advanced/__manifest__.py @@ -6,7 +6,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { "name": "Import Storage product image", - "version": "14.0.1.0.2", + "version": "15.0.1.0.0", "summary": "Import product images using CSV", "author": "Akretion, Camptocamp, Odoo Community Association (OCA)", "company": "Akretion", diff --git a/storage_import_image_advanced/i18n/storage_import_image_advanced.pot b/storage_import_image_advanced/i18n/storage_import_image_advanced.pot index d1cf2ecb7f..0830be5377 100644 --- a/storage_import_image_advanced/i18n/storage_import_image_advanced.pot +++ b/storage_import_image_advanced/i18n/storage_import_image_advanced.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 14.0\n" +"Project-Id-Version: Odoo Server 15.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: \n" "Language-Team: \n" @@ -158,7 +158,6 @@ msgstr "" #. module: storage_import_image_advanced #: model_terms:ir.ui.view,arch_db:storage_import_image_advanced.product_import_image_form_view -#: model_terms:ir.ui.view,arch_db:storage_import_image_advanced.product_import_image_tree_view msgid "Import storage product images" msgstr "" @@ -269,7 +268,7 @@ 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" +msgid "Source Type" msgstr "" #. module: storage_import_image_advanced diff --git a/storage_import_image_advanced/models/import_image.py b/storage_import_image_advanced/models/import_image.py index 77fc76fc1c..ca211f15d5 100644 --- a/storage_import_image_advanced/models/import_image.py +++ b/storage_import_image_advanced/models/import_image.py @@ -48,7 +48,6 @@ def gen_chunks(iterable, chunksize=10): class ProductImageImportWizard(models.Model): - _name = "storage.import.product_image" _description = "Handle import of storage product images" @@ -66,7 +65,6 @@ def _default_csv_header(self): ("product.template", "Product template"), ("product.product", "Product variants"), ], - string="Product Model", required=True, ) source_type = fields.Selection( @@ -75,7 +73,6 @@ def _default_csv_header(self): ("zip_file", "Zip file"), ("external_storage", "External storage"), ], - string="Source type", required=True, default="url", ) @@ -152,7 +149,7 @@ def _get_base64(self, file_path): binary = getattr(self, "_read_from_" + self.source_type)(file_path) if binary: mimetype = magic.from_buffer(binary, mime=True) - res = {"mimetype": mimetype, "b64": base64.encodestring(binary)} + res = {"mimetype": mimetype, "b64": base64.encodebytes(binary)} return res def _read_from_url(self, file_path): @@ -200,7 +197,7 @@ def _get_lines(self): line = {key: row[column] for key, column in mapping.items()} except KeyError as e: _logger.error(e) - raise exceptions.UserError(_("CSV Schema Incompatible")) + raise exceptions.UserError(_("CSV Schema Incompatible")) from e lines.append(line) return lines diff --git a/storage_import_image_advanced/static/description/index.html b/storage_import_image_advanced/static/description/index.html index c012826438..204ac0c4f5 100644 --- a/storage_import_image_advanced/static/description/index.html +++ b/storage_import_image_advanced/static/description/index.html @@ -1,20 +1,20 @@ - + - + Import Storage product image + + +
+

Import Storage product image thumbnail creation tool

+ + +

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

+

Hook to auto install thumbnail creation when importing massively images with tool

+
+

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

+
    +
  • ForgeFlow
  • +
+
+
+

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_thumbnail/tests/__init__.py b/fs_import_image_advanced_thumbnail/tests/__init__.py new file mode 100644 index 0000000000..d8bf9059cd --- /dev/null +++ b/fs_import_image_advanced_thumbnail/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fs_import_image_advanced_thumbnail diff --git a/fs_import_image_advanced_thumbnail/tests/test_fs_import_image_advanced_thumbnail.py b/fs_import_image_advanced_thumbnail/tests/test_fs_import_image_advanced_thumbnail.py new file mode 100644 index 0000000000..dabe3af740 --- /dev/null +++ b/fs_import_image_advanced_thumbnail/tests/test_fs_import_image_advanced_thumbnail.py @@ -0,0 +1,24 @@ +from odoo.addons.fs_import_image_advanced.tests.test_import_image import ( + TestStorageImportImage, +) + + +class TestStorageImportImageThumbnail(TestStorageImportImage): + def setUp(self): + super().setUp() + + def test_thumbnail_creation(self): + wiz = self._get_wizard() + wiz.do_import() + products_with_image_attachments = self.products.filtered( + lambda x: x.default_code in ["A001", "A002"] + ) + images = products_with_image_attachments.mapped("image_ids") + attachments = self.env["ir.attachment"] + for image in images: + attachments |= image.image_id.image.attachment + thumbnails = self.env["fs.thumbnail"] + for attachment in attachments: + for thumbnail in attachment.thumbnail_ids: + thumbnails |= thumbnail + self.assertEqual(len(thumbnails), len(attachments) * 2) From 4a1cfbe19fee57d2897e22d93e81cdad1fce4cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Duy=20=28=C4=90=E1=BB=97=20Anh=29?= Date: Thu, 17 Apr 2025 14:25:52 +0700 Subject: [PATCH 25/29] [IMP] fs_import_image_advanced: pre-commit auto fixes --- fs_import_image_advanced/README.rst | 28 +++++++++---------- fs_import_image_advanced/data/ir_cron.xml | 24 ++++++++-------- .../models/import_image.py | 2 +- fs_import_image_advanced/pyproject.toml | 3 ++ .../readme/CONTRIBUTORS.md | 4 +++ .../readme/CONTRIBUTORS.rst | 4 --- .../readme/DESCRIPTION.md | 2 ++ .../readme/DESCRIPTION.rst | 2 -- .../security/ir_model_access.xml | 20 ++++++------- .../static/description/index.html | 11 ++++---- .../tests/test_import_image.py | 2 +- .../views/import_product_image_view.xml | 4 +-- .../views/report_html.xml | 26 ++++++++--------- 13 files changed, 66 insertions(+), 66 deletions(-) create mode 100644 fs_import_image_advanced/pyproject.toml create mode 100644 fs_import_image_advanced/readme/CONTRIBUTORS.md delete mode 100644 fs_import_image_advanced/readme/CONTRIBUTORS.rst create mode 100644 fs_import_image_advanced/readme/DESCRIPTION.md delete mode 100644 fs_import_image_advanced/readme/DESCRIPTION.rst diff --git a/fs_import_image_advanced/README.rst b/fs_import_image_advanced/README.rst index 624c347471..b6fd58b0ba 100644 --- a/fs_import_image_advanced/README.rst +++ b/fs_import_image_advanced/README.rst @@ -17,19 +17,19 @@ Import Storage product image :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/16.0/fs_import_image_advanced + :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-16-0/storage-16-0-fs_import_image_advanced + :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=16.0 + :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 +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. @@ -47,7 +47,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -55,21 +55,21 @@ Credits ======= Authors -~~~~~~~ +------- * Akretion * Camptocamp Contributors -~~~~~~~~~~~~ +------------ -* Sylvain Calador -* Saritha -* Simone Orsi -* Héctor Villarreal +- Sylvain Calador +- Saritha +- Simone Orsi +- Héctor Villarreal Maintainers -~~~~~~~~~~~ +----------- This module is maintained by the OCA. @@ -81,6 +81,6 @@ 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. +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/data/ir_cron.xml b/fs_import_image_advanced/data/ir_cron.xml index 8c204ff442..5ee9695878 100644 --- a/fs_import_image_advanced/data/ir_cron.xml +++ b/fs_import_image_advanced/data/ir_cron.xml @@ -1,15 +1,15 @@ - - Storage image imports cleanup - - - 1 - days - -1 - - - code - model._cron_cleanup_obsolete() - + + Storage image imports cleanup + + + 1 + days + -1 + + + code + model._cron_cleanup_obsolete() + diff --git a/fs_import_image_advanced/models/import_image.py b/fs_import_image_advanced/models/import_image.py index 5f4f529f89..4701a2f4b1 100644 --- a/fs_import_image_advanced/models/import_image.py +++ b/fs_import_image_advanced/models/import_image.py @@ -26,7 +26,7 @@ try: import magic import validators -except (ImportError, IOError) as err: +except (OSError, ImportError) as err: _logger.debug(err) 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..7405b657ce --- /dev/null +++ b/fs_import_image_advanced/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- Sylvain Calador \<\> +- Saritha \<\> +- Simone Orsi \<\> +- Héctor Villarreal \<\> diff --git a/fs_import_image_advanced/readme/CONTRIBUTORS.rst b/fs_import_image_advanced/readme/CONTRIBUTORS.rst deleted file mode 100644 index d5200c3bfc..0000000000 --- a/fs_import_image_advanced/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1,4 +0,0 @@ -* Sylvain Calador -* Saritha -* Simone Orsi -* Héctor Villarreal 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/readme/DESCRIPTION.rst b/fs_import_image_advanced/readme/DESCRIPTION.rst deleted file mode 100644 index 52bc10aa2e..0000000000 --- a/fs_import_image_advanced/readme/DESCRIPTION.rst +++ /dev/null @@ -1,2 +0,0 @@ -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 index dc20b1da9b..a8e59e8653 100644 --- a/fs_import_image_advanced/security/ir_model_access.xml +++ b/fs_import_image_advanced/security/ir_model_access.xml @@ -1,13 +1,13 @@ - - storage_import_product_image user - - - - - - - - + + storage_import_product_image user + + + + + + + + diff --git a/fs_import_image_advanced/static/description/index.html b/fs_import_image_advanced/static/description/index.html index b215587558..30d23a22cc 100644 --- a/fs_import_image_advanced/static/description/index.html +++ b/fs_import_image_advanced/static/description/index.html @@ -1,4 +1,3 @@ - @@ -370,9 +369,9 @@

Import Storage product image

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:6b8c3532008323c4855704ee06cb4519b25bf2ff635698948a373b218cc331cf !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

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

+

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. @@ -396,7 +395,7 @@

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.

+feedback.

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

@@ -426,7 +425,7 @@

Maintainers

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.

+

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/test_import_image.py b/fs_import_image_advanced/tests/test_import_image.py index 119cebe9dc..80d3e74909 100644 --- a/fs_import_image_advanced/tests/test_import_image.py +++ b/fs_import_image_advanced/tests/test_import_image.py @@ -4,9 +4,9 @@ import base64 import os +from unittest import mock import fsspec -import mock from odoo.exceptions import UserError from odoo.tools import mute_logger diff --git a/fs_import_image_advanced/views/import_product_image_view.xml b/fs_import_image_advanced/views/import_product_image_view.xml index 223e4c1178..dade040995 100644 --- a/fs_import_image_advanced/views/import_product_image_view.xml +++ b/fs_import_image_advanced/views/import_product_image_view.xml @@ -27,8 +27,7 @@ class="oe_button_box" name="active_button_box" attrs="{'invisible': [('id','=',False)]}" - > - + /> @@ -128,5 +127,4 @@ action="import_image_action" sequence="999" /> -
diff --git a/fs_import_image_advanced/views/report_html.xml b/fs_import_image_advanced/views/report_html.xml index c97d39791e..8a72b74e99 100644 --- a/fs_import_image_advanced/views/report_html.xml +++ b/fs_import_image_advanced/views/report_html.xml @@ -1,17 +1,17 @@ - From 5c72ff60d62c42c3b142be0bde23ee44d16db192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Duy=20=28=C4=90=E1=BB=97=20Anh=29?= Date: Thu, 17 Apr 2025 14:26:13 +0700 Subject: [PATCH 26/29] [IMP] fs_import_image_advanced_thumbnail: pre-commit auto fixes --- fs_import_image_advanced_thumbnail/README.rst | 21 ++++++++++--------- .../pyproject.toml | 3 +++ .../readme/CONTRIBUTORS.md | 1 + .../readme/CONTRIBUTORS.rst | 1 - .../{DESCRIPTION.rst => DESCRIPTION.md} | 3 ++- .../static/description/index.html | 9 ++++---- 6 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 fs_import_image_advanced_thumbnail/pyproject.toml create mode 100644 fs_import_image_advanced_thumbnail/readme/CONTRIBUTORS.md delete mode 100644 fs_import_image_advanced_thumbnail/readme/CONTRIBUTORS.rst rename fs_import_image_advanced_thumbnail/readme/{DESCRIPTION.rst => DESCRIPTION.md} (78%) diff --git a/fs_import_image_advanced_thumbnail/README.rst b/fs_import_image_advanced_thumbnail/README.rst index 056eeb3a0f..45e56044f5 100644 --- a/fs_import_image_advanced_thumbnail/README.rst +++ b/fs_import_image_advanced_thumbnail/README.rst @@ -17,18 +17,19 @@ Import Storage product image thumbnail creation tool :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/16.0/fs_import_image_advanced_thumbnail + :target: https://github.com/OCA/storage/tree/18.0/fs_import_image_advanced_thumbnail :alt: OCA/storage .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/storage-16-0/storage-16-0-fs_import_image_advanced_thumbnail + :target: https://translation.odoo-community.org/projects/storage-18-0/storage-18-0-fs_import_image_advanced_thumbnail :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=16.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=18.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| -Hook to auto install thumbnail creation when importing massively images with tool +Hook to auto install thumbnail creation when importing massively images +with tool .. IMPORTANT:: This is an alpha version, the data model and design can change at any time without warning. @@ -46,7 +47,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -54,17 +55,17 @@ Credits ======= Authors -~~~~~~~ +------- * ForgeFlow Contributors -~~~~~~~~~~~~ +------------ -* Christopher Ormaza +- Christopher Ormaza Maintainers -~~~~~~~~~~~ +----------- This module is maintained by the OCA. @@ -76,6 +77,6 @@ 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. +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_thumbnail/pyproject.toml b/fs_import_image_advanced_thumbnail/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/fs_import_image_advanced_thumbnail/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fs_import_image_advanced_thumbnail/readme/CONTRIBUTORS.md b/fs_import_image_advanced_thumbnail/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..9b6d3655b7 --- /dev/null +++ b/fs_import_image_advanced_thumbnail/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Christopher Ormaza \<\> diff --git a/fs_import_image_advanced_thumbnail/readme/CONTRIBUTORS.rst b/fs_import_image_advanced_thumbnail/readme/CONTRIBUTORS.rst deleted file mode 100644 index b647a292e4..0000000000 --- a/fs_import_image_advanced_thumbnail/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1 +0,0 @@ -* Christopher Ormaza diff --git a/fs_import_image_advanced_thumbnail/readme/DESCRIPTION.rst b/fs_import_image_advanced_thumbnail/readme/DESCRIPTION.md similarity index 78% rename from fs_import_image_advanced_thumbnail/readme/DESCRIPTION.rst rename to fs_import_image_advanced_thumbnail/readme/DESCRIPTION.md index 86ac524b0f..0b5dbe958a 100644 --- a/fs_import_image_advanced_thumbnail/readme/DESCRIPTION.rst +++ b/fs_import_image_advanced_thumbnail/readme/DESCRIPTION.md @@ -1 +1,2 @@ -Hook to auto install thumbnail creation when importing massively images with tool +Hook to auto install thumbnail creation when importing massively images +with tool diff --git a/fs_import_image_advanced_thumbnail/static/description/index.html b/fs_import_image_advanced_thumbnail/static/description/index.html index 9123cb17b2..6d055a8409 100644 --- a/fs_import_image_advanced_thumbnail/static/description/index.html +++ b/fs_import_image_advanced_thumbnail/static/description/index.html @@ -369,8 +369,9 @@

Import Storage product image thumbnail creation tool

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:12c7348e725edb528078c94d16ac9f21b523ef9e01cbaa9c369585b41f3d7f13 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

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

-

Hook to auto install thumbnail creation when importing massively images with tool

+

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

+

Hook to auto install thumbnail creation when importing massively images +with tool

Important

This is an alpha version, the data model and design can change at any time without warning. @@ -394,7 +395,7 @@

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.

+feedback.

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

@@ -420,7 +421,7 @@

Maintainers

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.

+

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.

From a527d95efda85aab688be49e8732954534aef313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Duy=20=28=C4=90=E1=BB=97=20Anh=29?= Date: Fri, 18 Apr 2025 15:12:58 +0700 Subject: [PATCH 27/29] [MIG] fs_import_image_advanced: Migration to 18.0 --- fs_import_image_advanced/README.rst | 1 + fs_import_image_advanced/__manifest__.py | 2 +- fs_import_image_advanced/data/ir_cron.xml | 2 - .../models/import_image.py | 26 +++++++------ .../readme/CONTRIBUTORS.md | 1 + .../static/description/index.html | 1 + .../tests/test_import_image.py | 2 +- .../views/import_product_image_view.xml | 38 ++++++++----------- .../views/report_html.xml | 4 +- 9 files changed, 36 insertions(+), 41 deletions(-) diff --git a/fs_import_image_advanced/README.rst b/fs_import_image_advanced/README.rst index b6fd58b0ba..1cc2689a92 100644 --- a/fs_import_image_advanced/README.rst +++ b/fs_import_image_advanced/README.rst @@ -67,6 +67,7 @@ Contributors - Saritha - Simone Orsi - Héctor Villarreal +- Do Anh Duy Maintainers ----------- diff --git a/fs_import_image_advanced/__manifest__.py b/fs_import_image_advanced/__manifest__.py index 4eb774317d..47b40b993e 100644 --- a/fs_import_image_advanced/__manifest__.py +++ b/fs_import_image_advanced/__manifest__.py @@ -6,7 +6,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { "name": "Import Storage product image", - "version": "16.0.1.0.0", + "version": "18.0.1.0.0", "summary": "Import product images using CSV", "author": "Akretion, Camptocamp, Odoo Community Association (OCA)", "company": "Akretion", diff --git a/fs_import_image_advanced/data/ir_cron.xml b/fs_import_image_advanced/data/ir_cron.xml index 5ee9695878..ffe6c7c4bf 100644 --- a/fs_import_image_advanced/data/ir_cron.xml +++ b/fs_import_image_advanced/data/ir_cron.xml @@ -6,8 +6,6 @@ 1 days - -1 - code model._cron_cleanup_obsolete() diff --git a/fs_import_image_advanced/models/import_image.py b/fs_import_image_advanced/models/import_image.py index 4701a2f4b1..abf1617c1c 100644 --- a/fs_import_image_advanced/models/import_image.py +++ b/fs_import_image_advanced/models/import_image.py @@ -16,7 +16,7 @@ from urllib.request import urlopen from zipfile import ZipFile -from odoo import _, api, exceptions, fields, models +from odoo import api, exceptions, fields, models from odoo.tools import date_utils from odoo.addons.base_sparse_field.models.fields import Serialized @@ -99,7 +99,6 @@ def _default_csv_header(self): default="path", required=True, ) - csv_delimiter = fields.Char(string="CSV file delimiter", default=",", 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( @@ -128,12 +127,13 @@ def _default_csv_header(self): @api.depends("report") def _compute_report_html(self): # TODO: add tests - tmpl = self.env.ref("fs_import_image_advanced.report_html") for record in self: if not record.report: record.report_html = "" continue - report_html = tmpl._render({"record": record}) + report_html = self.env["ir.qweb"]._render( + "fs_import_image_advanced.report_html", {"record": record} + ) record.report_html = report_html @api.model @@ -154,7 +154,7 @@ def _read_from_url(self, file_path): def _read_from_zip_file(self, file_path): if not self.source_zipfile: - raise exceptions.UserError(_("No zip file provided!")) + 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: @@ -166,7 +166,7 @@ def _read_from_zip_file(self, file_path): def _read_from_external_storage(self, file_path): if not self.source_fs_storage_id: - raise exceptions.UserError(_("No storage backend provided!")) + raise exceptions.UserError(self.env._("No storage backend provided!")) return self.source_fs_storage_id._get_filesystem().open(file_path) def _read_csv(self): @@ -192,7 +192,9 @@ def _get_lines(self): line = {key: row[column] for key, column in mapping.items()} except KeyError as e: _logger.error(e) - raise exceptions.UserError(_("CSV Schema Incompatible")) from e + raise exceptions.UserError( + self.env._("CSV Schema Incompatible") + ) from e lines.append(line) return lines @@ -230,7 +232,7 @@ def do_import(self, lines=None, last_chunk=False): report[k] = sorted(set(prev_report[k] + v)) # Lock as writing can come from several jobs - sql = "SELECT id FROM %s WHERE ID IN %%s FOR UPDATE" % self._table + 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( { @@ -370,9 +372,9 @@ def _cron_cleanup_obsolete(self, days=7): def _report_label_for(self, key): labels = { - "created": _("Created"), - "file_not_found": _("Image file not found"), - "missing": _("Product not found"), - "missing_tags": _("Tags not found"), + "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/readme/CONTRIBUTORS.md b/fs_import_image_advanced/readme/CONTRIBUTORS.md index 7405b657ce..89cd99ee93 100644 --- a/fs_import_image_advanced/readme/CONTRIBUTORS.md +++ b/fs_import_image_advanced/readme/CONTRIBUTORS.md @@ -2,3 +2,4 @@ - Saritha \<\> - Simone Orsi \<\> - Héctor Villarreal \<\> +- Do Anh Duy \<\> diff --git a/fs_import_image_advanced/static/description/index.html b/fs_import_image_advanced/static/description/index.html index 30d23a22cc..cd3ce8ff3d 100644 --- a/fs_import_image_advanced/static/description/index.html +++ b/fs_import_image_advanced/static/description/index.html @@ -414,6 +414,7 @@

Contributors

  • Saritha <saritha@cybrosys.in>
  • Simone Orsi <simone.orsi@camptoamp.com>
  • Héctor Villarreal <hector.villarreal@forgeflow.com>
  • +
  • Do Anh Duy <duyda@trobz.com>
  • diff --git a/fs_import_image_advanced/tests/test_import_image.py b/fs_import_image_advanced/tests/test_import_image.py index 80d3e74909..e2f2a5dcc2 100644 --- a/fs_import_image_advanced/tests/test_import_image.py +++ b/fs_import_image_advanced/tests/test_import_image.py @@ -29,7 +29,7 @@ def _get_file_content(name, base_path=None, as_binary=False): @classmethod def setUpClass(cls): super().setUpClass() - cls.temp_storage = cls.env.ref("fs_storage.default_fs_storage") + 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 diff --git a/fs_import_image_advanced/views/import_product_image_view.xml b/fs_import_image_advanced/views/import_product_image_view.xml index dade040995..8d6b2fa845 100644 --- a/fs_import_image_advanced/views/import_product_image_view.xml +++ b/fs_import_image_advanced/views/import_product_image_view.xml @@ -11,14 +11,14 @@ string="Import images" name="action_import" type="object" - states="new" + invisible="state != 'new'" />