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)