[IMP] base,product,website_sale,*: improve product images
* = account, purchase, website_sale_comparison, website_sale_wishlist Raw Image & Mixin ================= Previously, the main image from a product was stored resized. This caused inconsistencies with the size of the images on ecommerce: the main image was resized, but the extra images were not. Now, we always store all raw images, thanks to the new mixin. This way, product images that were created before the installation of ecommerce will be ready to be used in ecommerce without any other change. We also store the resized versions instead of computing them on the fly: this will take more disk space, but significantly increase the speed of the pages when displaying images, as well as reduce server CPU load. Images on variants ================== Previously, it was only possible to set the extra product images on a template, and not on a variant. Now, we allow extra images also on the variants, this gives more flexibility to the user. Both extra images on template and variant have been given a sequence field to be able to sort them easily. On /shop we always display the image of the template for performance reasons. On /product we never display the image of the template, unless the variant has no image, then the template image is the fallback. Carousel ======== The carousel view has been cleaned and improved: - remove the duplicate XML that was created with the product configurator fix - use a single loop from a single source to get all the images, instead of merging the different sources in the view with complex conditions - improve the HTML structure, reduce the CSS with appropriate classes Preview Image ============= Both in website and backend: make better use of `preview_image` to display a small image whenever possible, to reduce download size for the end user. task-34045 PR: #30656 Co-authored-by: Parth Gajjar <pga@odoo.com> Co-authored-by: Sébastien Theys <seb@odoo.com>
This commit is contained in:
committed by
Sébastien Theys
co-authored by
Parth Gajjar
Sébastien Theys
parent
413c63556f
commit
7daa85dd12
@@ -1693,7 +1693,6 @@ class AccountInvoiceLine(models.Model):
|
||||
ondelete='set null', index=True, oldname='uos_id')
|
||||
product_id = fields.Many2one('product.product', string='Product',
|
||||
ondelete='restrict', index=True)
|
||||
product_image = fields.Binary('Product Image', related="product_id.image", store=False, readonly=True)
|
||||
account_id = fields.Many2one('account.account', string='Account', domain=[('deprecated', '=', False)],
|
||||
default=_default_account,
|
||||
help="The income or expense account related to the selected product.")
|
||||
|
||||
@@ -83,7 +83,7 @@ class ProductProduct(models.Model):
|
||||
_name = "product.product"
|
||||
_description = "Product"
|
||||
_inherits = {'product.template': 'product_tmpl_id'}
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin', 'image.mixin']
|
||||
_order = 'default_code, name, id'
|
||||
|
||||
# price: total price, context dependent (partner, pricelist, quantity)
|
||||
@@ -118,20 +118,6 @@ class ProductProduct(models.Model):
|
||||
'product.attribute.value', string='Attribute Values', ondelete='restrict')
|
||||
product_template_attribute_value_ids = fields.Many2many(
|
||||
'product.template.attribute.value', string='Template Attribute Values', compute="_compute_product_template_attribute_value_ids")
|
||||
# image: all image fields are base64 encoded and PIL-supported
|
||||
image_variant = fields.Binary(
|
||||
"Variant Image",
|
||||
help="This field holds the image used as image for the product variant, limited to 1024x1024px.")
|
||||
image = fields.Binary(
|
||||
"Big-sized image", compute='_compute_images', inverse='_set_image',
|
||||
help="Image of the product variant (Big-sized image of product template if false). It is automatically "
|
||||
"resized as a 1024x1024px image, with aspect ratio preserved.")
|
||||
image_small = fields.Binary(
|
||||
"Small-sized image", compute='_compute_images', inverse='_set_image_small',
|
||||
help="Image of the product variant (Small-sized image of product template if false).")
|
||||
image_medium = fields.Binary(
|
||||
"Medium-sized image", compute='_compute_images', inverse='_set_image_medium',
|
||||
help="Image of the product variant (Medium-sized image of product template if false).")
|
||||
is_product_variant = fields.Boolean(compute='_compute_is_product_variant')
|
||||
|
||||
standard_price = fields.Float(
|
||||
@@ -241,47 +227,6 @@ class ProductProduct(models.Model):
|
||||
else:
|
||||
self.partner_ref = self.display_name
|
||||
|
||||
@api.one
|
||||
@api.depends('image_variant', 'product_tmpl_id.image')
|
||||
def _compute_images(self):
|
||||
if self._context.get('bin_size'):
|
||||
self.image_medium = self.image_variant
|
||||
self.image_small = self.image_variant
|
||||
self.image = self.image_variant
|
||||
else:
|
||||
resized_images = tools.image_get_resized_images(self.image_variant, return_big=True, avoid_resize_medium=True)
|
||||
self.image_medium = resized_images['image_medium']
|
||||
self.image_small = resized_images['image_small']
|
||||
self.image = resized_images['image']
|
||||
if not self.image_medium:
|
||||
self.image_medium = self.product_tmpl_id.image_medium
|
||||
if not self.image_small:
|
||||
self.image_small = self.product_tmpl_id.image_small
|
||||
if not self.image:
|
||||
self.image = self.product_tmpl_id.image
|
||||
|
||||
@api.one
|
||||
def _set_image(self):
|
||||
self._set_image_value(self.image)
|
||||
|
||||
@api.one
|
||||
def _set_image_medium(self):
|
||||
self._set_image_value(self.image_medium)
|
||||
|
||||
@api.one
|
||||
def _set_image_small(self):
|
||||
self._set_image_value(self.image_small)
|
||||
|
||||
@api.one
|
||||
def _set_image_value(self, value):
|
||||
if isinstance(value, str):
|
||||
value = value.encode('ascii')
|
||||
image = tools.image_resize_image_big(value)
|
||||
if self.product_tmpl_id.image:
|
||||
self.image_variant = image
|
||||
else:
|
||||
self.product_tmpl_id.image = image
|
||||
|
||||
@api.depends('product_tmpl_id', 'attribute_value_ids')
|
||||
def _compute_product_template_attribute_value_ids(self):
|
||||
# Fetch and pre-map the values first for performance. It assumes there
|
||||
@@ -306,6 +251,26 @@ class ProductProduct(models.Model):
|
||||
else:
|
||||
product.product_template_attribute_value_ids += values_per_template[product.product_tmpl_id.id][pav.id]
|
||||
|
||||
@api.multi
|
||||
def _get_image_fallback_record(self):
|
||||
"""Override to get the image from the template if no image is set on the
|
||||
variant."""
|
||||
return self.product_tmpl_id
|
||||
|
||||
@api.multi
|
||||
def _force_write_image_on_fallback(self):
|
||||
"""Override to always write on the template if there is only one
|
||||
variant.
|
||||
|
||||
This is needed because when there is only one variant, the user doesn't
|
||||
know there is a difference between template and variant, he expects both
|
||||
images to be the same.
|
||||
"""
|
||||
return self.env['product.product'].search_count([
|
||||
('product_tmpl_id', '=', self.product_tmpl_id.id),
|
||||
('active', '=', True),
|
||||
]) <= 1
|
||||
|
||||
@api.one
|
||||
def _get_pricelist_items(self):
|
||||
self.pricelist_item_ids = self.env['product.pricelist.item'].search([
|
||||
|
||||
@@ -12,7 +12,7 @@ from odoo.osv import expression
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_name = "product.template"
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin', 'image.mixin']
|
||||
_description = "Product Template"
|
||||
_order = "name"
|
||||
|
||||
@@ -134,20 +134,6 @@ class ProductTemplate(models.Model):
|
||||
|
||||
item_ids = fields.One2many('product.pricelist.item', 'product_tmpl_id', 'Pricelist Items')
|
||||
|
||||
# image: all image fields are base64 encoded and PIL-supported
|
||||
image = fields.Binary(
|
||||
"Image", help="This field holds the image used as image for the product, limited to 1024x1024px.")
|
||||
image_medium = fields.Binary(
|
||||
"Medium-sized image",
|
||||
help="Medium-sized image of the product. It is automatically "
|
||||
"resized as a 128x128px image, with aspect ratio preserved, "
|
||||
"only when the image exceeds one of those sizes. Use this field in form views or some kanban views.")
|
||||
image_small = fields.Binary(
|
||||
"Small-sized image",
|
||||
help="Small-sized image of the product. It is automatically "
|
||||
"resized as a 64x64px image, with aspect ratio preserved. "
|
||||
"Use this field anywhere a small image is required.")
|
||||
|
||||
@api.depends('product_variant_ids')
|
||||
def _compute_product_variant_id(self):
|
||||
for p in self:
|
||||
@@ -328,9 +314,6 @@ class ProductTemplate(models.Model):
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
''' Store the initial standard price in order to be able to retrieve the cost of a product template for a given date'''
|
||||
# TDE FIXME: context brol
|
||||
for vals in vals_list:
|
||||
tools.image_resize_images(vals)
|
||||
templates = super(ProductTemplate, self).create(vals_list)
|
||||
if "create_product_product" not in self._context:
|
||||
templates.with_context(create_from_tmpl=True).create_variant_ids()
|
||||
@@ -355,7 +338,6 @@ class ProductTemplate(models.Model):
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
tools.image_resize_images(vals)
|
||||
res = super(ProductTemplate, self).write(vals)
|
||||
if 'attribute_line_ids' in vals or vals.get('active'):
|
||||
self.create_variant_ids()
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
options='{"terminology": "archive"}'/>
|
||||
</button>
|
||||
</div>
|
||||
<field name="image_medium" widget="image" class="oe_avatar"/>
|
||||
<field name="image" widget="image" class="oe_avatar" options="{'preview_image': 'image_medium'}"/>
|
||||
<div class="oe_title">
|
||||
<label class="oe_edit_only" for="name" string="Product Name"/>
|
||||
<h1><field name="name" placeholder="Product Name"/></h1>
|
||||
@@ -275,7 +275,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<field name="id" invisible="1"/>
|
||||
<field name="image_medium" widget="image" class="oe_avatar"/>
|
||||
<field name="image" widget="image" class="oe_avatar" options="{'preview_image': 'image_medium'}"/>
|
||||
<div class="oe_title">
|
||||
<label class="oe_edit_only" for="name" string="Product Name"/>
|
||||
<h1><field name="name" readonly="1" placeholder="e.g. Odoo Enterprise Subscription"/></h1>
|
||||
|
||||
@@ -434,9 +434,6 @@ class PurchaseOrderLine(models.Model):
|
||||
taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
|
||||
product_uom = fields.Many2one('uom.uom', string='Product Unit of Measure', required=True)
|
||||
product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True, required=True)
|
||||
product_image = fields.Binary(
|
||||
'Product Image', related="product_id.image", readonly=False,
|
||||
help="Non-stored related field to allow portal user to see the image of the product he has ordered")
|
||||
product_type = fields.Selection(related='product_id.type', readonly=True)
|
||||
price_unit = fields.Float(string='Unit Price', required=True, digits=dp.get_precision('Product Price'))
|
||||
|
||||
|
||||
@@ -1114,9 +1114,6 @@ class SaleOrderLine(models.Model):
|
||||
# because the result might be different then.
|
||||
product_no_variant_attribute_value_ids = fields.Many2many('product.template.attribute.value', string='Product attribute values that do not create variants')
|
||||
|
||||
# Non-stored related field to allow portal user to see the image of the product he has ordered
|
||||
product_image = fields.Binary('Product Image', related="product_id.image", store=False, readonly=False)
|
||||
|
||||
qty_delivered_method = fields.Selection([
|
||||
('manual', 'Manual'),
|
||||
('analytic', 'Analytic From Expenses')
|
||||
|
||||
@@ -1220,10 +1220,10 @@ class WebsiteSale(ProductConfiguratorController):
|
||||
"""
|
||||
kw.pop('pricelist_id')
|
||||
res = self.get_combination_info(product_template_id, product_id, combination, add_qty, request.website.get_current_pricelist(), **kw)
|
||||
|
||||
if request.env.ref('website_sale.shop_product_carousel', raise_if_not_found=False): # IF for compatibility 12.0
|
||||
res.update(carousel=request.env['ir.ui.view'].render_template('website_sale.shop_product_carousel', values={
|
||||
carousel_view = request.env['ir.ui.view'].render_template('website_sale.shop_product_carousel',
|
||||
values={
|
||||
'product': request.env['product.template'].browse(res['product_template_id']),
|
||||
'product_variant': request.env['product.product'].browse(res['product_id']),
|
||||
}))
|
||||
})
|
||||
res['carousel'] = carousel_view
|
||||
return res
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td style="padding: 10px; width:150px;">
|
||||
<img src="/web/image/product.product/${line.product_id.id}/image" height="100px" width="100px" alt="Product image"></img>
|
||||
<img src="/web/image/product.product/${line.product_id.id}/image_medium" height="100px" width="100px" alt="Product image"></img>
|
||||
</td>
|
||||
<td>
|
||||
<strong>${line.product_id.display_name}</strong><br/>${line.name}
|
||||
|
||||
@@ -5,6 +5,7 @@ from . import account_invoice
|
||||
from . import crm_team
|
||||
from . import ir_http
|
||||
from . import mail_compose_message
|
||||
from . import product_image
|
||||
from . import product
|
||||
from . import res_country
|
||||
from . import res_partner
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.addons import decimal_precision as dp
|
||||
from odoo.tools.translate import html_translate
|
||||
@@ -124,7 +125,8 @@ class ProductTemplate(models.Model):
|
||||
public_categ_ids = fields.Many2many('product.public.category', string='Website Product Category',
|
||||
help="The product will be available in each mentioned e-commerce category. Go to"
|
||||
"Shop > Customize and enable 'E-commerce categories' to view all e-commerce categories.")
|
||||
product_image_ids = fields.One2many('product.image', 'product_tmpl_id', string='Images')
|
||||
|
||||
product_template_image_ids = fields.One2many('product.image', 'product_tmpl_id', string="Extra Product Images", copy=True)
|
||||
|
||||
# website_price deprecated, directly use _get_combination_info instead
|
||||
website_price = fields.Float('Website price', compute='_website_price', digits=dp.get_precision('Product Price'))
|
||||
@@ -346,12 +348,28 @@ class ProductTemplate(models.Model):
|
||||
domain = super(ProductTemplate, self)._rating_domain()
|
||||
return expression.AND([domain, [('website_published', '=', True)]])
|
||||
|
||||
@api.multi
|
||||
def _get_images(self):
|
||||
"""Return a list of records implementing `image.mixin` to
|
||||
display on the carousel on the website for this template.
|
||||
|
||||
This returns a list and not a recordset because the records might be
|
||||
from different models (template and image).
|
||||
|
||||
It contains in this order: the main image of the template and the
|
||||
Template Extra Images.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return [self] + list(self.product_template_image_ids)
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
_inherit = "product.product"
|
||||
|
||||
website_id = fields.Many2one(related='product_tmpl_id.website_id', readonly=False)
|
||||
|
||||
product_variant_image_ids = fields.One2many('product.image', 'product_variant_id', string="Extra Variant Images")
|
||||
|
||||
# website_price deprecated, directly use _get_combination_info instead
|
||||
website_price = fields.Float('Website price', compute='_website_price', digits=dp.get_precision('Product Price'))
|
||||
# website_public_price deprecated, directly use _get_combination_info instead
|
||||
@@ -371,11 +389,27 @@ class Product(models.Model):
|
||||
self.ensure_one()
|
||||
return self.product_tmpl_id.website_publish_button()
|
||||
|
||||
@api.multi
|
||||
def _get_images(self):
|
||||
"""Return a list of records implementing `image.mixin` to
|
||||
display on the carousel on the website for this variant.
|
||||
|
||||
class ProductImage(models.Model):
|
||||
_name = 'product.image'
|
||||
_description = 'Product Image'
|
||||
This returns a list and not a recordset because the records might be
|
||||
from different models (template, variant and image).
|
||||
|
||||
name = fields.Char('Name')
|
||||
image = fields.Binary('Image', attachment=True)
|
||||
product_tmpl_id = fields.Many2one('product.template', 'Related Product', copy=True)
|
||||
It contains in this order: the main image of the variant (if set), the
|
||||
Variant Extra Images, and the Template Extra Images.
|
||||
"""
|
||||
self.ensure_one()
|
||||
variant_images = list(self.product_variant_image_ids)
|
||||
if self.image_raw_original:
|
||||
# if the main variant image is set, display it first
|
||||
variant_images = [self] + variant_images
|
||||
else:
|
||||
# If the main variant image is empty, it will fallback to template
|
||||
# image, in this case insert it after the other variant images, so
|
||||
# that all variant images are first and all template images last.
|
||||
variant_images = variant_images + [self]
|
||||
# [1:] to remove the main image from the template, we only display
|
||||
# the template extra images here
|
||||
return variant_images + self.product_tmpl_id._get_images()[1:]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class ProductImage(models.Model):
|
||||
_name = 'product.image'
|
||||
_description = "Product Image"
|
||||
_inherit = ['image.mixin']
|
||||
_order = 'sequence, id'
|
||||
|
||||
name = fields.Char("Name", required=True)
|
||||
sequence = fields.Integer(default=10, index=True)
|
||||
|
||||
image_original = fields.Binary(required=True)
|
||||
|
||||
product_tmpl_id = fields.Many2one('product.template', "Product Template", index=True, ondelete='cascade')
|
||||
product_variant_id = fields.Many2one('product.product', "Product Variant", index=True, ondelete='cascade')
|
||||
@@ -209,6 +209,24 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend(ProductConfiguratorM
|
||||
}
|
||||
return ProductConfiguratorMixin.getSelectedVariantValues.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Write the properties of the form elements in the DOM to prevent the
|
||||
* current selection from being lost when activating the web editor.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
onChangeVariant: function (ev) {
|
||||
var $component = $(ev.currentTarget).closest('.js_product');
|
||||
$component.find('input').each(function () {
|
||||
var $el = $(this);
|
||||
$el.attr('checked', $el.is(':checked'));
|
||||
});
|
||||
$component.find('select option').each(function () {
|
||||
var $el = $(this);
|
||||
$el.attr('selected', $el.is(':selected'));
|
||||
});
|
||||
return ProductConfiguratorMixin.onChangeVariant.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
@@ -345,21 +363,12 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend(ProductConfiguratorM
|
||||
// Do not activate image zoom for mobile devices, since it might prevent users from scrolling the page
|
||||
if (!config.device.isMobile) {
|
||||
var autoZoom = $('.ecom-zoomable').data('ecom-zoom-auto') || false,
|
||||
factorZoom = parseFloat($('.ecom-zoomable').data('ecom-zoom-factor')) || 1.5,
|
||||
attach = '#o-carousel-product';
|
||||
_.each($('.ecom-zoomable img[data-zoom]'), function (el) {
|
||||
onImageLoaded(el, function () {
|
||||
var $img = $(el);
|
||||
if (!_.str.endsWith(el.src, el.dataset.zoomImage) || // if zoom-img != img
|
||||
el.naturalWidth >= $(attach).width() * factorZoom || el.naturalHeight >= $(attach).height() * factorZoom) {
|
||||
$img.zoomOdoo({event: autoZoom ? 'mouseenter' : 'click', attach: attach});
|
||||
$img.attr('data-zoom', 1); // add cursor (if previously removed)
|
||||
} else {
|
||||
$img.removeAttr('data-zoom'); // remove cursor
|
||||
// remove zooming but keep the attribute because
|
||||
// it can potentially be set back
|
||||
$img.attr('data-zoom-image', '');
|
||||
}
|
||||
$img.zoomOdoo({event: autoZoom ? 'mouseenter' : 'click', attach: attach});
|
||||
$img.attr('data-zoom', 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ odoo.define("website_sale.tour_shop_custom_attribute_value", function (require)
|
||||
var base = require("web_editor.base");
|
||||
|
||||
tour.register("shop_custom_attribute_value", {
|
||||
url: "/shop",
|
||||
url: "/shop?search=Customizable Desk",
|
||||
test: true,
|
||||
wait_for: base.ready()
|
||||
}, [{
|
||||
trigger: 'img[src*="/product.product/10"]',
|
||||
run: 'click'
|
||||
content: "click on Customizable Desk",
|
||||
trigger: '.oe_product_cart a:contains("Customizable Desk")',
|
||||
}, {
|
||||
trigger: 'li.js_attribute_value span:contains(Custom)',
|
||||
extra_trigger: 'li.js_attribute_value',
|
||||
|
||||
@@ -433,14 +433,6 @@ div#payment_method {
|
||||
.oe_website_sale .row .row .col-lg-12 {
|
||||
float: none;
|
||||
}
|
||||
/* product detail */
|
||||
|
||||
.product_detail_img {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.oe_website_sale input.js_quantity {
|
||||
min-width: 48px;
|
||||
@@ -615,46 +607,36 @@ a.no-decoration {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.carousel-inner div.item, .carousel-control-prev, .carousel-control-next {
|
||||
.carousel-outer {
|
||||
height: 400px;
|
||||
}
|
||||
.carousel-inner div.item img {
|
||||
max-height: 100%;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.carousel-control-prev, carousel-control-next {
|
||||
background-image: none !important;
|
||||
.carousel-control-prev, .carousel-control-next {
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
&:focus {
|
||||
opacity: 0.65;
|
||||
}
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
> span {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-indicators {
|
||||
background-color: transparent;
|
||||
margin: 10px 0 0;
|
||||
overflow-y: hidden;
|
||||
position: static;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
height: 100px !important;
|
||||
text-align: center;
|
||||
list-style-position: inside;
|
||||
|
||||
li {
|
||||
background-color: transparent !important;
|
||||
border-radius: 0;
|
||||
display: inline-block;
|
||||
margin: 0 !important;
|
||||
width: 54px !important;
|
||||
height: 54px !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
text-indent: unset;
|
||||
border: 1px solid gray('600');
|
||||
opacity: 0.5;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.active img {
|
||||
&.active {
|
||||
opacity: 1;
|
||||
border: 1px solid theme-color('primary');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
.o_website_sale_image_list .o_kanban_view.o_kanban_ungrouped.o_attachments_previews {
|
||||
.o_website_sale_image_list .o_kanban_view.o_kanban_ungrouped {
|
||||
width: auto;
|
||||
|
||||
.o_kanban_record.o_attachment.o_website_sale_image_card {
|
||||
border: none;
|
||||
background: none;
|
||||
margin: 0 0 $grid-gutter-width*0.2;
|
||||
padding: $o-mail-attachment-margin;
|
||||
.o_kanban_record{
|
||||
flex: 0 1 50%;
|
||||
position: relative;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex: 0 0 percentage(1/3);
|
||||
@@ -19,45 +16,26 @@
|
||||
@include media-breakpoint-up(xl) {
|
||||
flex: 0 0 percentage(1/6);
|
||||
}
|
||||
|
||||
.o_image_box {
|
||||
padding-top: 50%;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
.o_image_overlay {
|
||||
bottom: 16%;
|
||||
background: rgba(gray('900'), 0.2);
|
||||
}
|
||||
|
||||
.o_website_sale_image_card_title {
|
||||
@include o-text-overflow(block);
|
||||
padding:6px;
|
||||
// make the image square and in the center
|
||||
.o_squared_image {
|
||||
position: relative;
|
||||
background: gray('200');
|
||||
color:$headings-color;
|
||||
}
|
||||
|
||||
.o_website_sale_image_card_remove, .o_website_sale_image_card_size {
|
||||
@include o-position-absolute(0, 0);
|
||||
border-radius: 0 2px;
|
||||
}
|
||||
|
||||
.o_website_sale_image_card_size {
|
||||
left: 0;
|
||||
right: auto;
|
||||
border-radius: 0 0 2px;
|
||||
font-weight: normal;
|
||||
font-size: $font-size-sm - 1;
|
||||
|
||||
&.label-normal {
|
||||
background-color: gray('200');
|
||||
color: $body-color;
|
||||
overflow: hidden;
|
||||
padding-bottom: 100%;
|
||||
> img {
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_product_image_size {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,47 +102,27 @@
|
||||
<field name="website_style_ids" widget="many2many_tags" groups="base.group_no_one"/>
|
||||
</group>
|
||||
</group>
|
||||
<group name="extra_images" string="Extra Images">
|
||||
<field name="product_image_ids" class="o_website_sale_image_list" context="{'default_name': name, 'default_product_tmpl_id': active_id}" nolabel="1">
|
||||
<kanban string="Product Images" class="o_attachments_previews">
|
||||
<field name="id"/>
|
||||
<field name="name"/>
|
||||
<field name="image"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="o_attachment oe_kanban_global_click o_website_sale_image_card">
|
||||
<div class="o_attachment_wrap">
|
||||
<div class="o_image_box">
|
||||
<div class="o_attachment_image" t-attf-style="background-image:url({{kanban_image('product.image', 'image', record.id.raw_value)}});"/>
|
||||
<div t-attf-class="o_image_overlay" t-att-title="record.name.value"/>
|
||||
<div class="o_website_sale_image_card_title" t-att-title="record.name.value">
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t t-set="size" t-value="[record.image.value][0]"/>
|
||||
<t t-set="size_status" t-value="'normal'" t-if="size.indexOf('Bytes') !== -1 or ( size.indexOf('Kb') !== -1 and size[2]=='.')"/>
|
||||
<t t-set="size_status" t-value="'warning'" t-elif="( size.indexOf('Kb') !== -1 and size[3]=='.')"/>
|
||||
<t t-set="size_status" t-value="'danger'" t-else=""/>
|
||||
<t t-set="message" t-value="'Acceptable file size'" t-if="size_status == 'normal'"></t>
|
||||
<t t-set="message" t-value="'Huge file size. The image should be optimized/reduced.'" t-elif="size_status == 'warning'"></t>
|
||||
<t t-set="message" t-value="'Optimization required! Reduce the image size or increase your compression settings.'" t-else=""></t>
|
||||
|
||||
<span t-attf-class="o_website_sale_image_card_size badge badge-#{size_status}" t-att-title="message">
|
||||
<t t-esc="record.image.value"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
<group name="product_template_images" string="Extra Product Images">
|
||||
<field name="product_template_image_ids" class="o_website_sale_image_list" context="{'default_name': name}" mode="kanban" options="{'create_text':'Add an Image'}" nolabel="1"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_product_view_form_easy_inherit_website_sale" model="ir.ui.view">
|
||||
<field name="name">product.product.view.form.easy.inherit.website_sale</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="inherit_id" ref="product.product_variant_easy_edit_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<sheet position="inside">
|
||||
<group name="product_variant_images" string="Extra Variant Images">
|
||||
<field name="product_variant_image_ids" class="o_website_sale_image_list" context="{'default_name': name}" mode="kanban" options="{'create_text':'Add an Image'}" nolabel="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Product Public Categories -->
|
||||
<record id="product_public_category_form_view" model="ir.ui.view">
|
||||
<field name="name">product.public.category.form</field>
|
||||
@@ -216,25 +196,68 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_product_image_form">
|
||||
<field name="name">product.image.form</field>
|
||||
<!-- This view should only be used from the product o2m because the required field product_tmpl_id has to be automatically set. -->
|
||||
<record id="view_product_image_form" model="ir.ui.view">
|
||||
<field name="name">product.image.view.form</field>
|
||||
<field name="model">product.image</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Product Images">
|
||||
<div class="row o_website_sale_image_modal">
|
||||
<div class="col-md-6 col-xl-5">
|
||||
<div class="oe_title">
|
||||
<label class="small text-muted" for="name" string="Image Name"/>
|
||||
<h2><field name="name" placeholder="Image Name" required="True"/></h2>
|
||||
<field name="product_tmpl_id" invisible="1"/>
|
||||
</div>
|
||||
<label for="name" string="Image Name"/>
|
||||
<h2><field name="name" placeholder="Image Name"/></h2>
|
||||
<!-- Unfortunately for now we can't drag and drop kanban o2m, so we have to let the user input the sequence manually. -->
|
||||
<label for="sequence" string="Sequence"/><br/>
|
||||
<field name="sequence"/>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-7 text-center o_website_sale_image_modal_container">
|
||||
<field name="image" widget="image"/>
|
||||
<field name="image_original" widget="image"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="product_image_view_kanban" model="ir.ui.view">
|
||||
<field name="name">product.image.view.kanban</field>
|
||||
<field name="model">product.image</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban string="Product Images" default_order="sequence">
|
||||
<field name="id"/>
|
||||
<field name="name"/>
|
||||
<field name="image_original"/>
|
||||
<field name="sequence"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="card oe_kanban_global_click p-0">
|
||||
<div class="o_squared_image">
|
||||
<img class="card-img-top" t-att-src="kanban_image('product.image', 'image_original', record.id.value)" t-att-alt="record.name.value"/>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<h4 class="card-title p-2 m-0 bg-200">
|
||||
<small><field name="name"/></small>
|
||||
</h4>
|
||||
</div>
|
||||
<!-- below 100 Kb: good -->
|
||||
<t t-if="record.image_original.raw_value.length < 100*1000">
|
||||
<t t-set="size_status" t-value="'badge-success'"/>
|
||||
<t t-set="message">Acceptable file size</t>
|
||||
</t>
|
||||
<!-- below 1000 Kb: decent -->
|
||||
<t t-elif="record.image_original.raw_value.length < 1000*1000">
|
||||
<t t-set="size_status" t-value="'badge-warning'" />
|
||||
<t t-set="message">Huge file size. The image should be optimized/reduced.</t>
|
||||
</t>
|
||||
<!-- above 1000 Kb: bad -->
|
||||
<t t-else="1">
|
||||
<t t-set="size_status" t-value="'badge-danger'"/>
|
||||
<t t-set="message">Optimization required! Reduce the image size or increase your compression settings.</t>
|
||||
</t>
|
||||
<span t-attf-class="badge #{size_status} o_product_image_size" t-esc="record.image_original.value" t-att-title="message"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -99,11 +99,10 @@
|
||||
|
||||
<!-- Product item used by /shop and /shop/cart -->
|
||||
<template id="products_item" name="Products item">
|
||||
<t t-set="product_href" t-value="keep('/shop/product/%s' % slug(product), page=(pager['page']['num'] if pager['page']['num']>1 else None))" />
|
||||
<t t-set="product_href" t-value="request.httprequest.url_root + keep('shop/product/%s' % slug(product), page=(pager['page']['num'] if pager['page']['num']>1 else None))" />
|
||||
|
||||
<t t-set="first_possible_combination" t-value="product._get_first_possible_combination()"/>
|
||||
<t t-set="combination_info" t-value="product._get_combination_info(first_possible_combination, add_qty=add_qty or 1, pricelist=pricelist)"/>
|
||||
<t t-set="product_variant" t-value="product.env['product.product'].browse(combination_info['product_id'])"/>
|
||||
|
||||
<form action="/shop/cart/update" method="post">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" />
|
||||
@@ -113,8 +112,7 @@
|
||||
</div>
|
||||
<div class="oe_product_image">
|
||||
<a itemprop="url" t-att-href="product_href">
|
||||
<span t-if="product_variant" itemprop="image" t-attf-content="{{request.httprequest.url_root}}web/image/product.product/{{product_variant.id}}/image" t-field="product_variant.image" t-options="{'widget': 'image', 'resize': None if product_image_big else '300x300', 'zoom': 'image'}" t-att-alt="product.name" />
|
||||
<span t-else="" itemprop="image" t-attf-content="{{request.httprequest.url_root}}web/image/product.template/{{product.id}}/image" t-field="product.image" t-options="{'widget': 'image', 'resize': None if product_image_big else '300x300', 'zoom': 'image'}" t-att-alt="product.name" />
|
||||
<span t-field="product.image" t-options="{'widget': 'image', 'preview_image': 'image' if product_image_big else 'image_large'}"/>
|
||||
</a>
|
||||
</div>
|
||||
<t t-if="show_publish">
|
||||
@@ -457,45 +455,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<t t-set="variant_img" t-value="product_variant and product_variant.image_variant"/>
|
||||
<t t-set="image_ids" t-value="product.product_image_ids"/>
|
||||
<div id="o-carousel-product" class="carousel slide" data-ride="carousel" data-interval="0">
|
||||
<div class="carousel-outer">
|
||||
<div class="carousel-inner">
|
||||
<div t-if="variant_img" class="carousel-item active" itemprop="image" t-field="product_variant.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': str(product['__last_update']) + (str(product_variant['__last_update']) or '')}"/>
|
||||
<div t-attf-class="carousel-item#{'' if variant_img else ' active'}" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update']}"/>
|
||||
<t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
|
||||
<div class="carousel-item" t-field="pimg.image" t-options='{"widget": "image", "class": "product_detail_img", "alt-field": "name", "zoom": "image" }'/>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<t t-if="len(image_ids) or variant_img">
|
||||
<a class="carousel-control-prev" href="#o-carousel-product" role="button" data-slide="prev" >
|
||||
<span class="fa fa-chevron-left" role="img" aria-label="Previous" title="Previous"/>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#o-carousel-product" role="button" data-slide="next">
|
||||
<span class="fa fa-chevron-right" role="img" aria-label="Next" title="Next"/>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<ol class="carousel-indicators" t-if="len(image_ids) or variant_img">
|
||||
<li t-if="variant_img" data-target="#o-carousel-product" data-slide-to="0" class="active">
|
||||
<img class="img img-fluid js_variant_img_small" t-attf-src="/website/image/product.product/{{product_variant.id}}/image/90x90" t-att-alt="product.name"/>
|
||||
</li>
|
||||
<li data-target="#o-carousel-product" t-att-data-slide-to="1 if variant_img else '0'" t-att-class="'' if variant_img else 'active'">
|
||||
<img class="img img-fluid" t-attf-src="/website/image/product.template/{{product.id}}/image/90x90" t-att-alt="product.name"/>
|
||||
</li>
|
||||
<t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
|
||||
<li data-target="#o-carousel-product" t-att-data-slide-to="pimg_index + (variant_img and 2 or 1)">
|
||||
<img class="img img-fluid" t-attf-src="/website/image/product.image/{{pimg.id}}/image/90x90" t-att-alt="pimg.name"/>
|
||||
</li>
|
||||
</t>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-8">
|
||||
<t t-call="website_sale.shop_product_carousel"/>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-4 offset-xl-2" id="product_details">
|
||||
<div class="col-md-6 col-xl-4" id="product_details">
|
||||
<h1 itemprop="name" t-field="product.name">Product Name</h1>
|
||||
<span itemprop="url" style="display:none;" t-esc="'%sshop/product/%s' % (request.httprequest.url_root, slug(product))"/>
|
||||
<form t-if="product._is_add_to_cart_possible()" action="/shop/cart/update" method="POST">
|
||||
@@ -1707,41 +1670,32 @@
|
||||
</template>
|
||||
|
||||
<template id="website_sale.shop_product_carousel" name="Shop Product Carousel">
|
||||
<t t-set="variant_img" t-value="product_variant and product_variant.image_variant"/>
|
||||
<t t-set="image_ids" t-value="product.product_image_ids"/>
|
||||
<t t-set="product_images" t-value="product_variant._get_images() if product_variant else product._get_images()"/>
|
||||
<div id="o-carousel-product" class="carousel slide" data-ride="carousel" data-interval="0">
|
||||
<div class="carousel-outer">
|
||||
<div class="carousel-inner">
|
||||
<div t-if="variant_img" class="carousel-item active" itemprop="image" t-field="product_variant.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': str(product['__last_update']) + (str(product_variant['__last_update']) or '')}"/>
|
||||
<div t-attf-class="carousel-item#{'' if variant_img else ' active'}" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update']}"/>
|
||||
<t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
|
||||
<div class="carousel-item" t-field="pimg.image" t-options='{"widget": "image", "class": "product_detail_img", "alt-field": "name", "zoom": "image" }'/>
|
||||
<div class="carousel-outer position-relative">
|
||||
<div class="carousel-inner h-100">
|
||||
<t t-foreach="product_images" t-as="product_image">
|
||||
<div t-attf-class="carousel-item h-100#{' active' if product_image_first else ''}">
|
||||
<div t-field="product_image.image" class="d-flex align-items-center justify-content-center h-100" t-options='{"widget": "image", "preview_image": "image", "class": "product_detail_img mh-100", "alt-field": "name", "zoom": product_image.can_image_be_zoomed and "image_original"}'/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<t t-if="len(image_ids) or variant_img">
|
||||
<a class="carousel-control-prev" href="#o-carousel-product" role="button" data-slide="prev" >
|
||||
<span class="fa fa-chevron-left" role="img" aria-label="Previous" title="Previous"/>
|
||||
<t t-if="len(product_images) > 1">
|
||||
<a class="carousel-control-prev" href="#o-carousel-product" role="button" data-slide="prev">
|
||||
<span class="fa fa-chevron-left p-2" role="img" aria-label="Previous" title="Previous"/>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#o-carousel-product" role="button" data-slide="next">
|
||||
<span class="fa fa-chevron-right" role="img" aria-label="Next" title="Next"/>
|
||||
<span class="fa fa-chevron-right p-2" role="img" aria-label="Next" title="Next"/>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<ol t-if="len(image_ids) or variant_img" class="carousel-indicators">
|
||||
<li t-if="variant_img" data-target="#o-carousel-product" data-slide-to="0" class="active">
|
||||
<img class="img img-fluid js_variant_img_small" t-attf-src="/website/image/product.product/{{product_variant.id}}/image/90x90" t-att-alt="product.name"/>
|
||||
</li>
|
||||
<li data-target="#o-carousel-product" t-att-data-slide-to="1 if variant_img else '0'" t-att-class="'' if variant_img else 'active'">
|
||||
<img class="img img-fluid" t-attf-src="/website/image/product.template/{{product.id}}/image/90x90" t-att-alt="product.name"/>
|
||||
</li>
|
||||
<t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
|
||||
<li data-target="#o-carousel-product" t-att-data-slide-to="pimg_index + (variant_img and 2 or 1)">
|
||||
<img class="img img-fluid" t-attf-src="/website/image/product.image/{{pimg.id}}/image/90x90" t-att-alt="pimg.name"/>
|
||||
</li>
|
||||
</t>
|
||||
</ol>
|
||||
<div t-ignore="True" class="d-none d-md-block text-center">
|
||||
<ol t-if="len(product_images) > 1" class="carousel-indicators d-inline-block position-static mx-auto my-0 p-1 text-left">
|
||||
<t t-foreach="product_images" t-as="product_image"><li t-attf-class="d-inline-block m-1 align-top {{'active' if product_image_first else ''}}" data-target="#o-carousel-product" t-att-data-slide-to="str(product_image_index)">
|
||||
<div t-field="product_image.image_small" class="d-flex align-items-center justify-content-center h-100" t-options='{"widget": "image", "alt-field": "name"}'/>
|
||||
</li></t>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<strong class='float-right'>x</strong>
|
||||
</a>
|
||||
<a t-att-href="'/shop/product/%s' % slug(product.product_tmpl_id)">
|
||||
<img t-attf-src="/web/image/product.product/#{product.id}/image/200x150" class="img img-fluid" style="margin:auto;" alt="Product image"/>
|
||||
<img t-attf-src="/web/image/product.product/#{product.id}/image_large" class="img img-fluid" style="margin:auto;" alt="Product image"/>
|
||||
</a>
|
||||
<div class='product_summary'>
|
||||
<a class="o_product_comparison_table" t-attf-href="/shop/product/#{ slug(product.product_tmpl_id) }">
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<tr t-att-data-wish-id='wish.id' t-att-data-product-id='wish.product_id.id'>
|
||||
<td class='td-img'>
|
||||
<a t-att-href="'/shop/product/%s' % slug(wish.product_id.product_tmpl_id)">
|
||||
<img t-attf-src="/web/image/product.product/#{wish.product_id.id}/image/100x100" class="img img-fluid" style="margin:auto;" alt="Product image"/>
|
||||
<img t-attf-src="/web/image/product.product/#{wish.product_id.id}/image_medium" class="img img-fluid" style="margin:auto;" alt="Product image"/>
|
||||
</a>
|
||||
</td>
|
||||
<td class='text-left'>
|
||||
|
||||
@@ -30,6 +30,9 @@ from . import ir_module
|
||||
from . import ir_demo
|
||||
from . import ir_demo_failure
|
||||
from . import report_paperformat
|
||||
|
||||
from . import image_mixin
|
||||
|
||||
from . import res_country
|
||||
from . import res_lang
|
||||
from . import res_partner
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields, api, tools
|
||||
|
||||
|
||||
class ImageMixin(models.AbstractModel):
|
||||
_name = 'image.mixin'
|
||||
_description = "Image Mixin"
|
||||
|
||||
# all image fields are base64 encoded and PIL-supported
|
||||
|
||||
# all image_raw fields are technical and should not be displayed to the user
|
||||
image_raw_original = fields.Binary("Raw Original Image")
|
||||
|
||||
# resized fields stored (as attachment) for performance
|
||||
image_raw_big = fields.Binary("Raw Big-sized Image", compute='_compute_image_raw', store=True)
|
||||
image_raw_large = fields.Binary("Raw Large-sized Image", compute='_compute_image_raw', store=True)
|
||||
image_raw_medium = fields.Binary("Raw Medium-sized Image", compute='_compute_image_raw', store=True)
|
||||
image_raw_small = fields.Binary("Raw Small-sized Image", compute='_compute_image_raw', store=True)
|
||||
can_image_raw_be_zoomed = fields.Boolean("Can image raw be zoomed", compute='_compute_image_raw', store=True)
|
||||
|
||||
# Computed fields that are used to create a fallback if necessary, it's
|
||||
# recommended to display those fields to the user.
|
||||
image_original = fields.Binary("Original Image", compute='_compute_image_original', inverse='_set_image_original', help="Image in its original size, as it was uploaded.")
|
||||
image = fields.Binary("Big-sized Image", help="1024px * 1024px", compute='_compute_image', inverse='_set_image')
|
||||
image_large = fields.Binary("Large-sized Image", help="256px * 256px", compute='_compute_image_large')
|
||||
image_medium = fields.Binary("Medium-sized Image", help="128px * 128px", compute='_compute_image_medium')
|
||||
image_small = fields.Binary("Small-sized Image", help="64px * 64px", compute='_compute_image_small')
|
||||
can_image_be_zoomed = fields.Boolean("Can image be zoomed", compute='_compute_can_image_be_zoomed')
|
||||
|
||||
@api.multi
|
||||
@api.depends('image_raw_original')
|
||||
def _compute_image_raw(self):
|
||||
for record in self:
|
||||
images = tools.image_get_resized_images(record.image_raw_original, big_name=False)
|
||||
record.image_raw_big = tools.image_get_resized_images(record.image_raw_original,
|
||||
large_name=False, medium_name=False, small_name=False, preserve_aspect_ratio=True)['image']
|
||||
record.image_raw_large = images['image_large']
|
||||
record.image_raw_medium = images['image_medium']
|
||||
record.image_raw_small = images['image_small']
|
||||
record.can_image_raw_be_zoomed = tools.is_image_size_above(record.image_raw_original)
|
||||
|
||||
@api.multi
|
||||
def _compute_image_original(self):
|
||||
for record in self:
|
||||
record.image_original = record.image_raw_original or record._get_image_fallback_record().image_raw_original
|
||||
|
||||
@api.multi
|
||||
def _set_image_original(self):
|
||||
for record in self:
|
||||
fallback = record._get_image_fallback_record()
|
||||
if (
|
||||
# We are trying to remove an image even though it is already
|
||||
# not set, remove it from the fallback record instead.
|
||||
not record.image_original and not record.image_raw_original or
|
||||
# We are trying to add an image, but the fallback image is
|
||||
# not set, write on the fallback instead.
|
||||
record.image_original and not fallback.image_raw_original or
|
||||
# The record using the mixin asks to always write on fallback.
|
||||
record._force_write_image_on_fallback()
|
||||
):
|
||||
fallback.image_raw_original = record.image_original
|
||||
else:
|
||||
record.image_raw_original = record.image_original
|
||||
|
||||
@api.multi
|
||||
def _compute_image(self):
|
||||
for record in self:
|
||||
record.image = record.image_raw_big or record._get_image_fallback_record().image_raw_big
|
||||
|
||||
@api.multi
|
||||
def _set_image(self):
|
||||
for record in self:
|
||||
record.image_original = record.image
|
||||
|
||||
@api.multi
|
||||
def _compute_image_large(self):
|
||||
for record in self:
|
||||
record.image_large = record.image_raw_large or record._get_image_fallback_record().image_raw_large
|
||||
|
||||
@api.multi
|
||||
def _compute_image_medium(self):
|
||||
for record in self:
|
||||
record.image_medium = record.image_raw_medium or record._get_image_fallback_record().image_raw_medium
|
||||
|
||||
@api.multi
|
||||
def _compute_image_small(self):
|
||||
for record in self:
|
||||
record.image_small = record.image_raw_small or record._get_image_fallback_record().image_raw_small
|
||||
|
||||
@api.multi
|
||||
def _compute_can_image_be_zoomed(self):
|
||||
for record in self:
|
||||
record.can_image_be_zoomed = record.can_image_raw_be_zoomed if record.image_raw_original else record._get_image_fallback_record().can_image_raw_be_zoomed
|
||||
|
||||
@api.multi
|
||||
def _get_image_fallback_record(self):
|
||||
"""Return a record to fallback on when getting an image if the image is
|
||||
not set for self.
|
||||
|
||||
The returned record must implement the current mixin, or at least have
|
||||
the different ``image_raw`` fields defined on it."""
|
||||
self.ensure_one()
|
||||
return self
|
||||
|
||||
@api.multi
|
||||
def _force_write_image_on_fallback(self):
|
||||
"""Return whether we should always write the image on the fallback.
|
||||
|
||||
:return: True to always write on the fallback no matter the situation,
|
||||
False to write on the current record or on the fallback depending
|
||||
on the situation. See `_set_image_original`.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return False
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
"""If the fallback image is not set, move the current image to the
|
||||
fallback."""
|
||||
for record in self:
|
||||
fallback = record._get_image_fallback_record()
|
||||
if fallback != record and record.image_raw_original and not fallback.image_raw_original:
|
||||
fallback.image_raw_original = record.image_raw_original
|
||||
return super(ImageMixin, self).unlink()
|
||||
Reference in New Issue
Block a user