From 752dcfa0f1edbcae172964939382e4d849faef3a Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Tue, 6 Aug 2019 11:09:08 +0000 Subject: [PATCH 1/8] [MOV] sale_product_configurator -> product: part of product_configurator logic for matrix part of product_configurator logic moved to product to be accessible in purchase even if p_configurator isn't installed. And adapt configurator to be extended by matrix --- addons/product/models/product_template.py | 24 ++++++++++++++++++ .../models/product.py | 14 +++-------- .../src/js/product_configurator_widget.js | 25 ++++++++++++------- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index fd05e009dc4..98fb97d49af 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -150,12 +150,22 @@ class ProductTemplate(models.Model): item_ids = fields.One2many('product.pricelist.item', 'product_tmpl_id', 'Pricelist Items') can_image_1024_be_zoomed = fields.Boolean("Can Image 1024 be zoomed", compute='_compute_can_image_1024_be_zoomed', store=True) + has_configurable_attributes = fields.Boolean("Is a configurable product", compute='_compute_has_configurable_attributes', store=True) @api.depends('image_1920', 'image_1024') def _compute_can_image_1024_be_zoomed(self): for template in self: template.can_image_1024_be_zoomed = template.image_1920 and tools.is_image_size_above(template.image_1920, template.image_1024) + @api.depends('attribute_line_ids', 'attribute_line_ids.value_ids', 'attribute_line_ids.attribute_id.create_variant') + def _compute_has_configurable_attributes(self): + """A product is considered configurable if: + - It has dynamic attributes + - It has any attribute line with at least 2 attribute values configured + """ + for product in self: + product.has_configurable_attributes = product.has_dynamic_attributes() or any(len(ptal.value_ids) >= 2 for ptal in product.attribute_line_ids) + @api.depends('product_variant_ids') def _compute_product_variant_id(self): for p in self: @@ -984,6 +994,20 @@ class ProductTemplate(models.Model): self.ensure_one() return self.env.company + def get_single_product_variant(self): + """ Method used by the product configurator to check if the product is configurable or not. + + We need to open the product configurator if the product: + - is configurable (see has_configurable_attributes) + - has optional products (method is extended in sale to return optional products info) + """ + self.ensure_one() + if self.product_variant_count == 1 and not self.has_configurable_attributes: + return { + 'product_id': self.product_variant_id.id, + } + return {} + @api.model def get_empty_list_help(self, help): self = self.with_context( diff --git a/addons/sale_product_configurator/models/product.py b/addons/sale_product_configurator/models/product.py index 3250a72add6..d565de3fc2f 100644 --- a/addons/sale_product_configurator/models/product.py +++ b/addons/sale_product_configurator/models/product.py @@ -12,7 +12,6 @@ class ProductTemplate(models.Model): string='Optional Products', help="Optional Products are suggested " "whenever the customer hits *Add to Cart* (cross-sell strategy, " "e.g. for computers: warranty, software, etc.).") - has_configurable_attributes = fields.Boolean("Is a configurable product", compute='_compute_has_configurable_attributes', store=True) @api.depends('attribute_line_ids.value_ids.is_custom', 'attribute_line_ids.attribute_id.create_variant') def _compute_has_configurable_attributes(self): @@ -32,17 +31,12 @@ class ProductTemplate(models.Model): - is configurable (see has_configurable_attributes) - has optional products """ self.ensure_one() - - if self.product_variant_count == 1 and not self.has_configurable_attributes: + res = super(ProductTemplate, self).get_single_product_variant() + if res.get('product_id', False): has_optional_products = False for optional_product in self.product_variant_id.optional_product_ids: if optional_product.has_dynamic_attributes() or optional_product._get_possible_variants(self.product_variant_id.product_template_attribute_value_ids): has_optional_products = True break - - return { - 'product_id': self.product_variant_id.id, - 'has_optional_products': has_optional_products - } - - return None + res.update({'has_optional_products': has_optional_products}) + return res diff --git a/addons/sale_product_configurator/static/src/js/product_configurator_widget.js b/addons/sale_product_configurator/static/src/js/product_configurator_widget.js index 6f41174be73..8ed2fd43f0e 100644 --- a/addons/sale_product_configurator/static/src/js/product_configurator_widget.js +++ b/addons/sale_product_configurator/static/src/js/product_configurator_widget.js @@ -65,7 +65,7 @@ ProductConfiguratorWidget.include({ productTemplateId ] }).then(function (result) { - if (result && !result.has_optional_products) { + if (result.product_id && !result.has_optional_products) { self.trigger_up('field_changed', { dataPointID: dataPointId, changes: { @@ -76,15 +76,8 @@ ProductConfiguratorWidget.include({ }, }); } else { - self._openProductConfigurator({ - configuratorMode: (result && result.has_optional_products ? 'options' : 'add'), - default_pricelist_id: self._getPricelistId(), - default_product_template_id: productTemplateId - }, - dataPointId - ); + return self._openConfigurator(result, productTemplateId, dataPointId); } - return Promise.resolve(true); // always returns true for the moment because no other configurator exists. }); }, @@ -115,6 +108,20 @@ ProductConfiguratorWidget.include({ } }, + _openConfigurator: function (result, productTemplateId, dataPointId) { + if (!result.mode || result.mode === 'configurator') { + this._openProductConfigurator({ + configuratorMode: result && result.has_optional_products ? 'options' : 'add', + default_pricelist_id: this._getPricelistId(), + default_product_template_id: productTemplateId + }, + dataPointId + ); + return Promise.resolve(true); + } + return Promise.resolve(false); + }, + /** * Opens the product configurator to allow configuring the product template * and its various options. From 1514cd0b68ee3b15341581fb1e8b04058b128064 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Wed, 10 Jul 2019 12:57:43 +0000 Subject: [PATCH 2/8] [MOV] sale -> product : product variant creation based on combination of product.template.attribute.value --- addons/product/models/product_template.py | 48 ++++++++++++++++++++++- addons/sale/models/product_template.py | 45 --------------------- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 98fb97d49af..17e8594b006 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -2,7 +2,8 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import itertools - +import logging +_logger = logging.getLogger(__name__) from odoo import api, fields, models, tools, _ from odoo.exceptions import ValidationError, RedirectWarning, UserError @@ -831,6 +832,51 @@ class ProductTemplate(models.Model): attribute_values = filtered_combination.mapped('product_attribute_value_id') return self.env['product.product'].browse(self._get_variant_id_for_combination(attribute_values)) + def _create_product_variant(self, combination, log_warning=False): + """ Create if necessary and possible and return the product variant + matching the given combination for this template. + + It is possible to create only if the template has dynamic attributes + and the combination itself is possible. + + :param combination: the combination for which to get or create variant. + The combination must contain all necessary attributes, including + those of type no_variant. Indeed even though those attributes won't + be included in the variant if newly created, they are needed when + checking if the combination is possible. + :type combination: recordset of `product.template.attribute.value` + + :param log_warning: whether a warning should be logged on fail + :type log_warning: bool + + :return: the product variant matching the combination or none + :rtype: recordset of `product.product` + """ + self.ensure_one() + + Product = self.env['product.product'] + + product_variant = self._get_variant_for_combination(combination) + if product_variant: + return product_variant + + if not self.has_dynamic_attributes(): + if log_warning: + _logger.warning('The user #%s tried to create a variant for the non-dynamic product %s.' % (self.env.user.id, self.id)) + return Product + + if not self._is_combination_possible(combination): + if log_warning: + _logger.warning('The user #%s tried to create an invalid variant for the product %s.' % (self.env.user.id, self.id)) + return Product + + attribute_values = combination.mapped('product_attribute_value_id')._without_no_variant_attributes() + + return Product.sudo().create({ + 'product_tmpl_id': self.id, + 'attribute_value_ids': [(6, 0, attribute_values.ids)] + }) + @tools.ormcache('self.id', 'attribute_values') def _get_variant_id_for_combination(self, attribute_values): """See `_get_variant_for_combination`. This method returns an ID diff --git a/addons/sale/models/product_template.py b/addons/sale/models/product_template.py index e0816d269d8..1102d3e93b1 100644 --- a/addons/sale/models/product_template.py +++ b/addons/sale/models/product_template.py @@ -59,51 +59,6 @@ class ProductTemplate(models.Model): } return action - def _create_product_variant(self, combination, log_warning=False): - """ Create if necessary and possible and return the product variant - matching the given combination for this template. - - It is possible to create only if the template has dynamic attributes - and the combination itself is possible. - - :param combination: the combination for which to get or create variant. - The combination must contain all necessary attributes, including - those of type no_variant. Indeed even though those attributes won't - be included in the variant if newly created, they are needed when - checking if the combination is possible. - :type combination: recordset of `product.template.attribute.value` - - :param log_warning: whether a warning should be logged on fail - :type log_warning: bool - - :return: the product variant matching the combination or none - :rtype: recordset of `product.product` - """ - self.ensure_one() - - Product = self.env['product.product'] - - product_variant = self._get_variant_for_combination(combination) - if product_variant: - return product_variant - - if not self.has_dynamic_attributes(): - if log_warning: - _logger.warning('The user #%s tried to create a variant for the non-dynamic product %s.' % (self.env.user.id, self.id)) - return Product - - if not self._is_combination_possible(combination): - if log_warning: - _logger.warning('The user #%s tried to create an invalid variant for the product %s.' % (self.env.user.id, self.id)) - return Product - - attribute_values = combination.mapped('product_attribute_value_id')._without_no_variant_attributes() - - return Product.sudo().create({ - 'product_tmpl_id': self.id, - 'attribute_value_ids': [(6, 0, attribute_values.ids)] - }) - def create_product_variant(self, product_template_attribute_value_ids): """ Create if necessary and possible and return the id of the product variant matching the given combination for this template. From 56aab8bc1195efbdf6d12e773126f5f802be9b7d Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Mon, 8 Jul 2019 12:55:18 +0000 Subject: [PATCH 3/8] [IMP] web/dialog.js : support xl dialog modal size. --- addons/web/static/src/js/core/dialog.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/core/dialog.js b/addons/web/static/src/js/core/dialog.js index 63b33497cd3..94741f98233 100644 --- a/addons/web/static/src/js/core/dialog.js +++ b/addons/web/static/src/js/core/dialog.js @@ -30,7 +30,8 @@ var Dialog = Widget.extend({ * @param {Object} [options] * @param {string} [options.title=Odoo] * @param {string} [options.subtitle] - * @param {string} [options.size=large] - 'large', 'medium' or 'small' + * @param {string} [options.size=large] - 'extra-large', 'large', 'medium' + * or 'small' * @param {boolean} [options.fullscreen=false] - whether or not the dialog * should be open in fullscreen mode (the main usecase is mobile) * @param {string} [options.dialogClass] - class to add to the modal-body @@ -114,6 +115,9 @@ var Dialog = Widget.extend({ renderFooter: self.renderFooter, })); switch (self.size) { + case 'extra-large': + self.$modal.find('.modal-dialog').addClass('modal-xl'); + break; case 'large': self.$modal.find('.modal-dialog').addClass('modal-lg'); break; From 3e41a1dbf25cff38820a1b82ecfe1b4f57005d09 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Wed, 10 Jul 2019 13:42:41 +0000 Subject: [PATCH 4/8] [IMP] purchase: split onchange product_id for matrix triggering. --- addons/purchase/models/purchase.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 5ff8dcae7ae..72bfab95f50 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -598,8 +598,17 @@ class PurchaseOrderLine(models.Model): # Reset date, price and quantity since _onchange_quantity will provide default values self.date_planned = datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT) self.price_unit = self.product_qty = 0.0 - self.product_uom = self.product_id.uom_po_id or self.product_id.uom_id + self._product_id_change() + + self._suggest_quantity() + self._onchange_quantity() + + def _product_id_change(self): + if not self.product_id: + return + + self.product_uom = self.product_id.uom_po_id or self.product_id.uom_id product_lang = self.product_id.with_context( lang=self.partner_id.lang, partner_id=self.partner_id.id, @@ -611,9 +620,6 @@ class PurchaseOrderLine(models.Model): self._compute_tax_id() - self._suggest_quantity() - self._onchange_quantity() - @api.onchange('product_id') def onchange_product_id_warning(self): if not self.product_id: From f6f4b8bc1eb645e7ceafacfae849d291389f2ad6 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Mon, 15 Jul 2019 13:45:48 +0000 Subject: [PATCH 5/8] [IMP] purchase : name for xpath --- addons/purchase/views/purchase_views.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml index f705dab0a19..634b1585394 100644 --- a/addons/purchase/views/purchase_views.xml +++ b/addons/purchase/views/purchase_views.xml @@ -338,13 +338,13 @@ - + - + From 82465461387a2802cc8521ef22770edb753a8c05 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Wed, 10 Jul 2019 11:36:30 +0000 Subject: [PATCH 6/8] [FIX] sale: sale_coupon isn't an enterprise module anymore --- addons/sale/views/res_config_settings_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale/views/res_config_settings_views.xml b/addons/sale/views/res_config_settings_views.xml index 9b0d7e20b99..6f67fab1d6e 100644 --- a/addons/sale/views/res_config_settings_views.xml +++ b/addons/sale/views/res_config_settings_views.xml @@ -92,7 +92,7 @@
- +