Se rendre au contenu

odoo-bin shell : la console cachée d'Odoo et 15 patterns ORM essentiels

Série Tech · Article 4/5 · Boîte à outils Linux du dev Odoo
14 mai 2026 par
odoo-bin shell : la console cachée d'Odoo et 15 patterns ORM essentiels
B.Mustapha
| Aucun commentaire pour l'instant

odoo-bin shell : la console cachée d'Odoo et 15 patterns ORM essentiels

La commande odoo-bin shell ouvre une console Python interactive avec l'environnement Odoo déjà initialisé — env directement disponible, transaction en cours, droits super-utilisateur. C'est l'outil le plus efficace pour explorer une base, fixer mille enregistrements en trois lignes, ou tester un compute avant de l'écrire dans un modèle. Pourtant la commande reste sous-utilisée par méconnaissance des patterns ORM qui en font toute la valeur. Cet article rassemble les quinze patterns essentiels validés sur Odoo 19, avec les pièges spécifiques de la version — notamment la dépréciation officielle d'odoo.osv.expression.

Cet article constitue l'épisode 4/5 de la série S01 — Boîte à outils Linux du dev Odoo. Les trois précédents ont couvert Linux, Git/GitHub et Bash. La présente section bascule du système vers le moteur Odoo lui-même. L'épisode suivant explorera PostgreSQL côté psql.

1. Lancer le shell — interfaces REPL et options

La commande odoo-bin shell accepte les options de démarrage du serveur, plus deux options propres au mode console : --shell-interface et --shell-file (déclarées lignes 64 et 69 de odoo/cli/shell.py). Sans argument explicite, le shell tente successivement IPython, ptpython, bpython, puis le REPL Python standard (liste supported_shells ligne 57).

# Lancer le shell sur une base existante (la forme la plus utilisée)
./odoo-bin shell -c odoo.conf -d ma_base

# Forcer IPython (recommandé pour l'interactif — historique, autocomplétion)
./odoo-bin shell -c odoo.conf -d ma_base --shell-interface=ipython

# Forcer le REPL Python standard (recommandé pour scripts batch)
./odoo-bin shell -c odoo.conf -d ma_base --shell-interface=python

# Exécuter un script Python après démarrage du REPL (équivalent PYTHONSTARTUP)
./odoo-bin shell -c odoo.conf -d ma_base --shell-file=preload.py

# Mode batch — exécuter un script complet sans interaction
cat fix_partner_dz.py | ./odoo-bin shell -c odoo.conf -d ma_base

Le mode batch via pipe stdin est détecté automatiquement : si sys.stdin n'est pas un TTY, le shell exécute le contenu reçu puis quitte (logique os.isatty ligne 80 de shell.py). Mode reproductible par excellence — un script versionné dans Git, exécuté identiquement en dev, staging et production.

2. Découvrir l'environnement — env, self, odoo

Dès l'ouverture du shell, quatre variables sont injectées dans le scope local par Shell.shell() (lignes 130 à 151 du code source).

# env — instance api.Environment(cr, SUPERUSER_ID, context_get())
>>> env
<odoo.api.Environment object at 0x7f...>

# self — équivalent à env.user (utilisateur courant, ici l'admin OdooBot)
>>> self.login
'__system__'

# odoo — module odoo complet (odoo.fields, odoo.tools, odoo.exceptions...)
>>> odoo.release.version
'19.0'

# openerp — alias historique pointant vers le même module odoo (legacy)
>>> openerp is odoo
True

# env['model.name'] retourne le wrapper du modèle (vide, prêt à recevoir search/create)
>>> env['res.partner']
res.partner()

# env.cr — curseur PostgreSQL (transaction en cours)
# env.context — dict de contexte (lang, tz, allowed_company_ids, etc.)
>>> env.context.get('lang')
'fr_FR'

📖 Définition — env dans Odoo

env est une instance d'api.Environment qui regroupe trois éléments indissociables : le curseur SQL (cr), l'identifiant utilisateur (uid) et le dictionnaire de contexte (context). Toute opération ORM passe par cet objet. La notation env['res.partner'] retourne un recordset vide du modèle, sur lequel s'enchaînent search, create, browse, ou toute méthode métier déclarée. Le shell instancie l'environnement avec SUPERUSER_ID (uid 1) — filtres de sécurité contournés. Pour tester un compute avec les droits d'un utilisateur métier : env(user=env.ref('base.user_demo').id).

3. Patterns 1 à 3 — search, search_count, browse

Les trois patterns fondamentaux couvrent 80 % des besoins d'inspection. search filtre via un domain et retourne un recordset. search_count compte sans charger les enregistrements. browse récupère un recordset à partir d'IDs déjà connus, sans requête SELECT supplémentaire — un point important pour la performance.

# Pattern 1 — search standard (domain + limit)
partners = env['res.partner'].search(
    [('is_company', '=', True)],
    limit=10,
)

# Pattern 2 — search_count (Odoo 19 introduit le paramètre limit)
total_companies = env['res.partner'].search_count(
    [('is_company', '=', True)],
)
# Borne le compte à 50 (utile pour tester l'existence d'au moins N résultats)
premier_50 = env['res.partner'].search_count(
    [('is_company', '=', True)],
    limit=50,
)

# Pattern 3 — browse direct par ID (aucune requête supplémentaire)
admin = env['res.users'].browse(1)
print(admin.login)
# Browse accepte aussi une liste d'IDs et retourne un recordset multi
trois_users = env['res.users'].browse([1, 2, 3])
print(len(trois_users))

La distinction search / browse est structurante. search traduit un domain en SELECT id FROM … et retourne le recordset. browse construit le recordset directement à partir des IDs, sans toucher la base : la requête n'est déclenchée qu'au premier accès à un champ. Différence critique en boucle — itérer sur browse(ids) sans prefetch déclenche autant de requêtes que d'enregistrements (problème N+1 classique).

4. Patterns 4 et 5 — search_fetch et prefetch optimisé

Odoo 19 introduit officiellement search_fetch dans le tronc commun de l'ORM. La méthode est définie ligne 1383 de odoo/orm/models.py. Sa signature combine un domain et une liste de champs à précharger en une seule descente SQL — l'équivalent en une ligne d'un search suivi d'un read.

# Pattern 4 — search_fetch v19 — combine search + prefetch en un seul appel
partners = env['res.partner'].search_fetch(
    [('country_id.code', '=', 'DZ')],
    ['name', 'email', 'phone', 'country_id'],
    limit=100,
)
for p in partners:
    # Aucun N+1 — les quatre champs sont déjà en cache après search_fetch
    print(p.name, p.email, p.phone)

# Pattern 5 — prefetch manuel via read() (style v17/v18, encore valide v19)
partners = env['res.partner'].search([('is_company', '=', True)])
# read() force le fetch en une requête, puis itérer librement
partners.read(['name', 'email'])
for p in partners:
    print(p.name, p.email)

💡 Astuce performance — Pourquoi search_fetch change la donne

L'évolution v19 de search consiste à fusionner chercher et précharger en une seule descente SQL. Sur une boucle parcourant 1 000 partenaires en affichant name et email, un search nu suivi de l'accès aux champs déclenche un SELECT par enregistrement, soit 1 001 requêtes. search_fetch regroupe le tout en deux requêtes. Gain mesuré régulier au-delà du facteur 5×, encore plus marqué si la latence réseau vers PostgreSQL est élevée. Source : odoo/orm/models.py lignes 1383 à 1390.

5. Patterns 6 à 8 — create, write, unlink

L'écriture en base depuis le shell suit un principe absolu : sans env.cr.commit() explicite, rien n'est persisté. Le code de sortie (cr.rollback() ligne 149 de shell.py) annule toute modification non validée. Ce comportement protège l'inspection mais piège quiconque oublie le commit après une opération destinée à durer.

# Pattern 6 — create
new_partner = env['res.partner'].create({
    'name': 'Client Test SARL',
    'email': 'test@example.com',
    'is_company': True,
})
env.cr.commit()  # SANS commit, rollback à la sortie — le partenaire disparaît

# Pattern 7 — write en lot sur un recordset filtré
env['res.partner'].search([('country_id', '=', False)]).write({
    'country_id': env.ref('base.dz').id,
})
env.cr.commit()

# Pattern 8 — unlink avec garde-fou (inspection avant suppression)
draft_orders = env['sale.order'].search([
    ('state', '=', 'draft'),
    ('date_order', '<', '2024-01-01'),
])
print(f"{len(draft_orders)} bons de commande à supprimer")
# Pause manuelle recommandée en mode interactif :
# input("Confirmer la suppression ? [Entrée pour continuer]")
draft_orders.unlink()
env.cr.commit()
Terminal Ubuntu affichant une session odoo-bin shell interactive avec exécution de env['res.partner'].search_count([]) et retour numérique
Figure 1 — Session interactive ./odoo-bin shell -c odoo.conf -d ma_base --shell-interface=ipython sur Odoo 19. Les variables env, self, odoo sont injectées au démarrage et listées par le shell. L'appel env['res.partner'].search_count([]) retourne le nombre total de partenaires en base sans charger aucun enregistrement en mémoire.

6. Patterns 9 et 10 — flush_model et invalidate_model

Le shell permet d'exécuter directement du SQL via env.cr.execute() — pratique pour des mises à jour massives qui passeraient mal par l'ORM. Mais l'ORM maintient un cache des champs lus : un UPDATE direct sans précaution laisse ce cache désynchronisé, et les lectures suivantes retournent l'ancienne valeur. Le pattern v19 encadre l'UPDATE par flush_model (force l'écriture des valeurs en attente) puis invalidate_model (vide le cache).

# Pattern 9 — UPDATE SQL direct propre (pattern v19 obligatoire)
env['res.partner'].flush_model(['name'])
env.cr.execute(
    "UPDATE res_partner SET name = UPPER(name) WHERE country_id = %s",
    (env.ref('base.dz').id,),
)
env['res.partner'].invalidate_model(['name'])
env.cr.commit()

# Pattern 10 — variante ciblée sur un recordset précis (flush_recordset)
records = env['res.partner'].search([('is_company', '=', True)], limit=5)
records.flush_recordset(['name', 'email'])
# UPDATE limité à ces 5 lignes
env.cr.execute(
    "UPDATE res_partner SET name = CONCAT('[B2B] ', name) WHERE id IN %s",
    (tuple(records.ids),),
)
records.invalidate_recordset(['name'])
env.cr.commit()

7. Patterns 11 et 12 — déclencher actions métier et post de facture

Au-delà du CRUD, le shell permet de déclencher toute méthode publique d'un modèle — simuler une action utilisateur. Confirmer un devis, poster une facture, valider un transfert de stock : toute la logique métier est accessible. Règle identique au CRUD — sans commit, l'opération est annulée à la sortie.

# Pattern 11 — confirmer un devis (équivaut à cliquer "Confirmer" en interface)
draft_so = env['sale.order'].search([('state', '=', 'draft')], limit=1)
draft_so.action_confirm()  # passe à 'sale', crée le stock.picking et les écritures
env.cr.commit()

# Pattern 12 — poster une facture brouillon
draft_invoice = env['account.move'].browse(123)
draft_invoice._post()  # numérotation, écritures comptables, suivi analytique
env.cr.commit()

Ces patterns sont précieux en reprise de données. Un import massif de devis arrive souvent en état draft ; un script shell qui itère sur le recordset et appelle action_confirm() termine la migration en quelques secondes — avec les mêmes effets de bord qu'une confirmation manuelle (stock, comptabilité, séquences).

8. Pattern 13 — domain composite v19 avec Domain

La notation polonaise inline (opérateurs |, &, ! avant les conditions) reste valide en v19. Mais l'API objet odoo.osv.expression et ses helpers OR() / AND() sont officiellement dépréciés depuis la v19. Le fichier odoo/osv/expression.py émet un DeprecationWarning à l'import (lignes 173, 242, 248). Remplaçant officiel : odoo.fields.Domain, qui réexporte la classe Domain définie dans odoo/orm/domains.py (voir ligne 20 de odoo/fields/__init__.py).

# DEPRECATED v19 — déclenche un DeprecationWarning
from odoo.osv.expression import OR, AND
domain = OR([[('state', '=', 'draft')], [('state', '=', 'sent')]])

# RECOMMANDÉ v19 — API objet Domain
from odoo.fields import Domain

d1 = Domain('state', '=', 'draft')
d2 = Domain('state', '=', 'sent')
domain = d1 | d2  # opérateur Python pour le OR logique
orders = env['sale.order'].search(domain)

# Combinaisons plus riches
clients_dz = Domain('country_id.code', '=', 'DZ')
companies = Domain('is_company', '=', True)
domain = clients_dz & companies  # opérateur Python pour le AND logique
partners_dz_companies = env['res.partner'].search(domain)

# Helpers statiques pour N domains
domain = Domain.OR([d1, d2, Domain('state', '=', 'done')])

# La forme inline polonaise reste valide v19
orders = env['sale.order'].search([
    '|',
    ('state', '=', 'draft'),
    ('state', '=', 'sent'),
])

9. Patterns 14 et 15 — script batch et fusion de doublons

Le mode batch — pipe stdin ou option --shell-file — transforme le shell en runner reproductible. Versionner le script dans Git, l'exécuter via cat script.py | ./odoo-bin shell …, archiver le log : chaque opération de masse devient auditable. Les deux patterns ci-dessous illustrent une correction de données et une fusion de doublons — cas récurrents en mission.

# Pattern 14 — script batch piped (fichier fix_partner_dz.py)
# Corrige le pays manquant pour tous les partenaires dont l'email
# est dans le domaine adicops.dz

partners = env['res.partner'].search_fetch(
    [('country_id', '=', False), ('email', 'ilike', '@adicops.dz')],
    ['name', 'email', 'country_id'],
)
for p in partners:
    p.country_id = env.ref('base.dz')
env.cr.commit()
print(f"Fixed: {len(partners)} partners DZ")

# Exécution :
# cat fix_partner_dz.py | ./odoo-bin shell -c odoo.conf -d ma_base
# Pattern 15 — fusion de doublons en 12 lignes (cas réel récurrent)
# Tous les partenaires partageant le même email fusionnent vers le premier ;
# les liens (devis, factures) sont réaffectés au master avant suppression
# des slaves.

doublons = env['res.partner'].search([
    ('email', '=', 'contact@example.com'),
])
if len(doublons) > 1:
    master = doublons[0]
    slaves = doublons[1:]
    # Réaffectation des devis liés
    env['sale.order'].search([
        ('partner_id', 'in', slaves.ids),
    ]).write({'partner_id': master.id})
    # Réaffectation des factures liées
    env['account.move'].search([
        ('partner_id', 'in', slaves.ids),
    ]).write({'partner_id': master.id})
    # Suppression des doublons
    slaves.unlink()
    env.cr.commit()
    print(f"Fusionné {len(slaves)} doublons vers id={master.id}")
Terminal Ubuntu affichant le résultat d'un script Python piped via cat fix_partner_dz.py | odoo-bin shell, avec la ligne de sortie Fixed: 12 partners DZ
Figure 2 — Exécution batch du script fix_partner_dz.py via pipe stdin : cat fix_partner_dz.py | ./odoo-bin shell -c odoo.conf -d ma_base --shell-interface=python. La détection automatique du mode non-TTY déclenche l'exécution complète du script. La dernière ligne affichée — Fixed: 12 partners DZ — permet l'audit du nombre exact de partenaires corrigés.

10. Shell, XML-RPC ou SQL — matrice de décision

Le shell ORM n'est pas la seule porte d'entrée sur une base Odoo. Trois canaux coexistent, chacun pertinent dans un contexte précis. La matrice ci-dessous fixe la règle de choix.

Cas d'usageOutilPourquoi
Exploration interactive en mission odoo-bin shell --shell-interface=ipython REPL avec autocomplétion, historique, accès direct à env super-user
Script batch reproductible (correction de données, import) cat script.py | odoo-bin shell Versionnable dans Git, exécutable identiquement en dev et prod, log archivable
Intégration externe (Postman, n8n, autre service) XML-RPC ou JSON-RPC /web/dataset/call_kw Pas d'accès SSH requis, authentification par login/key, langage agnostique
Audit massif ou requête cross-modèles complexe psql direct (lecture seule) Performance des jointures SQL natives, EXPLAIN disponible — épisode S05
Fix de données en production JAMAIS sans backup validé Sauvegarde pg_dump + filestore avant tout commit — détails épisode S05

11. Les 15 patterns en synthèse

Le tableau ci-dessous condense les quinze patterns. Mémoriser le cas d'usage suffit à retrouver l'incantation correspondante sous pression.

#PatternCas d'usage
1Model.search(domain, limit=N)Filtrer un recordset par domain
2Model.search_count(domain, limit=N)Compter sans charger, avec borne v19
3Model.browse(ids)Recordset par IDs déjà connus
4Model.search_fetch(domain, fields, limit=N)Search + prefetch en une descente SQL (v19)
5recordset.read([fields])Forcer le prefetch en style v17/v18
6Model.create({...})Créer un enregistrement
7recordset.write({...})Modifier en lot sur un recordset filtré
8recordset.unlink()Supprimer après garde-fou d'inspection
9flush_model → SQL → invalidate_modelUPDATE SQL direct, cache ORM synchronisé
10flush_recordset + invalidate_recordsetVariante ciblée sur enregistrements précis
11recordset.action_xxx()Déclencher une action métier publique
12account_move._post()Poster une facture brouillon
13from odoo.fields import DomainDomain composite v19 (remplace osv.expression)
14cat script.py | odoo-bin shellMode batch reproductible
15Recherche → réaffectation → unlinkFusion de doublons en 12 lignes

Le pattern UPDATE SQL direct encadré par flush_model / invalidate_model est repris en détail dans le prochain épisode S05, consacré aux requêtes psql. Le shell ORM et psql sont les deux faces d'une même médaille — l'ORM pour la logique métier transactionnelle, psql pour l'audit en lecture seule.

Voir aussi dans la série

Publié
S01 — Linux : 30 commandes pour la sandbox

logs, processus, ports, permissions, systemd, alias .bashrc.

Publié
S02 — Git & GitHub : 20 commandes pour cloner, brancher, contribuer

clone shallow, submodules OCA, workflow feature-branch, PR via gh.

Publié
S03 — Bash : 5 scripts qui font gagner 1 heure par jour

start-odoo.sh, restore-db.sh, dump-and-clean.sh, install-and-test.sh, update-all-addons.sh.

Article actuel
S04 — odoo-bin shell : 15 patterns ORM essentiels

env, search_fetch v19, Domain, flush_model / invalidate_model.

Publié
S05 — PostgreSQL : psql, SELECT, dump propre

Requêtes psql, EXPLAIN ANALYZE, garde-fous UPDATE / DELETE, CTA E3.

Articles complémentaires

#59 — Modèles de base Odoo 19 : Model, TransientModel, AbstractModel

Fondations ORM — modèles dont le shell manipule les enregistrements.

#66 — Méthodes de modèle Odoo 19 : create, write, unlink

Méthodes ORM appelées en shell — décorateur @api.model_create_multi v19.

Se connecter pour laisser un commentaire.
Bash pour Odoo : 5 scripts qui font gagner 1 heure par jour
Série Tech · Article 3/5 · Boîte à outils Linux du dev Odoo