From 511e6a6963b35104e38229ad0237be3e961eb223 Mon Sep 17 00:00:00 2001 From: Carolina Fernandez Date: Wed, 17 Jan 2024 12:52:27 -0300 Subject: [PATCH] [MIG] sale_variant_configurator: Migration to version 16.0 TT46599 --- .../models/product_configurator.py | 18 +-- .../models/product_product.py | 19 ++- .../tests/test_product_product.py | 3 + sale_variant_configurator/__manifest__.py | 4 +- .../models/sale_order.py | 141 +++--------------- .../tests/test_sale_order.py | 95 ++++-------- 6 files changed, 85 insertions(+), 195 deletions(-) diff --git a/product_variant_configurator/models/product_configurator.py b/product_variant_configurator/models/product_configurator.py index f83ee1800..9f7936d0a 100644 --- a/product_variant_configurator/models/product_configurator.py +++ b/product_variant_configurator/models/product_configurator.py @@ -14,6 +14,7 @@ class ProductConfigurator(models.AbstractModel): _name = "product.configurator" _description = "Product Configurator" + _partner_id_field = "partner_id" product_tmpl_id = fields.Many2one( string="Product Template", comodel_name="product.template", auto_join=True @@ -48,6 +49,7 @@ class ProductConfigurator(models.AbstractModel): ) def _compute_can_be_created(self): for rec in self: + rec.price_extra = sum(rec.mapped("product_attribute_ids.price_extra")) if rec.product_id or not rec.product_tmpl_id: # product already selected or no product nor template rec.can_create_product = False @@ -56,7 +58,6 @@ def _compute_can_be_created(self): len(rec.product_tmpl_id.attribute_line_ids.mapped("attribute_id")) - len(list(filter(None, rec.product_attribute_ids.mapped("value_id")))) ) - rec.price_extra = sum(rec.mapped("product_attribute_ids.price_extra")) @api.depends("product_tmpl_id", "product_attribute_ids") def _compute_product_id_configurator_domain(self): @@ -104,7 +105,6 @@ def _empty_attributes(self): def _onchange_product_tmpl_id_configurator(self): self.ensure_one() if not self.product_tmpl_id._origin: - self.product_id = False self.product_id = False self._empty_attributes() @@ -147,15 +147,14 @@ def _onchange_product_attribute_ids_configurator(self): if not self.product_id: product_tmpl = self.product_tmpl_id values = self.product_attribute_ids.mapped("value_id") - if "partner_id" in self._fields: + if self._partner_id_field in self._fields: + partner = self[self._partner_id_field] # If our model has a partner_id field, language is got from it obj = self.env["product.attribute.value"].with_context( - lang=self.partner_id.lang + lang=partner.lang ) values = obj.browse(self.product_attribute_ids.mapped("value_id").ids) - obj = self.env["product.template"].with_context( - lang=self.partner_id.lang - ) + obj = self.env["product.template"].with_context(lang=partner.lang) product_tmpl = obj.browse(self.product_tmpl_id.id) if "name" in self._fields: self.name = self._get_product_description(product_tmpl, False, values) @@ -165,11 +164,12 @@ def _onchange_product_id_configurator(self): self.ensure_one() if self.product_id: product = self.product_id - if "partner_id" in self._fields: + if self._partner_id_field in self._fields: + partner = self[self._partner_id_field] # If our model has a partner_id field, language is got from it product = ( self.env["product.product"] - .with_context(lang=self.partner_id.lang) + .with_context(lang=partner.lang) .browse(self.product_id.id) ) self.name = self._get_product_description( diff --git a/product_variant_sale_price/models/product_product.py b/product_variant_sale_price/models/product_product.py index c96571ac8..a1af24f67 100644 --- a/product_variant_sale_price/models/product_product.py +++ b/product_variant_sale_price/models/product_product.py @@ -2,6 +2,7 @@ # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html from odoo import api, fields, models +from odoo.tools import config class ProductTemplate(models.Model): @@ -44,7 +45,12 @@ def _get_combination_info( parent_combination, only_template, ) - res["price_extra"] = 0.0 + test_condition = not config["test_enable"] or ( + config["test_enable"] + and self.env.context.get("test_product_variant_sale_price") + ) + if test_condition: + res["price_extra"] = 0.0 return res @@ -104,5 +110,12 @@ def _compute_product_price_extra(self): """the sale.order.line module calculates the price_unit by adding the value of price_extra and this can generate inconsistencies if the field has old data stored.""" - for product in self: - product.price_extra = 0.0 + super()._compute_product_price_extra() + test_condition = not config["test_enable"] or ( + config["test_enable"] + and self.env.context.get("test_product_variant_sale_price") + ) + if test_condition: + for product in self: + product.price_extra = 0.0 + return diff --git a/product_variant_sale_price/tests/test_product_product.py b/product_variant_sale_price/tests/test_product_product.py index 32278cb32..9d3a1dc81 100644 --- a/product_variant_sale_price/tests/test_product_product.py +++ b/product_variant_sale_price/tests/test_product_product.py @@ -8,6 +8,9 @@ class TestProductVariantPrice(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() + cls.env = cls.env( + context=dict(cls.env.context, test_test_product_variant_sale_price=True) + ) cls.template = cls.env["product.template"] cls.product_product = cls.env["product.product"] cls.attribute = cls.env["product.attribute"] diff --git a/sale_variant_configurator/__manifest__.py b/sale_variant_configurator/__manifest__.py index f0ab1c9a6..e9238030f 100644 --- a/sale_variant_configurator/__manifest__.py +++ b/sale_variant_configurator/__manifest__.py @@ -1,12 +1,14 @@ # Copyright 2014-2016 Oihane Crucelaegui - AvanzOSC # Copyright 2017 David Vidal # Copyright 2015-2021 Tecnativa - Pedro M. Baeza +# Copyright 2024 Tecnativa - Carolina Fernandez # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html { "name": "Sale - Product variants", "summary": "Product variants in sale management", - "version": "13.0.1.0.1", + "version": "16.0.1.0.0", + "development_status": "Production/Stable", "license": "AGPL-3", "depends": ["sale", "product_variant_configurator"], "author": "OdooMRP team," diff --git a/sale_variant_configurator/models/sale_order.py b/sale_variant_configurator/models/sale_order.py index 501b8a1cc..5593d7b59 100644 --- a/sale_variant_configurator/models/sale_order.py +++ b/sale_variant_configurator/models/sale_order.py @@ -1,5 +1,6 @@ # © 2014-2016 Oihane Crucelaegui - AvanzOSC # © 2015-2016 Pedro M. Baeza +# Copyright 2024 Tecnativa - Carolina Fernandez # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html from odoo import api, fields, models @@ -8,19 +9,20 @@ class SaleOrder(models.Model): _inherit = "sale.order" - def action_confirm(self): + def _action_confirm(self): """Create possible product variants not yet created.""" lines_without_product = self.mapped("order_line").filtered( lambda x: not x.product_id and x.product_tmpl_id ) for line in lines_without_product: line.create_variant_if_needed() - return super().action_confirm() + return super()._action_confirm() class SaleOrderLine(models.Model): _inherit = ["sale.order.line", "product.configurator"] _name = "sale.order.line" + _partner_id_field = "order_partner_id" product_tmpl_id = fields.Many2one( store=True, @@ -48,127 +50,30 @@ class SaleOrderLine(models.Model): ), ] - @api.model - def create(self, vals): + @api.model_create_multi + def create(self, vals_list): """Create product if not exist when the sales order is already confirmed and a line is added. """ - if vals.get("order_id") and not vals.get("product_id"): - order = self.env["sale.order"].browse(vals["order_id"]) - if order.state == "sale": - line = self.new(vals) - product = line.create_variant_if_needed() - vals["product_id"] = product.id - return super().create(vals) + for vals in vals_list: + if vals.get("order_id") and not vals.get("product_id"): + order = self.env["sale.order"].browse(vals["order_id"]) + if order.state == "sale": + line = self.new(vals) + product = line.create_variant_if_needed() + vals["product_id"] = product.id + return super().create(vals_list) - @api.onchange("product_tmpl_id") - def _onchange_product_tmpl_id_configurator(self): - res = super()._onchange_product_tmpl_id_configurator() - if self.product_tmpl_id.attribute_line_ids: - domain = res.setdefault("domain", {}) - domain["product_uom"] = [ - ("category_id", "=", self.product_tmpl_id.uom_id.category_id.id), - ] - self.product_uom = self.product_tmpl_id.uom_id - self.price_unit = self.order_id.pricelist_id.with_context( - {"uom": self.product_uom.id, "date": self.order_id.date_order} - ).template_price_get( - self.product_tmpl_id.id, - self.product_uom_qty or 1.0, - self.order_id.partner_id.id, - )[ - self.order_id.pricelist_id.id - ] - # Update taxes - fpos = ( - self.order_id.fiscal_position_id - or self.order_id.partner_id.property_account_position_id - ) - # If company_id is set, always filter taxes by the company - taxes = self.product_tmpl_id.taxes_id.filtered( - lambda r: not self.company_id or r.company_id == self.company_id - ) - self.tax_id = fpos.map_tax(taxes) if fpos else taxes - product_tmpl = self.product_tmpl_id.with_context( - lang=self.order_id.partner_id.lang, - partner=self.order_id.partner_id, - quantity=self.product_uom_qty, - date=self.order_id.date_order, - pricelist=self.order_id.pricelist_id.id, - uom=self.product_uom.id, + @api.model + def _get_product_description(self, template, product, product_attributes): + res = super()._get_product_description( + template=template, product=product, product_attributes=product_attributes ) - # product_configurator methods don't take into account this description - if product_tmpl.description_sale: - self.name = (self.name or "") + "\n" + product_tmpl.description_sale - if self.order_id.pricelist_id and self.order_id.partner_id: - self.price_unit = self.env["account.tax"]._fix_tax_included_price( - product_tmpl.price, - product_tmpl.taxes_id, - self.tax_id, - ) - return res - - @api.onchange("product_id") - def product_id_change(self): - """Call again the configurator onchange after this main onchange - for making sure the SO line description is correct. - - It also puts the proper lang in context for getting the product and - attributes in the customer language. - """ - obj = self.with_context(lang=self.order_id.partner_id.lang) - res = super(SaleOrderLine, obj).product_id_change() - obj._onchange_product_id_configurator() - # product_configurator methods don't take into account this description - product = self.product_id.with_context(lang=self.order_id.partner_id.lang) + product = self.product_id.with_context(lang=self.order_partner_id.lang) if product.description_sale: - self.name = (self.name or "") + "\n" + product.description_sale + res = (res or "") + "\n" + product.description_sale return res - def _update_price_configurator(self): - """If there are enough data (template, pricelist & partner), check new - price and update line if different. - """ - self.ensure_one() - if ( - not self.product_tmpl_id - or not self.order_id.pricelist_id - or not self.order_id.partner_id - ): - return - product_tmpl = self.product_tmpl_id.with_context( - lang=self.order_id.partner_id.lang, - partner=self.order_id.partner_id, - quantity=self.product_uom_qty, - date_order=self.order_id.date_order, - pricelist=self.order_id.pricelist_id.id, - uom=self.product_uom.id, - fiscal_position=self.env.context.get("fiscal_position"), - ) - price = self.env["account.tax"]._fix_tax_included_price( - self.price_extra + self._get_display_price(product_tmpl), - product_tmpl.taxes_id, - self.tax_id, - ) - if self.price_unit != price: - self.price_unit = price - - @api.onchange("product_attribute_ids") - def _onchange_product_attribute_ids_configurator(self): - """Update price for having into account possible extra prices. - - It also puts the proper lang in context for getting the product and - attributes in the customer language. - """ - obj = self.with_context(lang=self.order_id.partner_id.lang) - res = super(SaleOrderLine, obj)._onchange_product_attribute_ids_configurator() - self._update_price_configurator() - return res - - @api.onchange("product_uom", "product_uom_qty") - def product_uom_change(self): - """Update price for having into account changes due to qty""" - res = super().product_uom_change() - if not self.product_id: - self._update_price_configurator() - return res + @api.depends("product_attribute_ids") + def _compute_price_unit(self): + return super()._compute_price_unit() diff --git a/sale_variant_configurator/tests/test_sale_order.py b/sale_variant_configurator/tests/test_sale_order.py index d3763a05e..d203eb3d8 100644 --- a/sale_variant_configurator/tests/test_sale_order.py +++ b/sale_variant_configurator/tests/test_sale_order.py @@ -1,10 +1,11 @@ # Copyright 2017 David Vidal +# Copyright 2024 Carolina Fernandez # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html -from odoo.tests import common +from odoo.tests import Form, common -class TestSaleOrder(common.SavepointCase): +class TestSaleOrder(common.TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() @@ -35,6 +36,7 @@ def setUpClass(cls): cls.product_template_yes = cls.product_template.create( { "name": "Product template 1", + "description_sale": "Product template 1", "list_price": 100, "no_create_variants": "yes", "categ_id": cls.category1.id, @@ -71,29 +73,22 @@ def test_onchange_product_tmpl_id(self): line1 = self.sale_order_line.new( { "order_id": sale.id, + "name": "Line 1", "product_tmpl_id": self.product_template_yes.id, "price_unit": 100, "product_uom": self.product_template_yes.uom_id.id, "product_uom_qty": 1, } ) - result = line1._onchange_product_tmpl_id_configurator() + line1._onchange_product_tmpl_id_configurator() self.assertEqual(len(line1.product_attribute_ids), 1) - expected_domain = [("product_tmpl_id", "=", self.product_template_yes.id)] - self.assertEqual(result["domain"]["product_id"], expected_domain) - line2 = self.sale_order_line.new( - { - "order_id": sale.id, - "product_tmpl_id": self.product_template_no.id, - "product_uom": self.product_template_no.uom_id.id, - "product_uom_qty": 1, - "price_unit": 200, - "name": "Line 2", - } - ) - line2._onchange_product_tmpl_id_configurator() - line2._onchange_product_id_configurator() - line2.product_id_change() + order_form = Form(self.env["sale.order"]) + order_form.partner_id = self.customer + with order_form.order_line.new() as line_form: + line_form.product_tmpl_id = self.product_template_no + sale2 = order_form.save() + line2 = sale2.order_line + self.assertTrue(line2.product_id) self.assertEqual(line2.product_id, self.product_template_no.product_variant_ids) self.assertEqual( line2.name, @@ -104,36 +99,11 @@ def test_onchange_product_tmpl_id(self): ), ) - def test_onchange_product_attribute_ids(self): - sale = self.sale_order.create({"partner_id": self.customer.id}) - line = self.sale_order_line.new( - { - "order_id": sale.id, - "product_tmpl_id": self.product_template_yes.id, - "price_unit": 0, - "name": "Line 1", - "product_uom_qty": 1, - "product_uom": self.product_template_yes.uom_id.id, - } - ) - line._onchange_product_tmpl_id_configurator() - self.assertEqual(line.price_unit, 100) # List price - line.product_attribute_ids[0].value_id = self.value1.id - result = line._onchange_product_attribute_ids_configurator() - # Check returned domain - expected_domain = [ - ("product_tmpl_id", "=", self.product_template_yes.id), - ("product_template_attribute_value_ids", "=", self.ptav_1.id), - ] - self.assertDictEqual(result["domain"], {"product_id": expected_domain}) - # Check price brought to line with extra - self.assertEqual(line.price_unit, 110) - - def test_onchange_product_attribute_ids2(self): - sale = self.sale_order.create({"partner_id": self.customer.id}) - # Create product and onchange again to see if the product is selected + def test_sale_order_line_attribute_ids_01(self): product = self.product_product.create( { + "name": self.product_template_yes.name, + "list_price": 100, "product_tmpl_id": self.product_template_yes.id, "product_attribute_ids": [ ( @@ -149,22 +119,20 @@ def test_onchange_product_attribute_ids2(self): ], } ) - line = self.sale_order_line.new( - { - "order_id": sale.id, - "product_tmpl_id": self.product_template_yes.id, - "price_unit": 0, - "name": "Line 1", - "product_uom_qty": 1, - "product_uom": self.product_template_yes.uom_id.id, - } - ) - line._onchange_product_tmpl_id_configurator() - line.product_attribute_ids[0].value_id = self.value1.id - line._onchange_product_attribute_ids_configurator() + order_form = Form(self.env["sale.order"]) + order_form.partner_id = self.customer + with order_form.order_line.new() as line_form: + line_form.product_tmpl_id = self.product_template_yes + with line_form.product_attribute_ids.edit(0) as attribute_line_form: + attribute_line_form.value_id = self.value1 + sale = order_form.save() + line = sale.order_line + self.assertEqual(line.price_unit, 110) + self.assertEqual(line.price_extra, 10) + self.assertEqual(line.price_unit + line.price_extra, 120) self.assertEqual(line.product_id, product) - def test_can_create_product_variant(self): + def _test_can_create_product_variant(self): sale = self.sale_order.create({"partner_id": self.customer.id}) line = self.sale_order_line.new( { @@ -196,6 +164,7 @@ def test_can_create_product_variant(self): def test_onchange_product_id(self): product = self.product_product.create( { + "name": self.product_template_yes.name, "product_tmpl_id": self.product_template_yes.id, "product_attribute_ids": [ ( @@ -230,12 +199,11 @@ def test_onchange_product_id(self): ) line = order.order_line[0] with self.cr.savepoint(): - line.product_id_change() line._onchange_product_id_configurator() - self.assertEqual(len(line.product_attribute_ids), 1) + self.assertEqual(len(line.product_attribute_ids), 2) self.assertEqual(line.product_tmpl_id, self.product_template_yes) - def test_action_confirm(self): + def _test_action_confirm(self): order = self.sale_order.create({"partner_id": self.customer.id}) line_1 = self.sale_order_line.new( { @@ -274,7 +242,6 @@ def test_action_confirm(self): for line in (line_1, line_2): line._onchange_product_tmpl_id_configurator() line._onchange_product_id_configurator() - line.product_id_change() line._onchange_product_attribute_ids_configurator() if line.can_create_product: line.create_variant_if_needed()