348 lines
16 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from odoo.http import Controller, request, route
class ProductConfiguratorController(Controller):
@route('/sale_product_configurator/get_values', type='json', auth='user')
def get_product_configurator_values(
self,
product_template_id,
quantity,
currency_id,
so_date,
product_uom_id=None,
company_id=None,
pricelist_id=None,
ptav_ids=None,
only_main_product=False,
):
""" Return all product information needed for the product configurator.
:param int product_template_id: The product for which to seek information, as a
`product.template` id.
:param int quantity: The quantity of the product.
:param int currency_id: The currency of the transaction, as a `res.currency` id.
:param str so_date: The date of the `sale.order`, to compute the price at the right rate.
:param int|None product_uom_id: The unit of measure of the product, as a `uom.uom` id.
:param int|None company_id: The company to use, as a `res.company` id.
:param int|None pricelist_id: The pricelist to use, as a `product.pricelist` id.
:param recordset|None product_template_attribute_value_ids: The combination of the product,
as a `product.template.attribute
.value` recordset.
:param bool only_main_product: Whether the optional products should be included or not.
:rtype: dict
:return: A dict containing a list of products and a list of optional products information,
generated by :meth:`_get_product_information`.
"""
if company_id:
request.update_context(allowed_company_ids=[company_id])
product_template = request.env['product.template'].browse(product_template_id)
combination = request.env['product.template.attribute.value']
if ptav_ids:
combination = request.env['product.template.attribute.value'].browse(ptav_ids).filtered(
lambda ptav: ptav.product_tmpl_id.id == product_template_id
)
# Set missing attributes (unsaved no_variant attributes, or new attribute on existing product)
unconfigured_ptals = (
product_template.attribute_line_ids - combination.attribute_line_id).filtered(
lambda ptal: ptal.attribute_id.display_type != 'multi')
combination += unconfigured_ptals.mapped(
lambda ptal: ptal.product_template_value_ids._only_active()[:1]
)
if not combination:
combination = product_template._get_first_possible_combination()
return dict(
products=[
dict(
**self._get_product_information(
product_template,
combination,
currency_id,
so_date,
quantity=quantity,
product_uom_id=product_uom_id,
pricelist_id=pricelist_id,
),
parent_product_tmpl_ids=[],
)
],
optional_products=[
dict(
**self._get_product_information(
optional_product_template,
optional_product_template._get_first_possible_combination(
parent_combination=combination
),
currency_id,
so_date,
# giving all the ptav of the parent product to get all the exclusions
parent_combination=product_template.attribute_line_ids.\
product_template_value_ids,
pricelist_id=pricelist_id,
),
parent_product_tmpl_ids=[product_template.id],
) for optional_product_template in product_template.optional_product_ids
] if not only_main_product else []
)
@route('/sale_product_configurator/create_product', type='json', auth='user')
def sale_product_configurator_create_product(self, product_template_id, combination):
""" Create the product when there is a dynamic attribute in the combination.
:param int product_template_id: The product for which to seek information, as a
`product.template` id.
:param recordset combination: The combination of the product, as a
`product.template.attribute.value` recordset.
:rtype: int
:return: The product created, as a `product.product` id.
"""
product_template = request.env['product.template'].browse(product_template_id)
combination = request.env['product.template.attribute.value'].browse(combination)
product = product_template._create_product_variant(combination)
return product.id
@route('/sale_product_configurator/update_combination', type='json', auth='user')
def sale_product_configurator_update_combination(
self,
product_template_id,
combination,
currency_id,
so_date,
quantity,
product_uom_id=None,
company_id=None,
pricelist_id=None,
):
""" Return the updated combination information.
:param int product_template_id: The product for which to seek information, as a
`product.template` id.
:param recordset combination: The combination of the product, as a
`product.template.attribute.value` recordset.
:param int currency_id: The currency of the transaction, as a `res.currency` id.
:param str so_date: The date of the `sale.order`, to compute the price at the right rate.
:param int quantity: The quantity of the product.
:param int|None product_uom_id: The unit of measure of the product, as a `uom.uom` id.
:param int|None company_id: The company to use, as a `res.company` id.
:param int|None pricelist_id: The pricelist to use, as a `product.pricelist` id.
:rtype: dict
:return: Basic informations about a product, generated by
:meth:`_get_basic_product_information`.
"""
if company_id:
request.update_context(allowed_company_ids=[company_id])
product_template = request.env['product.template'].browse(product_template_id)
pricelist = request.env['product.pricelist'].browse(pricelist_id)
product_uom = request.env['uom.uom'].browse(product_uom_id)
currency = request.env['res.currency'].browse(currency_id)
combination = request.env['product.template.attribute.value'].browse(combination)
product = product_template._get_variant_for_combination(combination)
return self._get_basic_product_information(
product or product_template,
pricelist,
combination,
quantity=quantity or 0.0,
uom=product_uom,
currency=currency,
date=datetime.fromisoformat(so_date),
)
@route('/sale_product_configurator/get_optional_products', type='json', auth='user')
def sale_product_configurator_get_optional_products(
self,
product_template_id,
combination,
parent_combination,
currency_id,
so_date,
company_id=None,
pricelist_id=None,
):
""" Return information about optional products for the given `product.template`.
:param int product_template_id: The product for which to seek information, as a
`product.template` id.
:param recordset combination: The combination of the product, as a
`product.template.attribute.value` recordset.
:param recordset parent_combination: The combination of the parent product, as a
`product.template.attribute.value` recordset.
:param int currency_id: The currency of the transaction, as a `res.currency` id.
:param str so_date: The date of the `sale.order`, to compute the price at the right rate.
:param int|None company_id: The company to use, as a `res.company` id.
:param int|None pricelist_id: The pricelist to use, as a `product.pricelist` id.
:rtype: [dict]
:return: A list of optional products information, generated by
:meth:`_get_product_information`.
"""
if company_id:
request.update_context(allowed_company_ids=[company_id])
product_template = request.env['product.template'].browse(product_template_id)
parent_combination = request.env['product.template.attribute.value'].browse(
parent_combination + combination
)
return [
dict(
**self._get_product_information(
optional_product_template,
optional_product_template._get_first_possible_combination(
parent_combination=parent_combination
),
currency_id,
so_date,
parent_combination=parent_combination,
pricelist_id=pricelist_id,
),
parent_product_tmpl_ids=[product_template.id],
) for optional_product_template in product_template.optional_product_ids
]
def _get_product_information(
self,
product_template,
combination,
currency_id,
so_date,
quantity=1,
product_uom_id=None,
pricelist_id=None,
parent_combination=None,
):
""" Return complete information about a product.
:param recordset product_template: The product for which to seek information, as a
`product.template` record.
:param recordset combination: The combination of the product, as a
`product.template.attribute.value` recordset.
:param int currency_id: The currency of the transaction, as a `res.currency` id.
:param str so_date: The date of the `sale.order`, to compute the price at the right rate.
:param int quantity: The quantity of the product.
:param int|None product_uom_id: The unit of measure of the product, as a `uom.uom` id.
:param int|None pricelist_id: The pricelist to use, as a `product.pricelist` id.
:param recordset|None parent_combination: The combination of the parent product, as a
`product.template.attribute.value` recordset.
:rtype: dict
:return: A dict with the following structure:
{
'product_tmpl_id': int,
'id': int,
'description_sale': str|False,
'display_name': str,
'price': float,
'quantity': int
'attribute_line': [{
'id': int
'attribute': {
'id': int
'name': str
'display_type': str
},
'attribute_value': [{
'id': int,
'name': str,
'price_extra': float,
'html_color': str|False,
'image': str|False,
'is_custom': bool
}],
'selected_attribute_id': int,
}],
'exclusions': dict,
'archived_combination': dict,
'parent_exclusions': dict,
}
"""
pricelist = request.env['product.pricelist'].browse(pricelist_id)
product_uom = request.env['uom.uom'].browse(product_uom_id)
currency = request.env['res.currency'].browse(currency_id)
product = product_template._get_variant_for_combination(combination)
attribute_exclusions = product_template._get_attribute_exclusions(
parent_combination=parent_combination,
combination_ids=combination.ids,
)
return dict(
product_tmpl_id=product_template.id,
**self._get_basic_product_information(
product or product_template,
pricelist,
combination,
quantity=quantity,
uom=product_uom,
currency=currency,
date=datetime.fromisoformat(so_date),
),
quantity=quantity,
attribute_lines=[dict(
id=ptal.id,
attribute=dict(**ptal.attribute_id.read(['id', 'name', 'display_type'])[0]),
attribute_values=[
dict(
**ptav.read(['name', 'html_color', 'image', 'is_custom'])[0],
price_extra=ptav.currency_id._convert(
ptav.price_extra,
currency,
request.env.company,
datetime.fromisoformat(so_date).date(),
),
) for ptav in ptal.product_template_value_ids
if ptav.ptav_active or combination and ptav.id in combination.ids
],
selected_attribute_value_ids=combination.filtered(
lambda c: ptal in c.attribute_line_id
).ids,
create_variant=ptal.attribute_id.create_variant,
) for ptal in product_template.attribute_line_ids],
exclusions=attribute_exclusions['exclusions'],
archived_combinations=attribute_exclusions['archived_combinations'],
parent_exclusions=attribute_exclusions['parent_exclusions'],
)
def _get_basic_product_information(self, product_or_template, pricelist, combination, **kwargs):
""" Return basic information about a product
:param recordset product_or_template: The product for which to seek information, as a
`product.product` or `product.template` record.
:param recordset|None pricelist: The pricelist to use, as a `product.pricelist` record.
:param recordset combination: The combination of the product, as a
`product.template.attribute.value` recordset.
:param dict kwargs: Locally unused data passed to `_get_product_price`
:rtype: dict
:return: A dict with the following structure::
{
'id': int, # if product_or_template is a record of `product.product`.
'description_sale': str|False,
'display_name': str,
'price': float,
'quantity': int,
}
"""
basic_information = dict(
**product_or_template.read(['description_sale', 'display_name'])[0]
)
# If the product is a template, check the combination to compute the name to take dynamic
# and no_variant attributes into account. Also, drop the id which was auto-included by the
# search but isn't relevant since it is supposed to be the id of a `product.product` record.
if not product_or_template.is_product_variant:
basic_information['id'] = False
combination_name = combination._get_combination_name()
if combination_name:
basic_information.update(
display_name=f"{basic_information['display_name']} ({combination_name})"
)
return dict(
**basic_information,
price=pricelist._get_product_price(
product_or_template.with_context(
**product_or_template._get_product_price_context(combination)
),
**kwargs,
),
)