View Inheritance e XPath in Odoo: l’unico approccio che sopravvive agli aggiornamenti

Come personalizzare viste backend e frontend con moduli custom, ereditarietà XML e XPath robusti, senza compromettere la manutenibilità e la compatibilità con le versioni successive.

Ogni sviluppatore Odoo, prima o poi, si trova davanti alla stessa scena: una richiesta di modifica a una vista esistente, una deadline stretta e la tentazione di aprire direttamente il file XML nativo per sistemare la cosa in cinque minuti. È una scorciatoia che si paga cara al prossimo odoo-bin -u all.

Questo articolo descrive il meccanismo corretto, ovvero la view inheritance via XPath, con la profondità che merita: non solo il "come", ma anche le distinzioni che la documentazione informale spesso omette, i limiti reali dell'approccio e i pattern che in Unitiva usiamo tutti i giorni.

L’analogia del libro e del “Post-it” tecnologico

Per chi si approccia al tema per la prima volta, un'analogia aiuta a fissare il concetto prima di scendere nel codice.

Immagina che ogni vista standard di Odoo sia un libro stampato e distribuito dalla casa madre. Modificare direttamente il file XML nativo equivale a scrivere a penna sulle pagine di quel libro: alla prossima release, il libro viene sostituito da una copia aggiornata e immacolata. Tutto cio' che hai scritto scompare.

Creare un modulo custom con una view inherited è invece come incollare un Post-it su quella pagina. Ma il Post-it ben scritto non dice "vai a pagina 12, riga 4": dice "trova l'elemento chiamato phone e inserisci questo subito dopo". Il libro originale rimane intatto, e l'istruzione continua a funzionare anche se nella nuova edizione quell'elemento si è spostato di pagina.

È qui che la metafora rivela il punto centrale dell'intero articolo: la solidità della personalizzazione dipende da come scrivi l'istruzione sul Post-it. Un'istruzione legata al contenuto ("l'elemento chiamato X") sopravvive agli aggiornamenti; un'istruzione legata alla posizione ("pagina 12, riga 4") si rompe alla prima ristampa.

La struttura di una view inherited: i due pattern

Il primo chiarimento necessario riguarda la sintassi. Esistono due pattern distinti, e confonderli è un errore comune, anche se sotto il cofano sono lo stesso meccanismo: entrambi creano un record ir.ui.view. Il template è semplicemente una scorciatoia che genera un ir.ui.view con type="qweb".

Per le viste QWeb (frontend, website):

<odoo>
  <template id="homepage_custom" inherit_id="website.homepage" priority="15">
    <xpath expr="//div[@id='wrap']" position="inside">
      <section class="oo-custom-banner">
        <h2>Contenuto personalizzato</h2>
      </section>
    </xpath>
  </template>
</odoo>


Per le viste backend (form, list, kanban, search):

<odoo>
  <record id="view_partner_form_custom" model="ir.ui.view">
    <field name="name">res.partner.form.custom</field>
    <field name="model">res.partner</field>
    <field name="inherit_id" ref="base.view_partner_form"/>
    <field name="arch" type="xml">
      <xpath expr="//field[@name='phone']" position="after">
        <field name="x_custom_field"/>
      </xpath>
    </field>
  </record>
</odoo>

La distinzione pratica resta utile: il template è il modo idiomatico di estendere viste QWeb (website, portal, report), mentre il record model="ir.ui.view" è quello che userai per form, list, kanban e search del backend. Usare la sintassi sbagliata nel contesto sbagliato porta spesso a viste che semplicemente non si applicano come ti aspetti, e il debug diventa frustrante.

XPath: precisione come requisito, non come preferenza

XPath è il linguaggio con cui dici a Odoo dove posizionare il bisturi. La qualità dell'espressione determina la robustezza della modifica nel tempo.

Le opzioni di position:

  • inside: aggiunge in coda all’interno dell’elemento trovato
  • before / after:  inserisce prima o dopo l’elemento trovato
  • replace:  sostituisce completamente l’elemento trovato (con ‘position=“replace”’ e contenuto vuoto, lo rimuove)
  • attributes:  modifica attributi specifici senza toccare il contenuto
  • move: sposta un elemento esistente in un altro punto della vista

Quest'ultima, move, è proprio lo strumento da usare quando una nuova versione di Odoo ha spostato un elemento e tu hai bisogno di riportarlo dove serve alla tua personalizzazione, invece di duplicarlo.

Il punto critico che la documentazione informale non dice: la fragilità di un XPath è proporzionale alla sua specificità. Un'espressione come questa si rompe non appena Odoo riorganizza la struttura della vista, anche senza modificare il campo target:

expr="//div[@class='o_form_view']/div[2]/div[1]/field[@name='partner_id']"

La versione robusta:

expr="//field[@name='partner_id']"

Oppure, se il contesto è ambiguo:

expr="//field[@name='partner_id'][ancestor::group[@string='Informazioni generali']]"

Un’avvertenza specifica sull’aggancio per classe CSS: ‘@class=‘o_form_view’’ fa match solo sulla stringa esatta di classi, quindi si rompe non appena Odoo aggiunge o riordina una classe. Odoo mette a disposizione la funzione ‘hasclass()’ proprio per questo, e arriva persino a loggare un warning quando rileva un uso fragile di ‘@class’:

expr="//div[hasclass('o_form_view')]"

Privilegia sempre l’aggancio a identificatori semantici (‘name’, ‘id’, ‘string’) rispetto a posizioni strutturali. La struttura di una vista può cambiare tra minor release; gli attributi semantici sono molto più stabili.

Come Odoo risolve la catena di ereditarietà a runtime

Odoo non "riapplica" le viste ereditate dopo ogni aggiornamento. L'arch finale viene combinato a partire dalla vista base più tutte le viste figlie. Si parte dalla vista base (arch originale), si raccolgono tutte le viste con inherit_id che puntano ad essa, ordinate per priority, e si applica ogni XPath in sequenza sull'albero risultante.

La conseguenza pratica è duplice.

Prima: se due moduli custom modificano lo stesso elemento, l'ordine di applicazione dipende da priority (default 16; valori più bassi vengono applicati prima). Quando costruisci sopra una vista già modificata da un modulo terzo, devi ragionare esplicitamente sulla sequenza.

Seconda, e qui va corretto un mito diffuso: un XPath che non trova il suo target non fallisce silenziosamente. Odoo solleva un errore esplicito ("Element ... cannot be located in parent view") e questo controllo gira tramite un constraint sull'arch al momento della scrittura della vista, cioè durante l'installazione o l'aggiornamento del modulo. In pratica: un XPath rotto fa abortire l'odoo-bin -u.

Questo non è un difetto, è la tua rete di sicurezza. Significa che un aggiornamento che ha invalidato un tuo aggancio si fa notare subito, rumorosamente, nel posto giusto: in fase di upgrade. Ed è esattamente il motivo per cui non si aggiorna mai direttamente in produzione: l'upgrade va sempre provato prima su staging o in CI, dove l'errore emerge senza fare danni.

Il vero scenario di frizione: XPath che trova il bersaglio sbagliato

Se l'XPath rotto blocca l'upgrade, qual è allora il problema davvero insidioso? È il caso opposto, e più subdolo: l'XPath che continua a trovare un target, ma non più quello giusto.

Odoo, tra una versione e l'altra, raramente elimina del tutto un elemento. Molto più spesso lo sposta, lo racchiude in un nuovo container, o ne cambia il contesto. Se il tuo aggancio era ancorato alla posizione strutturale, puo' succedere che continui a risolversi, magari su un nodo simile in un altro punto della vista, e la tua personalizzazione finisce nel posto sbagliato. Nessun errore, nessun crash all'upgrade: la vista si carica, ma il campo è finito nella scheda sbagliata o nel gruppo sbagliato.

Questo è il fallimento che non vedi in CI, perché tecnicamente "funziona". È il motivo per cui, dopo ogni aggiornamento di versione, il workflow che adottiamo non si ferma al "l'upgrade è passato": include una verifica che le viste critiche siano non solo applicate, ma applicate dove ci aspettiamo. È un controllo che, su progetti in produzione, non si puo' rimandare.

priority, groups e active: i parametri che fanno la differenza

Una view inherited completa non è solo XPath. Tre attributi che è necessario padroneggiare:

priority 

Controlla l'ordine di applicazione nella catena. Default 16. Se stai costruendo sopra una view di un modulo terzo che ha già modificato la vista base, devi ragionare esplicitamente sulla sequenza.

groups 

Limita la visibilità di una porzione di vista a gruppi specifici. Attenzione a un dettaglio che blocca chi sbaglia: su una vista ereditata non si usa il campo groups_id sul record (Odoo lo rifiuta con un errore esplicito). Il controllo per gruppo si fa con l'attributo groups sul nodo all'interno dell'arch:

<xpath expr="//field[@name='phone']" position="after">
  <field name="x_custom_field" groups="base.group_system"/>
</xpath>

Il groups_id sul record resta valido solo per le viste primarie, non per quelle ereditate.

active 

Può essere impostato a False per disabilitare una view inherited senza rimuoverla. Utile durante lo sviluppo e per funzionalità che si attivano condizionalmente tramite configurazione.

<record id="view_partner_form_custom" model="ir.ui.view">
  <field name="name">res.partner.form.custom</field>
  <field name="model">res.partner</field>
  <field name="inherit_id" ref="base.view_partner_form"/>
  <field name="priority">20</field>
  <field name="arch" type="xml">
    <xpath expr="//field[@name='phone']" position="after">
      <field name="x_custom_field" groups="base.group_system"/>
    </xpath>
  </field>
</record>

La struttura minima del modulo custom

Una view inherited non vive da sola. Il modulo che la contiene richiede due file obbligatori.

__manifest__.py

{
    'name': 'Custom Partner Form',
    'version': '19.0.1.0.0',
    'depends': ['base', 'contacts'],
    'data': ['views/res_partner_views.xml'],
    'installable': True,
    'auto_install': False,
}

La chiave depends non è solo dichiarativa: determina l'ordine di caricamento dei moduli. Se la tua view eredita da una vista definita in contacts, quel modulo deve essere in depends, altrimenti l'inherit_id potrebbe non essere ancora registrato quando il tuo modulo viene processato, con errori difficili da diagnosticare.

__init__.py

Puo' essere vuoto per moduli che contengono solo viste XML, ma deve esistere affinchè Python riconosca la cartella come package.

Quando la view inheritance non basta

C'è un limite architetturale che è onesto nominare. Le view inherited gestiscono la struttura della vista. Non gestiscono:

  • logiche di visibilità condizionale: nel backend di Odoo 19 si esprimono direttamente con attributi come invisible, readonly, required, column_invisible e un'espressione Python (gli attributi attrs e states, usati nelle versioni piu' vecchie, sono stati rimossi); nel frontend serve logica OWL
  • nuovi campi su modelli esistenti: richiedono un modello custom con _inherit
  • comportamenti che dipendono da dati a runtime: richiedono override Python di metodi come fields_get, default_get, onchange

La view inheritance è lo strumento giusto per modificare il layout e la struttura di una schermata esistente. Per tutto il resto, il modulo custom deve fare un passo in più, e capire quando quel passo è necessario è la differenza tra una customizzazione solida e una che si rompe al primo caso edge.

Ogni giorno in Unitiva mettiamo le mani su queste strutture: su upgrade che non devono interrompere la produzione, su personalizzazioni che devono coesistere con moduli di terze parti, su XPath che devono reggere al prossimo aggiornamento. La view inheritance non è un dettaglio: è la fondazione su cui si costruisce qualsiasi customizzazione mantenibile.

Hai un progetto Odoo da sviluppare o una customizzazione che deve reggere agli aggiornamenti?

Prenota una call con il team di sviluppo Unitiva: analizziamo insieme la tua situazione e ti diciamo cosa ha senso fare, senza giri di parole.

Prenota la tua consulenza gratuita →

Autoreadmin
Potrebbero interessarti...
back to top icon