javascriptpythonodoo

How to execute JavaScript only on a specific view in Odoo 15 without affecting other views?


I am working on a custom module in Odoo 15 and need to execute JavaScript code on a specific view (the lot verification view). However, the code is currently being executed globally across all views, which is causing errors and poor performance in unrelated views (like Settings).

The JS works fine on the lot verification view, but it also runs on other views, which causes errors and slows down the application.

What I'm trying to achieve:

Execute the JS code only on the lot verification view.

Prevent the JS from executing in views like Settings, Purchases, etc.

Maintain the performance of the code as it was originally, with fast execution.

The lot_enter.js file is being loaded correctly, but the issue is that it runs globally, affecting other views.

I have tried various ways to limit the code execution to the verification view, but I haven't been able to make it work correctly without it running globally.

Relevant code:

1º JS

odoo.define('verificacion_de_ubicacion.LotEnterRenderer', function(require) {
    'use strict';

    const FormRenderer = require('web.FormRenderer');

    const LotEnterRenderer = FormRenderer.extend({
        _renderView: function() {
            console.log('🎯 Ejecutando el JS personalizado');
            this._super.apply(this, arguments);

            // Verificar si estamos en la vista correcta
            if (this.el && this.el.classList.contains('lot_enter_view')) {
                console.log('🎯 Escaneo activo en formulario de verificación');
                
                setTimeout(() => {
                    const input = this.el.querySelector('input[name="escaneado"]');
                    const button = this.el.querySelector('button[name="procesar_lote"]');
                    if (!input || !button) return;

                    // Eliminar eventos anteriores para evitar duplicados
                    input.removeEventListener('keydown', handleKeydown);
                    input.addEventListener('keydown', handleKeydown);

                    function handleKeydown(e) {
                        if (e.key === 'Enter') {
                            console.log('🎯 Se presionó Enter');
                            e.preventDefault();
                            button.click();

                            // Poner el foco nuevamente en el input
                            input.focus();
                            input.select();
                            console.log('🎯 Foco devuelto al input');
                        }
                    }

                    // Enfoque inmediato al input
                    input.focus();
                    input.select();
                    console.log('🎯 Enfoque inicial al input');
                }, 100);
            }
        }
    });

    return LotEnterRenderer;
});

2º My XML: '''

<odoo>
  <!-- Vista principal de escaneo - intento de rebuild 2 -->
  <record id="view_verificacion_lote_form_custom" model="ir.ui.view">
    <field name="name">stock.verificacion.lote.form.custom</field>
    <field name="model">stock.verificacion.lote</field>
    <field name="priority" eval="10"/>
    <field name="arch" type="xml">
      <form string="Verificación de Lotes" class="lot_enter_view" create="false" edit="false" js_class="verificacion_de_ubicacion.LotEnterRenderer">
        <sheet>
          <group>
            <field name="ubicacion_id" readonly="1"/>
            <field name="escaneado" placeholder="Escanea un lote..." autofocus="1"/>
            <field name="resultado" readonly="1" nolabel="1"/>
            <field name="resultado_ok" invisible="1"/>
            <field name="lotes_verificados" invisible="1"/>
            <field name="lotes_movidos" invisible="1"/>
            <field name="lotes_duplicados" invisible="1"/>
            <button name="procesar_lote" type="object" invisible="1"/>
            <field name="lotes_html" readonly="1" widget="html"/>
          </group>
          <footer>
            <button name="action_terminar_conteo" string="Terminar Conteo" type="object" class="btn-primary"/>
            <button name="action_reiniciar_recuento" string="Reiniciar Conteo"
                    type="object" class="btn btn-secondary"
                    style="margin-left: 8px; border-radius: 0px;"/>
          </footer>
        </sheet>
      </form>
    </field>
  </record>

  <!-- Popup de confirmación -->
  <record id="view_confirmar_terminar_conteo" model="ir.ui.view">
    <field name="name">stock.verificacion.lote.confirm</field>
    <field name="model">stock.verificacion.lote</field>
    <field name="priority" eval="20"/>
    <field name="arch" type="xml">
      <form string="Confirmar Conteo" create="false" edit="false">
        <group>
          <div class="o_form_label">
            Se van a mover los lotes NO escaneados a la ubicación <b>'almacen/stock/encontrar'</b>. ¿Deseas continuar?
          </div>
        </group>
        <footer>
          <button name="action_confirmar_conteo" string="Confirmar" type="object" class="btn-primary"/>
          <button string="Cancelar" special="cancel" class="btn-secondary"/>
        </footer>
      </form>
    </field>
  </record>
</odoo>

''' 3º The model is this:

'''

from odoo import models, fields, api
import logging

_logger = logging.getLogger(__name__)

class VerificacionLote(models.TransientModel):
    _name = 'stock.verificacion.lote'
    _description = 'Verificación de Lotes en Ubicación'
    _rec_name = 'ubicacion_id'

    ubicacion_id = fields.Many2one('stock.location', string="Ubicación a verificar", required=True)
    escaneado = fields.Char(string="Escanear Lote")
    resultado = fields.Html(string="Resultado", readonly=True, sanitize=False)
    resultado_ok = fields.Boolean("Correcto", readonly=True)
    lotes_html = fields.Html(string="Lotes esperados", readonly=True)
    lotes_verificados = fields.Text(string="Lotes Verificados", readonly=True, default='')
    lotes_movidos = fields.Text(string="Lotes Movidos", readonly=True, default='')
    lotes_duplicados = fields.Text(string="Duplicados", readonly=True, default='')
    confirmado = fields.Boolean(string="Confirmado", default=False)

    def _get_lotes_en_ubicacion(self):
        if not self.ubicacion_id:
            return self.env['stock.quant']
        return self.env['stock.quant'].search([
            ('location_id', '=', self.ubicacion_id.id),
            ('quantity', '>', 0),
            ('lot_id', '!=', False),
        ])

    @api.model
    def default_get(self, fields_list):
        res = super().default_get(fields_list)
    
        ubicacion_ctx = self.env.context.get('ubicacion_id')
        if ubicacion_ctx:
            res['ubicacion_id'] = ubicacion_ctx
    
        try:
            # Hacemos un new solo si tenemos ubicacion
            if res.get('ubicacion_id'):
                rec = self.new(res)
                rec._refrescar_lotes_html()
                res['lotes_html'] = rec.lotes_html
        except Exception as e:
            _logger.warning("Error en default_get de verificacion_lote: %s", e)
            res['lotes_html'] = "<p style='color:red'>⚠️ Error cargando los lotes. Revisa la ubicación.</p>"
    
        return res


    def _refrescar_lotes_html(self):
        lotes = self._get_lotes_en_ubicacion()
        verificados = (self.lotes_verificados or '').split(',')
        movidos = (self.lotes_movidos or '').split(',')
        duplicados = (self.lotes_duplicados or '').split(',')

        html = "<ol>"
        for l in lotes:
            lote = l.lot_id.name if l.lot_id else "Sin nombre"
            producto = l.product_id.display_name or "Producto desconocido"
            icono = ''
            if lote in verificados:
                icono = '✔️'
            elif lote in movidos:
                icono = '📦'
            elif lote in duplicados:
                icono = '♻️'
            html += f"<li>{lote} - {producto} {icono}</li>"
        html += "</ol>"
        self.lotes_html = html

    def procesar_lote(self):
        self.resultado_ok = False
        lot_name = (self.escaneado or '').strip()
        if not lot_name:
            self.resultado = "⚠️ Campo vacío."
            return

        verificados = (self.lotes_verificados or '').split(',')
        movidos = (self.lotes_movidos or '').split(',')
        duplicados = (self.lotes_duplicados or '').split(',')

        if lot_name in verificados or lot_name in movidos:
            if lot_name not in duplicados:
                duplicados.append(lot_name)
                self.lotes_duplicados = ','.join(duplicados)
            self.resultado = f"♻️ Lote '{lot_name}' ya fue escaneado."
            return

        StockQuant = self.env['stock.quant']
        lot_quant = StockQuant.search([
            ('lot_id.name', '=', lot_name),
            ('quantity', '>', 0),
            ('lot_id', '!=', False),
        ], limit=1)

        if not lot_quant:
            self.resultado = f"❌ Lote '{lot_name}' no encontrado."
            return

        if lot_quant.location_id.id == self.ubicacion_id.id:
            verificados.append(lot_name)
            self.resultado = f"✅ Lote '{lot_name}' verificado en ubicación."
            self.resultado_ok = True
        else:

            # ⚠️ Verificamos que esté en una ubicación interna antes de moverlo
            if lot_quant.location_id.usage != 'internal':
                self.resultado = (
                    f"<p style='color:red; font-size: 18px;'>❌ Lote '{lot_name}' está en una ubicación no interna: "
                    f"<strong>{lot_quant.location_id.complete_name}</strong>. No se puede mover automáticamente.</p>"
                )
                self.resultado_ok = False
                return
        
            picking = self.env['stock.picking'].create({
                'picking_type_id': self.env.ref('stock.picking_type_internal').id,
                'location_id': lot_quant.location_id.id,
                'location_dest_id': self.ubicacion_id.id,
                'user_id': self.env.user.id,
            })
            move = picking.move_ids_without_package.create({
                'picking_id': picking.id,
                'name': lot_quant.product_id.name,
                'product_id': lot_quant.product_id.id,
                'product_uom_qty': 1,
                'product_uom': lot_quant.product_id.uom_id.id,
                'location_id': lot_quant.location_id.id,
                'location_dest_id': self.ubicacion_id.id,
            })
            picking.action_confirm()
            picking.action_assign()
            self.env['stock.move.line'].create({
                'picking_id': picking.id,
                'move_id': move.id,
                'product_id': lot_quant.product_id.id,
                'product_uom_id': lot_quant.product_id.uom_id.id,
                'qty_done': 1,
                'location_id': lot_quant.location_id.id,
                'location_dest_id': self.ubicacion_id.id,
                'lot_id': lot_quant.lot_id.id,
            })
            picking.button_validate()
            movidos.append(lot_name)
            self.resultado = f"📦 Lote '{lot_name}' movido a la ubicación."
            self.resultado_ok = True

        self.lotes_verificados = ','.join(verificados)
        self.lotes_movidos = ','.join(movidos)
        self.escaneado = ''
        self._refrescar_lotes_html()

    def action_reiniciar_recuento(self):
        self.lotes_verificados = ''
        self.lotes_duplicados = ''
        self.lotes_movidos = ''
        self.resultado = ''
        self.resultado_ok = False
        self.escaneado = ''
        self.confirmado = False
        self._refrescar_lotes_html()

    def action_confirmar_conteo(self):
        self.ensure_one()
        self.confirmado = True
        return self.action_terminar_conteo()

    def action_terminar_conteo(self):
        self.ensure_one()
    
        if not self.confirmado:
            return {
                'type': 'ir.actions.act_window',
                'name': 'Confirmar Conteo',
                'res_model': 'stock.verificacion.lote',
                'view_mode': 'form',
                'view_id': self.env.ref('verificacion_de_ubicacion.view_confirmar_terminar_conteo').id,
                'target': 'new',
                'res_id': self.id,
                'context': dict(self.env.context, force_confirm=True),
            }
    
        lotes_actuales = set([l.lot_id.name for l in self._get_lotes_en_ubicacion() if l.lot_id])
        verificados = set((self.lotes_verificados or '').split(','))
        movidos = set((self.lotes_movidos or '').split(','))
        encontrados = verificados.union(movidos)
        faltantes = lotes_actuales - encontrados
        
        _logger.info("Todos los lotes encontrados: %s", encontrados)
        _logger.info("Lotes actuales: %s", lotes_actuales)
        _logger.info("Faltantes (no encontrados): %s", faltantes)
        
        if not faltantes:
            return {
                'type': 'ir.actions.client',
                'tag': 'display_notification',
                'params': {
                    'title': "Recuento Finalizado",
                    'message': "✅ Todos los lotes han sido verificados o corregidos.",
                    'type': 'success',
                }
            }
    
        ubicacion_destino = self.env['stock.location'].browse(719)
        picking = self.env['stock.picking'].create({
            'picking_type_id': self.env.ref('stock.picking_type_internal').id,
            'location_id': self.ubicacion_id.id,
            'location_dest_id': ubicacion_destino.id,
            'user_id': self.env.user.id,
            'partner_id': self.env.user.partner_id.id,
            'origin': 'Lotes no encontrados en verificación',
        })
        
        for nombre in faltantes:
            quant = self.env['stock.quant'].search([
                ('lot_id.name', '=', nombre),
                ('quantity', '>', 0)
            ], limit=1)
        
            if quant:
                move = self.env['stock.move'].create({
                    'picking_id': picking.id,
                    'name': quant.product_id.name,
                    'product_id': quant.product_id.id,
                    'product_uom_qty': 1,
                    'product_uom': quant.product_id.uom_id.id,
                    'location_id': quant.location_id.id,
                    'location_dest_id': ubicacion_destino.id,
                })
        
                self.env['stock.move.line'].create({
                    'picking_id': picking.id,
                    'move_id': move.id,
                    'product_id': quant.product_id.id,
                    'product_uom_id': quant.product_id.uom_id.id,
                    'qty_done': 1,
                    'location_id': quant.location_id.id,
                    'location_dest_id': ubicacion_destino.id,
                    'lot_id': quant.lot_id.id,
                })
        
        # Confirmar y validar después de todos los movimientos
        picking.action_confirm()
        picking.action_assign()
        picking.button_validate()


    
             # 🧠 Una vez finalizado el loop completo:
        self.action_reiniciar_recuento()
        return {
                    'type': 'ir.actions.act_window',
                    'name': 'Verificación de Ubicación',
                    'res_model': 'stock.verificacion.lote',
                    'view_mode': 'form',
                    'views': [(self.env.ref('verificacion_de_ubicacion.view_verificacion_lote_form').id, 'form')],
                    'target': 'current',
                    'context': {
                        'ubicacion_id': self.ubicacion_id.id,
                        'reload_from_conteo': True,
                    }
                }  

             

'''


Solution

  • Thanks to Kenly´s feedback i could solve it.

    This ensures your JS only runs on your intended view and doesn't affect global performance or other modules.

    📁 Recommended module structure:

    your_module/
    ├── static/
    │   └── src/
    │       └── js/
    │           ├── custom_renderer.js
    │           └── custom_view.js
    ├── views/
    │   └── custom_form_view.xml
    ├── __manifest__.py
    

    🧠 1. custom_renderer.js

    odoo.define('your_module.CustomRenderer', function(require) {
        'use strict';
    
        const FormRenderer = require('web.FormRenderer');
    
        const CustomRenderer = FormRenderer.extend({
            _renderView: function() {
                this._super.apply(this, arguments);
                try {
                    console.log('🔧 CustomRenderer active only on this view');
    
                    // Your custom logic here
                    const input = this.el.querySelector('input[name="your_field_name"]');
                    if (input) {
                        input.focus();
                    }
    
                } catch (err) {
                    console.warn('⚠️ Error in CustomRenderer:', err);
                }
            }
        });
    
        return CustomRenderer;
    });
    

    🧩 2. custom_view.js

    odoo.define('your_module.CustomFormView', function(require) {
        'use strict';
    
        const FormView = require('web.FormView');
        const viewRegistry = require('web.view_registry');
        const CustomRenderer = require('your_module.CustomRenderer');
    
        const CustomFormView = FormView.extend({
            config: _.extend({}, FormView.prototype.config, {
                Renderer: CustomRenderer,
            }),
        });
    
        viewRegistry.add('custom_form_view', CustomFormView);
    
        return CustomFormView;
    });
    

    🧾 3. manifest.py

    'assets': {
        'web.assets_backend': [
            'your_module/static/src/js/custom_renderer.js',
            'your_module/static/src/js/custom_view.js',
        ],
    },
    

    🧱 4. In your view XML (custom_form_view.xml):

    <odoo>
      <record id="your_model_form_view" model="ir.ui.view">
        <field name="name">your.model.form</field>
        <field name="model">your.model</field>
        <field name="arch" type="xml">
          <form string="My Special View" js_class="custom_form_view">
            <sheet>
              <group>
                <field name="your_field_name"/>
              </group>
            </sheet>
          </form>
        </field>
      </record>
    </odoo>
    

    🔁 Important: js_class="custom_form_view" must match the name used in viewRegistry.add().

    ✅ Benefits: Modular and clean code.

    No interference with other views like Settings or Purchases.

    Scalable pattern for adding custom JS to specific form views.