<commit_before>from lxml import etree

from odoo.osv import orm
from odoo.addons.base.ir.ir_model import FIELD_TYPES

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


class FreeSelection(fields.Selection):

    def convert_to_cache(self, value, record, validate=True):
        return super(FreeSelection, self).convert_to_cache(
            value=value, record=record, validate=False)


class ProductConfigurator(models.TransientModel):
    _name = 'product.configurator'
    _inherits = {'product.config.session': 'config_session_id'}

    @property
    def _prefixes(self):
        """Return a dictionary with all dynamic field prefixes used to generate
        fields in the wizard. Any module extending this functionality should
        override this method to add all extra prefixes"""
        return {
            'field_prefix': '__attribute-',
            'custom_field_prefix': '__custom-',
        }

    # TODO: Remove _prefix suffix as this is implied by the class property name

    def _remove_dynamic_fields(self, fields):
        """Remove elements from the fields dictionary/list that begin with any
        prefix from the _prefixes property
            :param fields: list or dict of the form [fn1, fn2] / {fn1: val}
        """

        prefixes = self._prefixes.values()

        field_type = type(fields)

        if field_type == list:
            static_fields = []
        elif field_type == dict:
            static_fields = {}

        for field_name in fields:
            if any(prefix in field_name for prefix in prefixes):
                continue
            if field_type == list:
                static_fields.append(field_name)
            elif field_type == dict:
                static_fields[field_name] = fields[field_name]
        return static_fields

    @api.multi
    @api.depends('product_tmpl_id', 'value_ids', 'custom_value_ids')
    def _compute_cfg_image(self):
        # TODO: Update when allowing custom values to influence image

        cfg_sessions = self.config_session_id.with_context(bin_size=False)
        image = cfg_sessions.get_config_image()
        self.product_img = image

    @api.multi
    @api.depends('product_tmpl_id', 'product_tmpl_id.attribute_line_ids')
    def _compute_attr_lines(self):
        """Use compute method instead of related due to increased flexibility
        and strange behavior when attempting to have a related field point
        to computed values"""
        for configurator in self:
            attribute_lines = configurator.product_tmpl_id.attribute_line_ids
            configurator.attribute_line_ids = attribute_lines

    # TODO: We could use a m2o instead of a monkeypatched select field but
    # adding new steps should be trivial via custom development
    @api.multi
    def get_state_selection(self):
        """Get the states of the wizard using standard values and optional
        configuration steps set on the product.template via
        config_step_line_ids"""

        steps = [('select', "Select Template")]

        # Get the wizard id from context set via action_next_step method
        wizard_id = self.env.context.get('wizard_id')
        wiz = self.browse(wizard_id).exists()

        if not wiz:
            return steps

        open_lines = wiz.config_session_id.get_open_step_lines()

        if open_lines:
            open_steps = open_lines.mapped(
                lambda x: (str(x.id), x.config_step_id.name)
            )
            steps = open_steps if wiz.product_id else steps + open_steps
        else:
            steps.append(('configure', 'Configure'))
        return steps

    @api.onchange('product_tmpl_id')
    def onchange_product_tmpl(self):

        template = self.product_tmpl_id

        self.config_step_ids = template.config_step_line_ids.mapped(
            'config_step_id')

        # Set product preset if exist in session
        if template:
            session = self.env['product.config.session'].search_session(
                product_tmpl_id=template.id)
            self.product_preset_id = session.product_preset_id

        if self.value_ids:
            # TODO: Add confirmation button an delete cfg session
            raise UserError(
                _('Changing the product template while having an active '
                  'configuration will erase reset/clear all values')
            )

    def get_onchange_domains(self, values, cfg_val_ids):
        """Generate domains to be returned by onchange method in order
        to restrict the availble values of dynamically inserted fields

        :param values: values argument passed to onchance wrapper
        :cfg_val_ids: current configuration passed as a list of value_ids
        (usually in the form of db value_ids + interface value_ids)

        :returns: a dictionary of domains returned by onchance method
        """

        field_prefix = self._prefixes.get('field_prefix')

        domains = {}
        for line in self.product_tmpl_id.attribute_line_ids.sorted():
            field_name = field_prefix + str(line.attribute_id.id)

            if field_name not in values:
                continue

            vals = values[field_name]

            # get available values
            avail_ids = self.config_session_id.values_available(
                check_val_ids=line.value_ids.ids, value_ids=cfg_val_ids)
            domains[field_name] = [('id', 'in', avail_ids)]

            # Include custom value in the domain if attr line permits it
            if line.custom:
                custom_ext_id = 'product_configurator.custom_attribute_value'
                custom_val = self.env.ref(custom_ext_id)
                domains[field_name][0][2].append(custom_val.id)
                if line.multi and vals and custom_val.id in vals[0][2]:
                    continue
        return domains

    def get_onchange_vals(self, cfg_val_ids):
        """Onchange hook to add / modify returned values by onchange method"""
        # Remove None from cfg_val_ids if exist
        cfg_val_ids = [val for val in cfg_val_ids if val]

        product_img = self.config_session_id.get_config_image(cfg_val_ids)
        price = self.config_session_id.get_cfg_price(cfg_val_ids)
        weight = self.config_session_id.get_cfg_weight(value_ids=cfg_val_ids)

        return {
            'product_img': product_img,
            'value_ids': [(6, 0, cfg_val_ids)],
            'weight': weight,
            'price': price,
        }

    def get_form_vals(self, dynamic_fields, domains, cfg_val_ids=None):
        """Generate a dictionary to return new values via onchange method.
        Domains hold the values available, this method enforces these values
        if a selection exists in the view that is not available anymore.

        :param dynamic_fields: Dictionary with the current {dynamic_field: val}
        :param domains: Odoo domains restricting attribute values

        :returns vals: Dictionary passed to {'value': vals} by onchange method
        """

        vals = {}
        dynamic_fields = {k: v for k, v in dynamic_fields.items() if v}
        for k, v in dynamic_fields.items():
            if not v:
                continue
            available_val_ids = domains[k][0][2]
            if isinstance(v, list):
                if any(type(el) != int for el in v):
                    v = v[0][2]
                value_ids = list(set(v) & set(available_val_ids))
                dynamic_fields.update({k: value_ids})
                vals[k] = [[6, 0, value_ids]]
            elif v not in available_val_ids:
                dynamic_fields.update({k: None})
                vals[k] = None

        final_cfg_val_ids = list(dynamic_fields.values())

        vals.update(self.get_onchange_vals(final_cfg_val_ids))
        # To solve the Multi selection problem removing extra []
        if 'value_ids' in vals:
            val_ids = vals['value_ids'][0]
            vals['value_ids'] = [[val_ids[0], val_ids[1],
                                 tools.flatten(val_ids[2])]]

        return vals

    @api.multi
    def onchange(self, values, field_name, field_onchange):
        """ Override the onchange wrapper to return domains to dynamic
        fields as onchange isn't triggered for non-db fields
        """

        field_type = type(field_name)

        field_prefix = self._prefixes.get('field_prefix')

        if field_type == list or not field_name.startswith(field_prefix):
            res = super(ProductConfigurator, self).onchange(
                values, field_name, field_onchange)
            return res

        cfg_vals = self.value_ids

        view_val_ids = set()
        view_attribute_ids = set()

        try:
            cfg_step_id = int(self.state)
            cfg_step = self.product_tmpl_id.config_step_line_ids.filtered(
                lambda x: x.id == cfg_step_id)
        except Exception:
            cfg_step = self.env['product.config.step.line']

        dynamic_fields = {
            k: v for k, v in values.items() if k.startswith(field_prefix)
        }

        # Get the unstored values from the client view
        for k, v in dynamic_fields.items():
            attr_id = int(k.split(field_prefix)[1])
            # if isinstance(v, list):
            #    dynamic_fields[k] = v[0][2]
            line_attributes = cfg_step.attribute_line_ids.mapped(
                'attribute_id')
            if not cfg_step or attr_id in line_attributes.ids:
                view_attribute_ids.add(attr_id)
            else:
                continue
            if not v:
                continue
            if isinstance(v, list):
                view_val_ids |= set(v[0][2])
            elif isinstance(v, int):
                view_val_ids.add(v)

        # Clear all DB values belonging to attributes changed in the wizard
        cfg_vals = cfg_vals.filtered(
            lambda v: v.attribute_id.id not in view_attribute_ids
        )

        # Combine database values with wizard values_available
        cfg_val_ids = cfg_vals.ids + list(view_val_ids)

        domains = self.get_onchange_domains(values, cfg_val_ids)
        vals = self.get_form_vals(dynamic_fields, domains)
        return {'value': vals, 'domain': domains}

    config_session_id = fields.Many2one(
        required=True,
        ondelete='cascade',
        comodel_name='product.config.session',
        oldname='config_session',
        string='Configuration Session'
    )
    attribute_line_ids = fields.One2many(
        comodel_name='product.attribute.line',
        compute='_compute_attr_lines',
        string="Attributes",
        readonly=True,
        store=False
    )
    config_step_ids = fields.Many2many(
        comodel_name='product.config.step',
        relation="product_config_config_steps_rel",
        column1='config_wiz_id',
        column2='config_step_id',
        string="Configuration Steps",
        readonly=True,
        store=False
    )
    product_id = fields.Many2one(
        comodel_name='product.product',
        readonly=True,
        string='Product Variant',
        help='Set only when re-configuring a existing variant'
    )
    product_img = fields.Binary(
        compute='_compute_cfg_image',
        readonly=True
    )
    state = FreeSelection(
        selection='get_state_selection',
        default='select',
        string='State',
    )

    @api.onchange('state')
    def _onchange_state(self):
        if self.config_session_id:
            self.config_session_id.write({
                'value_ids': [[6, 0, self.value_ids.ids]],
                'config_step': self.state,

            })

    @api.onchange('product_preset_id')
    def _onchange_product_preset(self):
        self.value_ids = self.product_preset_id.attribute_value_ids

    @api.model
    def get_field_default_attrs(self):

        return {
            'company_dependent': False,
            'depends': (),
            'groups': False,
            'readonly': False,
            'manual': False,
            'required': False,
            'searchable': False,
            'store': False,
            'translate': False,
        }

    @api.model
    def fields_get(self, allfields=None, write_access=True, attributes=None):
        """ Artificially inject fields which are dynamically created using the
        attribute_ids on the product.template as reference"""

        field_prefix = self._prefixes.get('field_prefix')
        custom_field_prefix = self._prefixes.get('custom_field_prefix')

        res = super(ProductConfigurator, self).fields_get(
            allfields=allfields,
            attributes=attributes
        )

        wizard_id = self.env.context.get('wizard_id')

        # If wizard_id is not defined in the context then the wizard was just
        # launched and is not stored in the database yet
        if not wizard_id:
            return res
        # Get the wizard object from the database
        wiz = self.browse(wizard_id)
        active_step_id = wiz.state

        # If the product template is not set it is still at the 1st step
        if not wiz.product_tmpl_id:
            return res

        cfg_step_lines = wiz.product_tmpl_id.config_step_line_ids

        try:
            # Get only the attribute lines for the next step if defined
            active_step_line = cfg_step_lines.filtered(
                lambda l: l.id == int(active_step_id))
            if active_step_line:
                attribute_lines = active_step_line.attribute_line_ids
            else:
                attribute_lines = wiz.product_tmpl_id.attribute_line_ids
        except Exception:
            # If no configuration steps exist then get all attribute lines
            attribute_lines = wiz.product_tmpl_id.attribute_line_ids

        attribute_lines = wiz.product_tmpl_id.attribute_line_ids

        # Generate relational fields with domains restricting values to
        # the corresponding attributes

        # Default field attributes
        default_attrs = self.get_field_default_attrs()

        for line in attribute_lines:
            attribute = line.attribute_id
            value_ids = line.value_ids.ids

            value_ids = wiz.config_session_id.values_available(
                check_val_ids=value_ids
            )

            # If attribute lines allows custom values add the
            # generic "Custom" attribute.value to the list of options
            if line.custom:
                custom_ext_id = 'product_configurator.custom_attribute_value'
                custom_val = self.env.ref(custom_ext_id)
                value_ids.append(custom_val.id)

                # Set default field type
                field_type = 'char'

                if attribute.custom_type:
                    field_types = FIELD_TYPES
                    custom_type = line.attribute_id.custom_type
                    # TODO: Rename int to integer in values
                    if custom_type == 'int':
                        field_type = 'integer'
                    elif custom_type in [f[0] for f in field_types]:
                        field_type = custom_type

                # TODO: Implement custom string on custom attribute
                res[custom_field_prefix + str(attribute.id)] = dict(
                    default_attrs,
                    string="Custom",
                    type=field_type,
                    sequence=line.sequence,
                )

            # Add the dynamic field to the resultset using the convention
            # "__attribute-DBID" to later identify and extract it
            res[field_prefix + str(attribute.id)] = dict(
                default_attrs,
                type='many2many' if line.multi else 'many2one',
                domain=[('id', 'in', value_ids)],
                string=line.attribute_id.name,
                relation='product.attribute.value',
                sequence=line.sequence,
            )
        return res

    @api.model
    def fields_view_get(self, view_id=None, view_type='form',
                        toolbar=False, submenu=False):
        """ Generate view dynamically using attributes stored on the
        product.template"""
        if view_type == 'form' and not view_id:
            view_ext_id = 'product_configurator.product_configurator_form'
            view_id = self.env.ref(view_ext_id).id
        res = super(ProductConfigurator, self).fields_view_get(
            view_id=view_id, view_type=view_type,
            toolbar=toolbar, submenu=submenu
        )

        wizard_id = self.env.context.get('wizard_id')

        if res.get('type') != 'form' or not wizard_id:
            return res

        wiz = self.browse(wizard_id)

        # Get updated fields including the dynamic ones
        fields = self.fields_get()

        # Include all dynamic fields in the view
        dynamic_field_prefixes = tuple(self._prefixes.values())

        dynamic_fields = {
            k: v for k, v in fields.items() if k.startswith(
                dynamic_field_prefixes
            )
        }
        res['fields'].update(dynamic_fields)

        mod_view = self.add_dynamic_fields(res, dynamic_fields, wiz)

        # Update result dict from super with modified view
        res.update({'arch': etree.tostring(mod_view)})
        return res

    @api.model
    def add_dynamic_fields(self, res, dynamic_fields, wiz):
        """ Create the configuration view using the dynamically generated
            fields in fields_get()
        """
        field_prefix = self._prefixes.get('field_prefix')
        custom_field_prefix = self._prefixes.get('custom_field_prefix')

        try:
            # Search for view container hook and add dynamic view and fields
            xml_view = etree.fromstring(res['arch'])
            xml_static_form = xml_view.xpath(
                "//group[@name='static_form']")[0]
            xml_dynamic_form = etree.Element(
                'group',
                colspan='2',
                name='dynamic_form'
            )
            xml_parent = xml_static_form.getparent()
            xml_parent.insert(xml_parent.index(
                xml_static_form) + 1, xml_dynamic_form)
            xml_dynamic_form = xml_view.xpath(
                "//group[@name='dynamic_form']")[0]
        except Exception:<commit_msg>[T-00513][ADD] product_configurator_image_builder: add some changes for test-case<commit_after>from lxml import etree

from odoo.osv import orm
from odoo.addons.base.ir.ir_model import FIELD_TYPES

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


class FreeSelection(fields.Selection):

    def convert_to_cache(self, value, record, validate=True):
        return super(FreeSelection, self).convert_to_cache(
            value=value, record=record, validate=False)


class ProductConfigurator(models.TransientModel):
    _name = 'product.configurator'
    _inherits = {'product.config.session': 'config_session_id'}

    @property
    def _prefixes(self):
        """Return a dictionary with all dynamic field prefixes used to generate
        fields in the wizard. Any module extending this functionality should
        override this method to add all extra prefixes"""
        return {
            'field_prefix': '__attribute-',
            'custom_field_prefix': '__custom-',
        }

    # TODO: Remove _prefix suffix as this is implied by the class property name

    def _remove_dynamic_fields(self, fields):
        """Remove elements from the fields dictionary/list that begin with any
        prefix from the _prefixes property
            :param fields: list or dict of the form [fn1, fn2] / {fn1: val}
        """

        prefixes = self._prefixes.values()

        field_type = type(fields)

        if field_type == list:
            static_fields = []
        elif field_type == dict:
            static_fields = {}

        for field_name in fields:
            if any(prefix in field_name for prefix in prefixes):
                continue
            if field_type == list:
                static_fields.append(field_name)
            elif field_type == dict:
                static_fields[field_name] = fields[field_name]
        return static_fields

    @api.multi
    @api.depends('product_tmpl_id', 'value_ids', 'custom_value_ids')
    def _compute_cfg_image(self):
        # TODO: Update when allowing custom values to influence image

        cfg_sessions = self.config_session_id.with_context(bin_size=False)
        image = cfg_sessions.get_config_image()
        self.product_img = image

    @api.multi
    @api.depends('product_tmpl_id', 'product_tmpl_id.attribute_line_ids')
    def _compute_attr_lines(self):
        """Use compute method instead of related due to increased flexibility
        and strange behavior when attempting to have a related field point
        to computed values"""
        for configurator in self:
            attribute_lines = configurator.product_tmpl_id.attribute_line_ids
            configurator.attribute_line_ids = attribute_lines

    # TODO: We could use a m2o instead of a monkeypatched select field but
    # adding new steps should be trivial via custom development
    @api.multi
    def get_state_selection(self):
        """Get the states of the wizard using standard values and optional
        configuration steps set on the product.template via
        config_step_line_ids"""

        steps = [('select', "Select Template")]

        # Get the wizard id from context set via action_next_step method
        wizard_id = self.env.context.get('wizard_id')
        wiz = self.browse(wizard_id).exists()

        if not wiz:
            return steps

        open_lines = wiz.config_session_id.get_open_step_lines()

        if open_lines:
            open_steps = open_lines.mapped(
                lambda x: (str(x.id), x.config_step_id.name)
            )
            steps = open_steps if wiz.product_id else steps + open_steps
        else:
            steps.append(('configure', 'Configure'))
        return steps

    @api.onchange('product_tmpl_id')
    def onchange_product_tmpl(self):

        template = self.product_tmpl_id

        self.config_step_ids = template.config_step_line_ids.mapped(
            'config_step_id')

        # Set product preset if exist in session
        if template:
            session = self.env['product.config.session'].search_session(
                product_tmpl_id=template.id)
            self.product_preset_id = session.product_preset_id

        if self.value_ids:
            # TODO: Add confirmation button an delete cfg session
            raise UserError(
                _('Changing the product template while having an active '
                  'configuration will erase reset/clear all values')
            )

    def get_onchange_domains(self, values, cfg_val_ids):
        """Generate domains to be returned by onchange method in order
        to restrict the availble values of dynamically inserted fields

        :param values: values argument passed to onchance wrapper
        :cfg_val_ids: current configuration passed as a list of value_ids
        (usually in the form of db value_ids + interface value_ids)

        :returns: a dictionary of domains returned by onchance method
        """

        field_prefix = self._prefixes.get('field_prefix')

        domains = {}
        for line in self.product_tmpl_id.attribute_line_ids.sorted():
            field_name = field_prefix + str(line.attribute_id.id)

            if field_name not in values:
                continue

            vals = values[field_name]

            # get available values
            avail_ids = self.config_session_id.values_available(
                check_val_ids=line.value_ids.ids, value_ids=cfg_val_ids)
            domains[field_name] = [('id', 'in', avail_ids)]

            # Include custom value in the domain if attr line permits it
            if line.custom:
                custom_ext_id = 'product_configurator.custom_attribute_value'
                custom_val = self.env.ref(custom_ext_id)
                domains[field_name][0][2].append(custom_val.id)
                if line.multi and vals and custom_val.id in vals[0][2]:
                    continue
        return domains

    def get_onchange_vals(self, cfg_val_ids):
        """Onchange hook to add / modify returned values by onchange method"""
        # Remove None from cfg_val_ids if exist
        cfg_val_ids = [val for val in cfg_val_ids if val]

        product_img = self.config_session_id.get_config_image(cfg_val_ids)
        price = self.config_session_id.get_cfg_price(cfg_val_ids)
        weight = self.config_session_id.get_cfg_weight(value_ids=cfg_val_ids)

        return {
            'product_img': product_img,
            'value_ids': [(6, 0, cfg_val_ids)],
            'weight': weight,
            'price': price,
        }

    def get_form_vals(self, dynamic_fields, domains, cfg_val_ids=None):
        """Generate a dictionary to return new values via onchange method.
        Domains hold the values available, this method enforces these values
        if a selection exists in the view that is not available anymore.

        :param dynamic_fields: Dictionary with the current {dynamic_field: val}
        :param domains: Odoo domains restricting attribute values

        :returns vals: Dictionary passed to {'value': vals} by onchange method
        """

        vals = {}
        dynamic_fields = {k: v for k, v in dynamic_fields.items() if v}
        for k, v in dynamic_fields.items():
            if not v:
                continue
            available_val_ids = domains[k][0][2]
            if isinstance(v, list):
                if any(type(el) != int for el in v):
                    v = v[0][2]
                value_ids = list(set(v) & set(available_val_ids))
                dynamic_fields.update({k: value_ids})
                vals[k] = [[6, 0, value_ids]]
            elif v not in available_val_ids:
                dynamic_fields.update({k: None})
                vals[k] = None

        final_cfg_val_ids = list(dynamic_fields.values())

        vals.update(self.get_onchange_vals(final_cfg_val_ids))
        # To solve the Multi selection problem removing extra []
        if 'value_ids' in vals:
            val_ids = vals['value_ids'][0]
            vals['value_ids'] = [[val_ids[0], val_ids[1],
                                 tools.flatten(val_ids[2])]]

        return vals

    @api.multi
    def onchange(self, values, field_name, field_onchange):
        """ Override the onchange wrapper to return domains to dynamic
        fields as onchange isn't triggered for non-db fields
        """

        field_type = type(field_name)

        field_prefix = self._prefixes.get('field_prefix')

        if field_type == list or not field_name.startswith(field_prefix):
            res = super(ProductConfigurator, self).onchange(
                values, field_name, field_onchange)
            return res

        cfg_vals = self.value_ids

        view_val_ids = set()
        view_attribute_ids = set()

        try:
            cfg_step_id = int(self.state)
            cfg_step = self.product_tmpl_id.config_step_line_ids.filtered(
                lambda x: x.id == cfg_step_id)
        except Exception:
            cfg_step = self.env['product.config.step.line']

        dynamic_fields = {
            k: v for k, v in values.items() if k.startswith(field_prefix)
        }

        # Get the unstored values from the client view
        for k, v in dynamic_fields.items():
            attr_id = int(k.split(field_prefix)[1])
            # if isinstance(v, list):
            #    dynamic_fields[k] = v[0][2]
            line_attributes = cfg_step.attribute_line_ids.mapped(
                'attribute_id')
            if not cfg_step or attr_id in line_attributes.ids:
                view_attribute_ids.add(attr_id)
            else:
                continue
            if not v:
                continue
            if isinstance(v, list):
                view_val_ids |= set(v[0][2])
            elif isinstance(v, int):
                view_val_ids.add(v)

        # Clear all DB values belonging to attributes changed in the wizard
        cfg_vals = cfg_vals.filtered(
            lambda v: v.attribute_id.id not in view_attribute_ids
        )

        # Combine database values with wizard values_available
        cfg_val_ids = cfg_vals.ids + list(view_val_ids)

        domains = self.get_onchange_domains(values, cfg_val_ids)
        vals = self.get_form_vals(dynamic_fields, domains)
        return {'value': vals, 'domain': domains}

    config_session_id = fields.Many2one(
        required=True,
        ondelete='cascade',
        comodel_name='product.config.session',
        oldname='config_session',
        string='Configuration Session'
    )
    attribute_line_ids = fields.One2many(
        comodel_name='product.attribute.line',
        compute='_compute_attr_lines',
        string="Attributes",
        readonly=True,
        store=False
    )
    config_step_ids = fields.Many2many(
        comodel_name='product.config.step',
        relation="product_config_config_steps_rel",
        column1='config_wiz_id',
        column2='config_step_id',
        string="Configuration Steps",
        readonly=True,
        store=False
    )
    product_id = fields.Many2one(
        comodel_name='product.product',
        readonly=True,
        string='Product Variant',
        help='Set only when re-configuring a existing variant'
    )
    product_img = fields.Binary(
        compute='_compute_cfg_image',
        readonly=True
    )
    state = FreeSelection(
        selection='get_state_selection',
        default='select',
        string='State',
    )

    @api.onchange('state')
    def _onchange_state(self):
        if self.config_session_id:
            self.config_session_id.write({
                'value_ids': [[6, 0, self.value_ids.ids]],
                'config_step': self.state,

            })

    @api.onchange('product_preset_id')
    def _onchange_product_preset(self):
        self.value_ids = self.product_preset_id.attribute_value_ids

    @api.model
    def get_field_default_attrs(self):

        return {
            'company_dependent': False,
            'depends': (),
            'groups': False,
            'readonly': False,
            'manual': False,
            'required': False,
            'searchable': False,
            'store': False,
            'translate': False,
        }

    @api.model
    def fields_get(self, allfields=None, write_access=True, attributes=None):
        """ Artificially inject fields which are dynamically created using the
        attribute_ids on the product.template as reference"""

        field_prefix = self._prefixes.get('field_prefix')
        custom_field_prefix = self._prefixes.get('custom_field_prefix')

        res = super(ProductConfigurator, self).fields_get(
            allfields=allfields,
            attributes=attributes
        )

        wizard_id = self.env.context.get('wizard_id')

        # If wizard_id is not defined in the context then the wizard was just
        # launched and is not stored in the database yet
        if not wizard_id:
            return res

        # Get the wizard object from the database
        wiz = self.browse(wizard_id)
        active_step_id = wiz.state

        # If the product template is not set it is still at the 1st step
        if not wiz.product_tmpl_id:
            return res

        cfg_step_lines = wiz.product_tmpl_id.config_step_line_ids

        try:
            # Get only the attribute lines for the next step if defined
            active_step_line = cfg_step_lines.filtered(
                lambda l: l.id == int(active_step_id))
            if active_step_line:
                attribute_lines = active_step_line.attribute_line_ids
            else:
                attribute_lines = wiz.product_tmpl_id.attribute_line_ids
        except Exception:
            # If no configuration steps exist then get all attribute lines
            attribute_lines = wiz.product_tmpl_id.attribute_line_ids

        attribute_lines = wiz.product_tmpl_id.attribute_line_ids

        # Generate relational fields with domains restricting values to
        # the corresponding attributes

        # Default field attributes
        default_attrs = self.get_field_default_attrs()

        for line in attribute_lines:
            attribute = line.attribute_id
            value_ids = line.value_ids.ids

            value_ids = wiz.config_session_id.values_available(
                check_val_ids=value_ids
            )

            # If attribute lines allows custom values add the
            # generic "Custom" attribute.value to the list of options
            if line.custom:
                custom_ext_id = 'product_configurator.custom_attribute_value'
                custom_val = self.env.ref(custom_ext_id)
                value_ids.append(custom_val.id)

                # Set default field type
                field_type = 'char'

                if attribute.custom_type:
                    field_types = FIELD_TYPES
                    custom_type = line.attribute_id.custom_type
                    # TODO: Rename int to integer in values
                    if custom_type == 'int':
                        field_type = 'integer'
                    elif custom_type in [f[0] for f in field_types]:
                        field_type = custom_type

                # TODO: Implement custom string on custom attribute
                res[custom_field_prefix + str(attribute.id)] = dict(
                    default_attrs,
                    string="Custom",
                    type=field_type,
                    sequence=line.sequence,
                )

            # Add the dynamic field to the resultset using the convention
            # "__attribute-DBID" to later identify and extract it
            res[field_prefix + str(attribute.id)] = dict(
                default_attrs,
                type='many2many' if line.multi else 'many2one',
                domain=[('id', 'in', value_ids)],
                string=line.attribute_id.name,
                relation='product.attribute.value',
                sequence=line.sequence,
            )
        return res

    @api.model
    def fields_view_get(self, view_id=None, view_type='form',
                        toolbar=False, submenu=False):
        """ Generate view dynamically using attributes stored on the
        product.template"""

        if view_type == 'form' and not view_id:
            view_ext_id = 'product_configurator.product_configurator_form'
            view_id = self.env.ref(view_ext_id).id
        res = super(ProductConfigurator, self).fields_view_get(
            view_id=view_id, view_type=view_type,
            toolbar=toolbar, submenu=submenu
        )

        wizard_id = self.env.context.get('wizard_id')

        if res.get('type') != 'form' or not wizard_id:
            return res

        wiz = self.browse(wizard_id)

        # Get updated fields including the dynamic ones
        fields = self.fields_get()

        # Include all dynamic fields in the view
        dynamic_field_prefixes = tuple(self._prefixes.values())

        dynamic_fields = {
            k: v for k, v in fields.items() if k.startswith(
                dynamic_field_prefixes
            )
        }
        res['fields'].update(dynamic_fields)

        mod_view = self.add_dynamic_fields(res, dynamic_fields, wiz)

        # Update result dict from super with modified view
        res.update({'arch': etree.tostring(mod_view)})
        return res

    @api.model
    def add_dynamic_fields(self, res, dynamic_fields, wiz):
        """ Create the configuration view using the dynamically generated
            fields in fields_get()
        """

        field_prefix = self._prefixes.get('field_prefix')
        custom_field_prefix = self._prefixes.get('custom_field_prefix')

        try:
            # Search for view container hook and add dynamic view and fields
            xml_view = etree.fromstring(res['arch'])
            xml_static_form = xml_view.xpath(
                "//group[@name='static_form']")[0]
            xml_dynamic_form = etree.Element(
                'group',
                colspan='2',
                name='dynamic_form'
            )
            xml_parent = xml_static_form.getparent()
            xml_parent.insert(xml_parent.index(
                xml_static_form) + 1, xml_dynamic_form)
            xml_dynamic_form = xml_view.xpath(
                "//group[@name='dynamic_form']")[0]
        except Exception:
{'fraction_non_alphanumeric': 0.07952475462874854, 'fraction_numerical': 0.002365351676137136, 'mean_word_length': 2.099780886566661, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 26}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5623137', 'n_tokens_mistral': 10347, 'n_tokens_neox': 9911, 'n_words': 2607}