diff --git a/addons/web/tests/test_image.py b/addons/web/tests/test_image.py index 96e09444c05..e17073d0fb7 100644 --- a/addons/web/tests/test_image.py +++ b/addons/web/tests/test_image.py @@ -10,10 +10,35 @@ from odoo.tests.common import HttpCase class TestImage(HttpCase): def test_01_content_image_resize_placeholder(self): + """The goal of this test is to make sure the placeholder image is + resized appropriately depending on the given URL parameters.""" + + # CASE: resize placeholder to given size response = self.url_open('/web/image/0/200x150') image = Image.open(io.BytesIO(response.content)) self.assertEqual(image.size, (200, 150)) + # CASE: resize placeholder to small response = self.url_open('/web/image/fake/0/image_small') image = Image.open(io.BytesIO(response.content)) self.assertEqual(image.size, (64, 64)) + + # CASE: resize placeholder to medium + response = self.url_open('/web/image/fake/0/image_medium') + image = Image.open(io.BytesIO(response.content)) + self.assertEqual(image.size, (128, 128)) + + # CASE: resize placeholder to large + response = self.url_open('/web/image/fake/0/image_large') + image = Image.open(io.BytesIO(response.content)) + self.assertEqual(image.size, (256, 256)) + + # CASE: resize placeholder to big (but placeholder image is too small) + response = self.url_open('/web/image/fake/0/image') + image = Image.open(io.BytesIO(response.content)) + self.assertEqual(image.size, (256, 256)) + + # CASE: no size found, use placeholder original size + response = self.url_open('/web/image/fake/0/image_no_size') + image = Image.open(io.BytesIO(response.content)) + self.assertEqual(image.size, (256, 256)) diff --git a/addons/website_sale/tests/__init__.py b/addons/website_sale/tests/__init__.py index 89fc4f46d6f..483dd984704 100644 --- a/addons/website_sale/tests/__init__.py +++ b/addons/website_sale/tests/__init__.py @@ -2,4 +2,4 @@ from . import test_customize from . import test_sale_process from . import test_website_sale_pricelist from . import test_website_sale_product_attribute_value_config -from . import test_website_sale_zoom +from . import test_website_sale_image diff --git a/addons/website_sale/tests/test_website_sale_image.py b/addons/website_sale/tests/test_website_sale_image.py new file mode 100644 index 00000000000..bf47ecbfb73 --- /dev/null +++ b/addons/website_sale/tests/test_website_sale_image.py @@ -0,0 +1,206 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import io + +from PIL import Image + +import odoo.tests + + +@odoo.tests.common.tagged('post_install', '-at_install') +class TestWebsiteSaleImage(odoo.tests.HttpCase): + + # registry_test_mode = False # uncomment to save the product to test in browser + + def test_01_admin_shop_zoom_tour(self): + color_red = '#CD5C5C' + name_red = 'Indian Red' + + color_green = '#228B22' + name_green = 'Forest Green' + + color_blue = '#4169E1' + name_blue = 'Royal Blue' + + # create the color attribute + product_attribute = self.env['product.attribute'].create({ + 'name': 'Beautiful Color', + 'type': 'color', + }) + + # create the color attribute values + attr_values = self.env['product.attribute.value'].create([{ + 'name': name_red, + 'attribute_id': product_attribute.id, + 'html_color': color_red, + 'sequence': 1, + }, { + 'name': name_green, + 'attribute_id': product_attribute.id, + 'html_color': color_green, + 'sequence': 2, + }, { + 'name': name_blue, + 'attribute_id': product_attribute.id, + 'html_color': color_blue, + 'sequence': 3, + }]) + + # first image (blue) for the template + f = io.BytesIO() + Image.new('RGB', (1920, 1080), color_blue).save(f, 'PNG') + f.seek(0) + blue_image = base64.b64encode(f.read()) + + # second image (red) for the variant 1, small image (no zoom) + f = io.BytesIO() + Image.new('RGB', (800, 500), color_red).save(f, 'PNG') + f.seek(0) + red_image = base64.b64encode(f.read()) + + # second image (green) for the variant 2, big image (zoom) + f = io.BytesIO() + Image.new('RGB', (1920, 1080), color_green).save(f, 'PNG') + f.seek(0) + green_image = base64.b64encode(f.read()) + + # Template Extra Image 1 + f = io.BytesIO() + Image.new('RGB', (124, 147)).save(f, 'GIF') + f.seek(0) + image_gif = base64.b64encode(f.read()) + + # Template Extra Image 2 + image_svg = base64.b64encode(b'') + + # Red Variant Extra Image 1 + f = io.BytesIO() + Image.new('RGB', (767, 247)).save(f, 'BMP') + f.seek(0) + image_bmp = base64.b64encode(f.read()) + + # Green Variant Extra Image 1 + f = io.BytesIO() + Image.new('RGB', (2147, 3251)).save(f, 'JPEG') + f.seek(0) + image_jpeg = base64.b64encode(f.read()) + + # create the template, without creating the variants + template = self.env['product.template'].with_context(create_product_product=True).create({ + 'name': 'A Colorful Image', + 'product_template_image_ids': [(0, 0, {'name': 'image 1', 'image': image_gif}), (0, 0, {'name': 'image 4', 'image': image_svg})], + }) + + # set the color attribute and values on the template + self.env['product.template.attribute.line'].create([{ + 'attribute_id': product_attribute.id, + 'product_tmpl_id': template.id, + 'value_ids': [(6, 0, attr_values.ids)] + }]) + + # set a different price on the variants to differentiate them + product_template_attribute_values = self.env['product.template.attribute.value'].search([('product_tmpl_id', '=', template.id)]) + + for val in product_template_attribute_values: + if val.name == name_red: + val.price_extra = 10 + else: + val.price_extra = 20 + + # Create RED variant, and set image to blue (will be set on the template + # because the template image is empty and there is only one variant) + product_red = self.env['product.product'].create({ + 'product_tmpl_id': template.id, + 'image': blue_image, + 'attribute_value_ids': [(6, 0, attr_values.filtered(lambda l: l.name == name_red).ids)], + 'product_variant_image_ids': [(0, 0, {'name': 'image 2', 'image': image_bmp})], + }) + + self.assertEqual(template.image_original, blue_image) + + # create the green variant + product_green = self.env['product.product'].create({ + 'image': green_image, + 'product_tmpl_id': template.id, + 'attribute_value_ids': [(6, 0, attr_values.filtered(lambda l: l.name == name_green).ids)], + 'product_variant_image_ids': [(0, 0, {'name': 'image 3', 'image': image_jpeg})], + }) + + # now set the red image on the first variant, that works because + # template image is not empty anymore and we have a second variant + product_red.image = red_image + + # Verify image_original size > 1024 can be zoomed + self.assertTrue(template.can_image_be_zoomed) + self.assertFalse(template.product_template_image_ids[0].can_image_be_zoomed) + self.assertFalse(template.product_template_image_ids[1].can_image_be_zoomed) + self.assertFalse(product_red.can_image_be_zoomed) + self.assertFalse(product_red.product_variant_image_ids[0].can_image_be_zoomed) + self.assertTrue(product_green.can_image_be_zoomed) + self.assertTrue(product_green.product_variant_image_ids[0].can_image_be_zoomed) + + # Verify original size: keep original + image = Image.open(io.BytesIO(base64.b64decode(template.image_original))) + self.assertEqual(image.size, (1920, 1080)) + image = Image.open(io.BytesIO(base64.b64decode(product_red.image_original))) + self.assertEqual(image.size, (800, 500)) + image = Image.open(io.BytesIO(base64.b64decode(product_green.image_original))) + self.assertEqual(image.size, (1920, 1080)) + + # Verify big size: keep aspect ratio + image = Image.open(io.BytesIO(base64.b64decode(template.image))) + self.assertEqual(image.size, (1024, 576)) + image = Image.open(io.BytesIO(base64.b64decode(product_red.image))) + self.assertEqual(image.size, (800, 500)) + image = Image.open(io.BytesIO(base64.b64decode(product_green.image))) + self.assertEqual(image.size, (1024, 576)) + + # Verify large size + image = Image.open(io.BytesIO(base64.b64decode(template.image_large))) + self.assertEqual(image.size, (256, 256)) + image = Image.open(io.BytesIO(base64.b64decode(product_red.image_large))) + self.assertEqual(image.size, (256, 256)) + image = Image.open(io.BytesIO(base64.b64decode(product_green.image_large))) + self.assertEqual(image.size, (256, 256)) + + # Verify medium size + image = Image.open(io.BytesIO(base64.b64decode(template.image_medium))) + self.assertEqual(image.size, (128, 128)) + image = Image.open(io.BytesIO(base64.b64decode(product_red.image_medium))) + self.assertEqual(image.size, (128, 128)) + image = Image.open(io.BytesIO(base64.b64decode(product_green.image_medium))) + self.assertEqual(image.size, (128, 128)) + + # Verify small size + image = Image.open(io.BytesIO(base64.b64decode(template.image_small))) + self.assertEqual(image.size, (64, 64)) + image = Image.open(io.BytesIO(base64.b64decode(product_red.image_small))) + self.assertEqual(image.size, (64, 64)) + image = Image.open(io.BytesIO(base64.b64decode(product_green.image_small))) + self.assertEqual(image.size, (64, 64)) + + # self.env.cr.commit() # uncomment to save the product to test in browser + + self.browser_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('shop_zoom')", "odoo.__DEBUG__.services['web_tour.tour'].tours.shop_zoom.ready", login="admin") + + # CASE: unlink move image to fallback if fallback image empty + template.image = False + product_red.unlink() + self.assertEqual(template.image_original, red_image) + + # CASE: unlink does nothing special if fallback image already set + product_green.copy().unlink() + self.assertEqual(template.image_original, red_image) + + # CASE: display variant image first if set + self.assertEqual(product_green._get_images()[0].image_original, green_image) + + # CASE: display variant fallback after variant o2m, correct fallback + # write on the raw field, otherwise it will write on the fallback here + product_green.image_raw_original = False + images = product_green._get_images() + self.assertEqual(images[0].image_original, image_jpeg) + self.assertEqual(images[1].image_original, red_image) + self.assertEqual(images[2].image_original, image_gif) + self.assertEqual(images[3].image_original, image_svg) diff --git a/addons/website_sale/tests/test_website_sale_zoom.py b/addons/website_sale/tests/test_website_sale_zoom.py deleted file mode 100644 index d12a905af6d..00000000000 --- a/addons/website_sale/tests/test_website_sale_zoom.py +++ /dev/null @@ -1,105 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import base64 -import io - -from PIL import Image - -import odoo.tests - - -@odoo.tests.common.tagged('post_install', '-at_install') -class TestWebsiteSaleZoom(odoo.tests.HttpCase): - - # registry_test_mode = False # uncomment to save the product to test in browser - - def test_01_admin_shop_zoom_tour(self): - color_red = '#CD5C5C' - name_red = 'Indian Red' - - color_green = '#228B22' - name_green = 'Forest Green' - - color_blue = '#4169E1' - name_blue = 'Royal Blue' - - # create the color attribute - product_attribute = self.env['product.attribute'].create({ - 'name': 'Beautiful Color', - 'type': 'color', - }) - - # create the color attribute values - attr_values = self.env['product.attribute.value'].create([{ - 'name': name_red, - 'attribute_id': product_attribute.id, - 'html_color': color_red, - 'sequence': 1, - }, { - 'name': name_green, - 'attribute_id': product_attribute.id, - 'html_color': color_green, - 'sequence': 2, - }, { - 'name': name_blue, - 'attribute_id': product_attribute.id, - 'html_color': color_blue, - 'sequence': 3, - }]) - - # first image (blue) for the template - f = io.BytesIO() - Image.new('RGB', (1920, 1080), color_blue).save(f, 'PNG') - f.seek(0) - blue_image = base64.b64encode(f.read()) - - # second image (red) for the variant 1, small image (no zoom) - f = io.BytesIO() - Image.new('RGB', (800, 500), color_red).save(f, 'PNG') - f.seek(0) - red_image = base64.b64encode(f.read()) - - # second image (green) for the variant 2, big image (zoom) - f = io.BytesIO() - Image.new('RGB', (1920, 1080), color_green).save(f, 'PNG') - f.seek(0) - green_image = base64.b64encode(f.read()) - - # create the first product and the template. - # Set image to blue (will be set on the template because it was empty) - product_red = self.env['product.product'].create({ - 'name': 'A Colorful Image', - 'image': blue_image, - 'attribute_value_ids': [(6, 0, attr_values.filtered(lambda l: l.name == name_red).ids)], - }) - - # now set the red image on the first variant - # that works because template image is not empty anymore - product_red.image = red_image - - # set the color attribute and values on the template - self.env['product.template.attribute.line'].create([{ - 'attribute_id': product_attribute.id, - 'product_tmpl_id': product_red.product_tmpl_id.id, - 'value_ids': [(6, 0, attr_values.ids)] - }]) - - # create the green variant - self.env['product.product'].create({ - 'image': green_image, - 'product_tmpl_id': product_red.product_tmpl_id.id, - 'attribute_value_ids': [(6, 0, attr_values.filtered(lambda l: l.name == name_green).ids)], - }) - - # set a different price on the variants to differentiate them - product_template_attribute_values = self.env['product.template.attribute.value'].search([('product_tmpl_id', '=', product_red.product_tmpl_id.id)]) - - for val in product_template_attribute_values: - if val.name == name_red: - val.price_extra = 10 - else: - val.price_extra = 20 - - # self.env.cr.commit() # uncomment to save the product to test in browser - - self.phantom_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('shop_zoom')", "odoo.__DEBUG__.services['web_tour.tour'].tours.shop_zoom.ready", login="admin") diff --git a/odoo/addons/base/models/image_mixin.py b/odoo/addons/base/models/image_mixin.py index dc65f31d1ae..1e55cea38ed 100644 --- a/odoo/addons/base/models/image_mixin.py +++ b/odoo/addons/base/models/image_mixin.py @@ -73,6 +73,10 @@ class ImageMixin(models.AbstractModel): def _set_image(self): for record in self: record.image_original = record.image + # We want the image field to be recomputed to have a correct size. + # Without this `invalidate_cache`, the image field will keep holding the + # image_original instead of the big-sized image. + self.invalidate_cache() @api.multi def _compute_image_large(self):