Quand vous filtrez des enregistrements liés dans Odoo, deux écritures coexistent depuis plusieurs versions. La notation pointée, du type [('partner_id.country_id.code', '=', 'FR')], reste la plus répandue. Depuis Odoo 17, l'opérateur any propose une autre voie pour traverser une relation avec un domaine imbriqué. Odoo 19 ajoute une variante que beaucoup de développeurs vont croiser sans en mesurer la portée : any!. Le point d'exclamation n'a rien de cosmétique, car il modifie le comportement de sécurité de la requête.
Ce que fait déjà any
L'opérateur any existe dans le cœur depuis Odoo 17. On le retrouve dans le tuple TERM_OPERATORS du fichier odoo/osv/expression.py, aux côtés de not any, en 17.0 comme en 18.0 1. Il sert à interroger un champ relationnel (many2one, one2many, many2many) ou le champ id, avec un domaine imbriqué.
Concrètement, [('order_line', 'any', [('product_id.type', '=', 'consu')])] sélectionne les commandes dont au moins une ligne porte un produit consommable. Le domaine placé en troisième position s'évalue sur le comodèle du champ. La forme not any retourne l'inverse : aucun enregistrement lié ne satisfait la condition. La documentation ORM précise d'ailleurs que la traversée classique par notation pointée revient en interne à un any. Écrire any de façon explicite rend surtout le domaine plus lisible quand la condition imbriquée devient longue, et évite les chaînes de points à rallonge.
Un détail mérite attention dès cette version. Lorsque la valeur est un domaine appliqué à un many2one ou à id, la recherche interne s'exécute avec active_test=False. Autrement dit, la traversée voit aussi les enregistrements archivés du comodèle.
Ce que réorganise Odoo 19
Odoo 19 déplace la logique des domaines. Le code historique de odoo/osv/expression.py migre vers un nouveau module, odoo/orm/domains.py. La liste des opérateurs reconnus s'appelle désormais STANDARD_CONDITION_OPERATORS, et elle contient quatre entrées pour cette famille : any, not any, any! et not any! 2. Les deux dernières sont nouvelles. Si vous migrez un module qui monkey-patchait expression.py, ce changement d'emplacement vous concerne directement.
any! : la traversée qui saute les règles d'accès
La différence tient en une phrase du code source. La docstring de domains.py indique que any! fonctionne comme any, mais qu'il évite d'ajouter les règles d'enregistrement sur le comodèle 3. En clair, la recherche imbriquée ne passe pas par les ir.rule définies sur le modèle lié.
Rappelons l'enjeu. Une règle d'enregistrement restreint les lignes qu'un utilisateur donné a le droit de voir. Avec un any ordinaire, le domaine imbriqué s'évalue en tenant compte de ces règles : un commercial qui ne voit que ses propres commandes ne matchera jamais un parent à travers un enregistrement lié qui lui est masqué. Avec any!, ce filtrage de sécurité est court-circuité. La correspondance du parent se calcule sur l'ensemble des données du comodèle, sans considérer qui interroge.
Le cœur d'Odoo emploie any! dans des cas précis : quand la valeur passée est déjà un objet SQL ou une Query, ou quand le champ porte l'attribut bypass_search_access. Ce sont des situations où le contrôle d'accès est censé avoir été traité en amont. Le problème apparaît quand un développeur reprend any! dans un domaine métier custom sans mesurer la portée. Le filtre semble alors « mieux marcher », puisqu'il remonte davantage de résultats, mais il peut révéler l'existence d'enregistrements qu'un profil ne devrait pas atteindre, ou classer un parent selon des enfants qui lui sont invisibles.
Le cas des enregistrements archivés
Le comportement active_test=False sur les many2one et id reste une source de surprises. Un domaine censé ignorer les partenaires archivés continuera de matcher à travers eux, parce que la traversée relationnelle les inclut. Le sujet est vivant côté dépôt : une correction récente ajuste justement la prise en compte des enregistrements archivés dans la recherche relationnelle par any 4. Pour les champs x2many, la logique diffère légèrement, la recherche s'appuyant sur le contexte du champ plutôt que sur un active_test forcé. Cette asymétrie vaut d'être testée sur vos propres modèles avant mise en production.
Notre lecture
Dans du code custom, any explicite gagne en lisibilité face aux chaînes pointées profondes, et nous le recommandons pour les domaines imbriqués un peu denses. any! relève d'un autre régime. Il ne doit pas servir à faire taire un utilisateur qui se plaint de résultats manquants, car ce résultat absent est peut-être une règle d'enregistrement qui fait son travail. Réservez-le aux cas où vous maîtrisez les données interrogées et où vous assumez de passer à côté des ir.rule. Le réflexe de validation reste le même que pour toute évolution ORM 19 : rejouez vos filtres sous un utilisateur à droits restreints, et vérifiez le comportement sur les enregistrements archivés. Pour le reste des ajustements ORM de cette version, nous détaillons les points qui touchent les modules dans notre panorama des changements ORM d'Odoo 19.
Sources
- odoo/odoo, orm/domains.py (branche 19.0) : https://github.com/odoo/odoo/blob/19.0/odoo/orm/domains.py
- odoo/odoo, osv/expression.py (branche 18.0) : https://github.com/odoo/odoo/blob/18.0/odoo/osv/expression.py
- odoo/odoo, osv/expression.py (branche 17.0) : https://github.com/odoo/odoo/blob/17.0/odoo/osv/expression.py
- odoo/odoo, PR #290380 « include archived records from relational 'any' domain search » : https://github.com/odoo/odoo/pull/290380
Footnotes
-
Tuple
TERM_OPERATORSdu fichierodoo/osv/expression.py, branches 17.0 et 18.0 du dépôt officiel : présence deanyetnot any, absence deany!. ↩ -
Ensemble
STANDARD_CONDITION_OPERATORSdu fichierodoo/orm/domains.py, branche 19.0 :any,not any,any!,not any!. ↩ -
Docstring de
odoo/orm/domains.py(19.0) : «any!works likeanybut bypass adding record rules on the comodel ». La même docstring précise le_searchavecactive_test=Falsepour un domaine sur many2one ouid. ↩ -
Pull request de correction sur le dépôt odoo/odoo visant à inclure les enregistrements archivés dans la recherche relationnelle par
any. ↩