¡Bienvenido a los foros Aeodoo!

Somos la comunidad de Odoo internacional hispanohablante.
Estos foros son para compartir y debatir dudas técnicas, funcionales y mejores prácticas para Odoo. Recuerda que no están permitidos los insultos, descalificaciones o spam, cualquier conducta reprobable supondrá el baneo del usuario.

This question has been flagged
135 Views

Buen día. Soy desarrollador Front-End en Odoo 17 Enterprise (no tenemos acceso al Back-End). Estoy intentando implementar una regla de automatización (Ejecutar Código) que busca crear una variante nueva de producto, para cada producto especificado en la Linea de la Orden de Venta, con base en los valores que se introduzcan en los siguientes campos: densidad, largo, ancho y alto. La regla se lleva a cabo cada vez que se guarda la cotización/órden de venta. Estoy obteniendo el error especificado en el título que, a lo que tengo entendido, significa que estoy intentando crear una variante que ya existe. Comparto mi código:

for record in records:
    product_tmpl = record.product_id.product_tmpl_id
   
    # 1. Obtener (o crear) los atributos
    def get_or_create_attribute(name):
        attr = env['product.attribute'].search([('name', '=', name)], limit=1)
        if not attr:
            attr = env['product.attribute'].create({
                'name': name,
                'display_type': 'select', 
                'create_variant': 'dynamic', 
            })
        return attr
   
    attribute_densidad = get_or_create_attribute('ProdTD')
    attribute_largo = get_or_create_attribute('ProdTL')
    attribute_ancho = get_or_create_attribute('ProdTA')
    attribute_alto  = get_or_create_attribute('ProdTP')
   
    # 2. Crear u obtener valores
    def get_or_create_attribute_value(attribute, value):
        val_str = str(value)
        val = env['product.attribute.value'].search([
            ('attribute_id', '=', attribute.id),
            ('name', '=', val_str),
        ], limit=1)
        if val:
            return val
        else:
            return env['product.attribute.value'].create({
                'attribute_id': attribute.id,
                'name': val_str,
               
            })
   
    val_densidad = get_or_create_attribute_value(attribute_densidad, record.production_density)
    val_largo = get_or_create_attribute_value(attribute_largo, record.length)
    val_ancho = get_or_create_attribute_value(attribute_ancho, record.width)
    val_alto  = get_or_create_attribute_value(attribute_alto, record.camber)
   
​# Crear valores para permitir al usuario de Ventas que configure la variante en la Línea ​# de la Órden de Venta
    cfg = 'Configurar en la Línea de la Orden'
    config_densidad = get_or_create_attribute_value(attribute_densidad, cfg)
    config_largo = get_or_create_attribute_value(attribute_largo, cfg)
    config_ancho = get_or_create_attribute_value(attribute_ancho, cfg)
    config_alto = get_or_create_attribute_value(attribute_alto, cfg)
   
   
    # 3. Asegurar que el template tenga las líneas de atributo
    def ensure_attribute_line(template, attribute, value):
        line = env['product.template.attribute.line'].search([
            ('product_tmpl_id', '=', template.id),
            ('attribute_id', '=', attribute.id),
        ], limit=1)
        if not line:
            line = env['product.template.attribute.line'].create({
                'product_tmpl_id': template.id,
                'attribute_id': attribute.id,
                'value_ids': [(6, 0, [value.id])],
            })
        elif value.id not in line.value_ids.ids:
            line.write({'value_ids': [(4, value.id, False)]})
        return line
   
   
    line_densidad = ensure_attribute_line(product_tmpl, attribute_densidad, val_densidad)
    line_largo = ensure_attribute_line(product_tmpl, attribute_largo, val_largo)
    line_ancho = ensure_attribute_line(product_tmpl, attribute_ancho, val_ancho)
    line_alto = ensure_attribute_line(product_tmpl, attribute_alto, val_alto)
   
    line_cfg_densidad = ensure_attribute_line(product_tmpl, attribute_densidad, config_densidad)
    line_cfg_largo = ensure_attribute_line(product_tmpl, attribute_largo, config_largo)
    line_cfg_ancho = ensure_attribute_line(product_tmpl, attribute_ancho, config_ancho)
    line_cfg_alto = ensure_attribute_line(product_tmpl, attribute_alto, config_alto)
   
    # ———————————————————————————————————————
    # 1) Variante “normal”: todos los atributos con sus valores reales
    # ———————————————————————————————————————
   
    # 1.1) IDs de los PTAV correspondientes a cada valor real
    real_ptav_ids = []
    for line_id, pav_id in zip(
        [line_densidad.id, line_largo.id, line_ancho.id, line_alto.id],
        [val_densidad.id,   val_largo.id,   val_ancho.id,   val_alto.id]
    ):
        ptav = env['product.template.attribute.value'].search([
            ('attribute_line_id',          '=', line_id),
            ('product_attribute_value_id', '=', pav_id),
        ], limit=1)
        if not ptav:
            raise UserError(f"No PTAV para línea {line_id} y valor {pav_id}")
        real_ptav_ids.append(ptav.id)
   
    # 1.2) Buscar en BD cualquier variante que contenga todos esos PTAVs
    domain = [('product_tmpl_id', '=', product_tmpl.id)]
    for ptav_id in real_ptav_ids:
        domain.append(('product_template_variant_value_ids', 'in', ptav_id))
    candidates = env['product.product'].search(domain)
   
    # 1.3) Filtrar la variante que tenga exactamente ese conjunto de PTAVs
    variant = False
    target = set(real_ptav_ids)
    for p in candidates:
        if set(p.product_template_variant_value_ids.ids) == target:
            variant = p
            break

    # 1.4) Crear sólo si no existe
    if not variant:
        variant = env['product.product'].create({
            'product_tmpl_id': product_tmpl.id,
            'product_template_variant_value_ids': [(6, 0, real_ptav_ids)],
        })
       
    # 1.5) Asignar al registro de la cotización
    try:
        record['product_id'] = variant
    except Exception:
        record['x_studio_debug_2'] = 'No se puede asignar variante'

Ya confirmé que los ids correspondientes a product.template.attribute.value se generan y capturan de manera correcta. También ya confirmé que no existe ningun registro en product.product que en su campo product_template_variant_value_ids tenga exactamente los product.template.attribute.value ids de la combinación que estoy intentando generar. ¿Cuál es el error en mi lógica?. A continuación pego el error completo que estoy obteniendo:

RPC_ERROR
Odoo Server Error
Traceback (most recent call last):
  File "/home/odoo/src/odoo/odoo/tools/safe_eval.py", line 399, in safe_eval
    return unsafe_eval(c, globals_dict, locals_dict)
  File "ir.actions.server(1326,)", line 78, in <module>
  File "ir.actions.server(1326,)", line 73, in ensure_attribute_line
  File "/home/odoo/src/odoo/addons/product/models/product_template_attribute_line.py", line 149, in write
    self._update_product_template_attribute_values()
  File "/home/odoo/src/odoo/addons/product/models/product_template_attribute_line.py", line 250, in _update_product_template_attribute_values
    self.product_tmpl_id._create_variant_ids()
  File "/home/odoo/src/odoo/addons/product/models/product_template.py", line 730, in _create_variant_ids
    self.env.flush_all()
  File "/home/odoo/src/odoo/odoo/api.py", line 739, in flush_all
    self[model_name].flush_model()
  File "/home/odoo/src/odoo/odoo/models.py", line 6362, in flush_model
    self._flush(fnames)
  File "/home/odoo/src/odoo/odoo/models.py", line 6464, in _flush
    model.browse(ids)._write(vals)
  File "/home/odoo/src/odoo/odoo/models.py", line 4548, in _write
    cr.execute(SQL(
  File "/home/odoo/src/odoo/odoo/sql_db.py", line 332, in execute
    res = self._obj.execute(query, params)
psycopg2.errors.UniqueViolation: duplicate key value violates unique constraint "product_product_combination_unique"
DETAIL:  Key (product_tmpl_id, combination_indices)=(59686, ) already exists.


During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/odoo/src/odoo/odoo/http.py", line 1788, in _serve_db
    return service_model.retrying(self._serve_ir_http, self.env)
  File "/home/odoo/src/odoo/odoo/service/model.py", line 152, in retrying
    result = func()
  File "/home/odoo/src/odoo/odoo/http.py", line 1816, in _serve_ir_http
    response = self.dispatcher.dispatch(rule.endpoint, args)
  File "/home/odoo/src/odoo/odoo/http.py", line 2020, in dispatch
    result = self.request.registry['ir.http']._dispatch(endpoint)
  File "/home/odoo/src/odoo/addons/website/models/ir_http.py", line 235, in _dispatch
    response = super()._dispatch(endpoint)
  File "/home/odoo/src/odoo/odoo/addons/base/models/ir_http.py", line 221, in _dispatch
    result = endpoint(**request.params)
  File "/home/odoo/src/odoo/odoo/http.py", line 757, in route_wrapper
    result = endpoint(self, *args, **params_ok)
  File "/home/odoo/src/odoo/addons/web/controllers/dataset.py", line 25, in call_kw
    return self._call_kw(model, method, args, kwargs)
  File "/home/odoo/src/odoo/addons/web/controllers/dataset.py", line 21, in _call_kw
    return call_kw(Model, method, args, kwargs)
  File "/home/odoo/src/odoo/odoo/api.py", line 484, in call_kw
    result = _call_kw_multi(method, model, args, kwargs)
  File "/home/odoo/src/odoo/odoo/api.py", line 469, in _call_kw_multi
    result = method(recs, *args, **kwargs)
  File "/home/odoo/src/odoo/addons/web/models/models.py", line 73, in web_save
    self = self.create(vals)
  File "<decorator-gen-408>", line 2, in create
  File "/home/odoo/src/odoo/odoo/api.py", line 430, in _model_create_multi
    return create(self, [arg])
  File "/home/odoo/src/odoo/addons/sale_project/models/sale_order.py", line 250, in create
    created_records = super().create(vals_list)
  File "<decorator-gen-403>", line 2, in create
  File "/home/odoo/src/odoo/odoo/api.py", line 431, in _model_create_multi
    return create(self, arg)
  File "/home/odoo/src/odoo/addons/website_sale/models/sale_order.py", line 48, in create
    return super().create(vals_list)
  File "<decorator-gen-400>", line 2, in create
  File "/home/odoo/src/odoo/odoo/api.py", line 431, in _model_create_multi
    return create(self, arg)
  File "/home/odoo/src/enterprise/sale_subscription/models/sale_order.py", line 616, in create
    orders = super().create(vals_list)
  File "<decorator-gen-378>", line 2, in create
  File "/home/odoo/src/odoo/odoo/api.py", line 431, in _model_create_multi
    return create(self, arg)
  File "/home/odoo/src/odoo/addons/sale/models/sale_order.py", line 813, in create
    return super().create(vals_list)
  File "<decorator-gen-141>", line 2, in create
  File "/home/odoo/src/odoo/odoo/api.py", line 431, in _model_create_multi
    return create(self, arg)
  File "/home/odoo/src/odoo/addons/mail/models/mail_thread.py", line 259, in create
    threads = super(MailThread, self).create(vals_list)
  File "<decorator-gen-12>", line 2, in create
  File "/home/odoo/src/odoo/odoo/api.py", line 431, in _model_create_multi
    return create(self, arg)
  File "/home/odoo/src/odoo/odoo/models.py", line 4654, in create
    records = self._create(data_list)
  File "/home/odoo/src/odoo/odoo/models.py", line 4901, in _create
    field.create([
  File "/home/odoo/src/odoo/odoo/fields.py", line 4348, in create
    self.write_batch(record_values, True)
  File "/home/odoo/src/odoo/odoo/fields.py", line 4374, in write_batch
    self.write_real(records_commands_list, create)
  File "/home/odoo/src/odoo/odoo/fields.py", line 4564, in write_real
    flush()
  File "/home/odoo/src/odoo/odoo/fields.py", line 4520, in flush
    comodel.create(to_create)
  File "<decorator-gen-410>", line 2, in create
  File "/home/odoo/src/odoo/odoo/api.py", line 431, in _model_create_multi
    return create(self, arg)
  File "/home/odoo/src/odoo/addons/base_automation/models/base_automation.py", line 745, in create
    automation._process(automation._filter_post(records, feedback=True))
  File "/home/odoo/src/odoo/addons/base_automation/models/base_automation.py", line 696, in _process
    action.with_context(**ctx).run()
  File "/home/odoo/src/odoo/odoo/addons/base/models/ir_actions.py", line 943, in run
    res = runner(run_self, eval_context=eval_context)
  File "/home/odoo/src/odoo/addons/website/models/ir_actions_server.py", line 61, in _run_action_code_multi
    res = super(ServerAction, self)._run_action_code_multi(eval_context)
  File "/home/odoo/src/odoo/odoo/addons/base/models/ir_actions.py", line 775, in _run_action_code_multi
    safe_eval(self.code.strip(), eval_context, mode="exec", nocopy=True, filename=str(self))  # nocopy allows to return 'action'
  File "/home/odoo/src/odoo/odoo/tools/safe_eval.py", line 413, in safe_eval
    raise ValueError('%s: "%s" while evaluating\n%r' % (ustr(type(e)), ustr(e), expr))
ValueError: <class 'psycopg2.errors.UniqueViolation'>: "duplicate key value violates unique constraint "product_product_combination_unique"
DETAIL:  Key (product_tmpl_id, combination_indices)=(59686, ) already exists.
" while evaluating
'# Available variables:\n#  - env: environment on which the action is triggered\n#  - model: model of the record on which the action is triggered; is a void recordset\n#  - record: record on which the action is triggered; may be void\n#  - records: recordset of all records on which the action is triggered in multi-mode; may be void\n#  - time, datetime, dateutil, timezone: useful Python libraries\n#  - float_compare: utility function to compare floats based on specific precision\n#  - log: log(message, level=\'info\'): logging function to record debug information in ir.logging table\n#  - _logger: _logger.info(message): logger to emit messages in server logs\n#  - UserError: exception class for raising user-facing warning messages\n#  - Command: x2many commands namespace\n# To return an action, assign: action = {...}\nfor record in records:\n    product_tmpl = record.product_id.product_tmpl_id\n    \n    # 1. Obtener (o crear) los atributos\n    def get_or_create_attribute(name):\n        attr = env[\'product.attribute\'].search([(\'name\', \'=\', name)], limit=1)\n        if not attr:\n            attr = env[\'product.attribute\'].create({\n                \'name\': name,\n                \'display_type\': \'select\',  # ajusta según tu preferencia\n                \'create_variant\': \'dynamic\',  # Asegura que las variantes se creen automáticamente\n            })\n        return attr\n    \n    attribute_densidad = get_or_create_attribute(\'ProdTD\')\n    attribute_largo = get_or_create_attribute(\'ProdTL\')\n    attribute_ancho = get_or_create_attribute(\'ProdTA\')\n    attribute_alto  = get_or_create_attribute(\'ProdTP\')\n    \n    # 2. Crear u obtener valores\n    def get_or_create_attribute_value(attribute, value):\n        val_str = str(value)\n        val = env[\'product.attribute.value\'].search([\n            (\'attribute_id\', \'=\', attribute.id),\n            (\'name\', \'=\', val_str),\n        ], limit=1)\n        if val:\n            return val\n        else:\n            return env[\'product.attribute.value\'].create({\n                \'attribute_id\': attribute.id,\n                \'name\': val_str,\n                \n            })\n    \n    val_densidad = get_or_create_attribute_value(attribute_densidad, record.production_density)\n    val_largo = get_or_create_attribute_value(attribute_largo, record.length)\n    val_ancho = get_or_create_attribute_value(attribute_ancho, record.width)\n    val_alto  = get_or_create_attribute_value(attribute_alto, record.camber)\n    \n    cfg = \'Configurar en la Línea de la Orden\'\n    config_densidad = get_or_create_attribute_value(attribute_densidad, cfg)\n    config_largo = get_or_create_attribute_value(attribute_largo, cfg)\n    config_ancho = get_or_create_attribute_value(attribute_ancho, cfg)\n    config_alto = get_or_create_attribute_value(attribute_alto, cfg)\n    \n    \n    # 3. Asegurar que el template tenga las líneas de atributo\n    def ensure_attribute_line(template, attribute, value):\n        line = env[\'product.template.attribute.line\'].search([\n            (\'product_tmpl_id\', \'=\', template.id),\n            (\'attribute_id\', \'=\', attribute.id),\n        ], limit=1)\n        if not line:\n            line = env[\'product.template.attribute.line\'].create({\n                \'product_tmpl_id\': template.id,\n                \'attribute_id\': attribute.id,\n                \'value_ids\': [(6, 0, [value.id])],\n            })\n        elif value.id not in line.value_ids.ids:\n            line.write({\'value_ids\': [(4, value.id, False)]})\n        return line\n    \n    \n    line_densidad = ensure_attribute_line(product_tmpl, attribute_densidad, val_densidad)\n    line_largo = ensure_attribute_line(product_tmpl, attribute_largo, val_largo)\n    line_ancho = ensure_attribute_line(product_tmpl, attribute_ancho, val_ancho)\n    line_alto = ensure_attribute_line(product_tmpl, attribute_alto, val_alto)\n    \n    line_cfg_densidad = ensure_attribute_line(product_tmpl, attribute_densidad, config_densidad)\n    line_cfg_largo = ensure_attribute_line(product_tmpl, attribute_largo, config_largo)\n    line_cfg_ancho = ensure_attribute_line(product_tmpl, attribute_ancho, config_ancho)\n    line_cfg_alto = ensure_attribute_line(product_tmpl, attribute_alto, config_alto)\n    \n    # ———————————————————————————————————————\n    # 1) Variante “normal”: todos los atributos con sus valores reales\n    # ———————————————————————————————————————\n    \n    # 1.1) IDs de los PTAV correspondientes a cada valor real\n    real_ptav_ids = []\n    for line_id, pav_id in zip(\n        [line_densidad.id, line_largo.id, line_ancho.id, line_alto.id],\n        [val_densidad.id,   val_largo.id,   val_ancho.id,   val_alto.id]\n    ):\n        ptav = env[\'product.template.attribute.value\'].search([\n            (\'attribute_line_id\',          \'=\', line_id),\n            (\'product_attribute_value_id\', \'=\', pav_id),\n        ], limit=1)\n        if not ptav:\n            raise UserError(f"No PTAV para línea {line_id} y valor {pav_id}")\n        real_ptav_ids.append(ptav.id)\n    \n    # 1.2) Buscar en BD cualquier variante que contenga todos esos PTAVs\n    domain = [(\'product_tmpl_id\', \'=\', product_tmpl.id)]\n    for ptav_id in real_ptav_ids:\n        domain.append((\'product_template_variant_value_ids\', \'in\', ptav_id))\n    candidates = env[\'product.product\'].search(domain)\n    \n    # 1.3) Filtrar la variante que tenga exactamente ese conjunto de PTAVs\n    variant = False\n    target = set(real_ptav_ids)\n    for p in candidates:\n        if set(p.product_template_variant_value_ids.ids) == target:\n            variant = p\n            break\n\n    # 1.4) Crear sólo si no existe\n    if not variant:\n        variant = env[\'product.product\'].create({\n            \'product_tmpl_id\': product_tmpl.id,\n            \'product_template_variant_value_ids\': [(6, 0, real_ptav_ids)],\n        })\n        \n    # 1.5) Asignar al registro de la cotización\n    try:\n        record[\'product_id\'] = variant\n    except Exception:\n        record[\'x_studio_debug_2\'] = \'No se puede asignar variante\''

The above server error caused the following client error:
RPC_ERROR: Odoo Server Error
    RPC_ERROR
        at makeErrorFromResponse (https://plexa-sandbox-19144517.dev.odoo.com/web/assets/c4a1e2c/web.assets_web.min.js:2939:163)
        at XMLHttpRequest.<anonymous> (https://plexa-sandbox-19144517.dev.odoo.com/web/assets/c4a1e2c/web.assets_web.min.js:2943:13)
Avatar
Discard

Your Answer

Please try to give a substantial answer. If you wanted to comment on the question or answer, just use the commenting tool. Please remember that you can always revise your answers - no need to answer the same question twice. Also, please don't forget to vote - it really helps to select the best questions and answers!