Skip to content

Commit

Permalink
[IMP] product_configurator: Price for custom values with formula
Browse files Browse the repository at this point in the history
  • Loading branch information
SirAionTech committed Aug 5, 2024
1 parent cab752c commit e7e758e
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 17 deletions.
29 changes: 29 additions & 0 deletions product_configurator/models/product_attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.safe_eval import test_python_expr


class ProductAttribute(models.Model):
Expand Down Expand Up @@ -83,6 +84,34 @@ def onchange_val_custom_field(self):
)
uom_id = fields.Many2one(comodel_name="uom.uom", string="Unit of Measure")
image = fields.Binary()
configurator_extra_price_formula = fields.Text(
string="Extra price formula",
help="Formula evaluated when computing "
"the extra price "
"for the custom value of this attribute.\n"
"The following variables are available:\n"
"- attribute: this attribute,\n"
"- config_session: the configuration session that configured the product,\n"
"- custom_value: the value provided by the user,\n"
"- product: the configured product,\n"
"The computed price "
"must be assigned to the `price` variable.",
)

@api.constrains(
"configurator_extra_price_formula",
)
def _constrain_configurator_extra_price_formula(self):
"""Check syntax of added formula for 'exec' evaluation."""
for attribute in self:
price_formula = attribute.configurator_extra_price_formula
if price_formula:
error_message = test_python_expr(
expr=price_formula,
mode="exec",
)
if error_message:
raise ValidationError(error_message)

Check warning on line 114 in product_configurator/models/product_attribute.py

View check run for this annotation

Codecov / codecov/patch

product_configurator/models/product_attribute.py#L114

Added line #L114 was not covered by tests

# TODO prevent the same attribute from being defined twice on the
# attribute lines
Expand Down
61 changes: 50 additions & 11 deletions product_configurator/models/product_config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging
from ast import literal_eval

from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools.misc import flatten, formatLang
from odoo.tools.safe_eval import safe_eval

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -332,6 +332,7 @@ class ProductConfigSession(models.Model):

@api.depends(
"value_ids",
"custom_value_ids.price",
"product_tmpl_id.list_price",
"product_tmpl_id.attribute_line_ids",
"product_tmpl_id.attribute_line_ids.value_ids",
Expand All @@ -358,12 +359,7 @@ def _get_custom_vals_dict(self):
{attribute_id: parsed_custom_value}"""
custom_vals = {}
for val in self.custom_value_ids:
if val.attribute_id.custom_type in ["float", "integer"]:
custom_vals[val.attribute_id.id] = literal_eval(val.value)
elif val.attribute_id.custom_type == "binary":
custom_vals[val.attribute_id.id] = val.attachment_ids
else:
custom_vals[val.attribute_id.id] = val.value
custom_vals[val.attribute_id.id] = val.eval()
return custom_vals

def _compute_config_step_name(self):
Expand Down Expand Up @@ -403,7 +399,7 @@ def get_cfg_weight(self, value_ids=None, custom_vals=None):
value_ids = self.value_ids.ids

if custom_vals is None:
custom_vals = {}
custom_vals = self._get_custom_vals_dict()

Check warning on line 402 in product_configurator/models/product_config.py

View check run for this annotation

Codecov / codecov/patch

product_configurator/models/product_config.py#L402

Added line #L402 was not covered by tests

product_tmpl = self.product_tmpl_id

Expand Down Expand Up @@ -827,20 +823,26 @@ def get_cfg_price(self, value_ids=None, custom_vals=None):
value_ids = self.value_ids.ids

if custom_vals is None:
custom_vals = {}
custom_vals = self._get_custom_vals_dict()

session_custom_values = self.custom_value_ids.filtered(
lambda cv: cv.attribute_id.id in custom_vals.keys()
)
custom_prices = session_custom_values.mapped("price")

price_extra = sum(custom_prices)

product_tmpl = self.product_tmpl_id
self = self.with_context(active_id=product_tmpl.id)

value_ids = self.flatten_val_ids(value_ids)

price_extra = 0.0
attr_val_obj = self.env["product.attribute.value"]
av_ids = attr_val_obj.browse(value_ids)
extra_prices = attr_val_obj.get_attribute_value_extra_prices(
product_tmpl_id=product_tmpl.id, pt_attr_value_ids=av_ids
)
price_extra = sum(extra_prices.values())
price_extra += sum(extra_prices.values())
return product_tmpl.list_price + price_extra

def _get_config_image(self, value_ids=None, custom_vals=None, size=None):
Expand Down Expand Up @@ -1608,6 +1610,43 @@ def _compute_val_name(self):
column2="attachment_id",
string="Attachments",
)
price = fields.Float(
compute="_compute_price",
help="Price computed using attribute's 'Extra price formula'.",
)

def _eval_price_formula_variables_dict(self):
"""Variables described in `product.attribute.configurator_extra_price_formula`."""
self.ensure_one()
return {
"attribute": self.attribute_id,
"config_session": self.cfg_session_id,
"custom_value": self.eval(),
"product": self.cfg_session_id.product_id,
}

def _eval_price_formula(self):
self.ensure_one()
price_formula = self.attribute_id.configurator_extra_price_formula
if price_formula:
variables_dict = self._eval_price_formula_variables_dict()
safe_eval(
price_formula,
globals_dict=variables_dict,
mode="exec",
nocopy=True,
)
price = variables_dict.get("price", 0)
else:
price = 0

Check warning on line 1641 in product_configurator/models/product_config.py

View check run for this annotation

Codecov / codecov/patch

product_configurator/models/product_config.py#L1641

Added line #L1641 was not covered by tests
return price

@api.depends(
"value",
)
def _compute_price(self):
for custom_value in self:
custom_value.price = custom_value._eval_price_formula()

def eval(self):
"""Return custom value evaluated using the related custom field type"""
Expand Down
1 change: 1 addition & 0 deletions product_configurator/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
# from . import test_product_attribute
# from . import test_product_config
# from . import test_wizard
from . import test_custom_attribute_price
112 changes: 112 additions & 0 deletions product_configurator/tests/test_custom_attribute_price.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Copyright 2024 Simone Rubino - Aion Tech
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo.fields import first
from odoo.tests import Form, TransactionCase
from odoo.tools.safe_eval import safe_eval


class TestCustomAttributePrice(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# The product attribute view only shows configuration fields
# (such as `val_custom`)
# when called with a specific context
# that is set by this action
configuration_attributes_action = cls.env.ref(
"product_configurator.action_attributes_view"
)
action_eval_context = configuration_attributes_action._get_eval_context()
configuration_attribute_context = safe_eval(
configuration_attributes_action.context, globals_dict=action_eval_context
)
configuration_attribute_model = cls.env["product.attribute"].with_context(
**configuration_attribute_context
)

custom_attribute_form = Form(configuration_attribute_model)
custom_attribute_form.name = "Test custom attribute"
custom_attribute_form.val_custom = True
cls.custom_attribute = custom_attribute_form.save()
cls.custom_attribute_value = cls.env.ref(
"product_configurator.custom_attribute_value"
)

regular_attribute_form = Form(configuration_attribute_model)
regular_attribute_form.name = "Test custom attribute"
regular_attribute_form.val_custom = False
with regular_attribute_form.value_ids.new() as value:
value.name = "Test value 1"
cls.regular_attribute = regular_attribute_form.save()

product_template_form = Form(cls.env["product.template"])
product_template_form.name = "Test configurable product"
with product_template_form.attribute_line_ids.new() as custom_line:
custom_line.attribute_id = cls.custom_attribute
with product_template_form.attribute_line_ids.new() as regular_line:
regular_line.attribute_id = cls.regular_attribute
regular_line.value_ids.add(first(cls.regular_attribute.value_ids))
product_template = product_template_form.save()
product_template.config_ok = True
cls.product_template = product_template

def test_integer_multiplier_formula(self):
"""The custom attribute has a formula `custom_value` * `multiplier`,
check that the configuration's price is computed correctly.
"""
# Arrange
regular_attribute = self.regular_attribute

multiplier = 5
custom_value = 3
custom_attribute = self.custom_attribute
custom_attribute.custom_type = "integer"
custom_attribute.configurator_extra_price_formula = (
"price = custom_value * %s" % multiplier
)
custom_attribute_value = self.custom_attribute_value

product_template = self.product_template

# Act: configure the product
wizard_action = product_template.configure_product()
wizard = self.env[wizard_action["res_model"]].browse(wizard_action["res_id"])
self.assertEqual(wizard.state, "select")
wizard.action_next_step()
self.assertEqual(wizard.state, "configure")
fields_prefixes = wizard._prefixes
field_prefix = fields_prefixes.get("field_prefix")
custom_field_prefix = fields_prefixes.get("custom_field_prefix")
wizard.write(
{
field_prefix
+ str(regular_attribute.id): first(regular_attribute.value_ids).id,
field_prefix + str(custom_attribute.id): custom_attribute_value.id,
custom_field_prefix + str(custom_attribute.id): custom_value,
}
)
wizard.action_config_done()

# Assert
configured_session = wizard.config_session_id
configured_custom_value = configured_session.custom_value_ids
self.assertEqual(configured_custom_value.price, custom_value * multiplier)

expected_configuration_price = (
product_template.list_price + configured_custom_value.price
)
self.assertEqual(configured_session.price, expected_configuration_price)

# Act: change the custom value
new_custom_value = 2
configured_custom_value.value = "%s" % new_custom_value

# Assert: the price has changed
new_expected_custom_price = new_custom_value * multiplier
self.assertEqual(configured_custom_value.price, new_expected_custom_price)

new_expected_configuration_price = (
product_template.list_price + configured_custom_value.price
)
self.assertEqual(configured_session.price, new_expected_configuration_price)
3 changes: 3 additions & 0 deletions product_configurator/views/product_attribute_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@
<group>
<field name="search_ok" />
</group>
<group>
<field name="configurator_extra_price_formula" />
</group>
</group>
</page>
</xpath>
Expand Down
1 change: 1 addition & 0 deletions product_configurator/views/product_config_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
<tree editable="bottom">
<field name="attribute_id" />
<field name="value" />
<field name="price" />
<field
name="attachment_ids"
widget="many2many_tags"
Expand Down
Loading

0 comments on commit e7e758e

Please sign in to comment.