Pendant des années, un champ fields.Binary d'Odoo a renvoyé une chaîne base64. Tout le monde a écrit base64.b64decode(record.file) et base64.b64encode(content) sans y penser, dans les contrôleurs, les imports, les exports EDI et les scripts d'intégration. Avec Odoo 20, ce réflexe devient une source de bugs. Les champs binaires manipulent désormais un objet BinaryValue qui contient des octets bruts, le champ ir.attachment.datas n'existe plus et le format RPC des binaires a changé en lecture1.
Le chantier s'est étalé sur plusieurs versions Odoo Online (19.2, 19.3, puis le label 19.5) avant d'arriver dans la branche 20.0. Si vous migrez depuis Odoo 19.0, vous recevez tout d'un coup.
Trois PR, une seule logique
La première étape (PR #235832, fusionnée le 23 janvier 2026) a interdit de stocker n'importe quoi dans un champ Binary. Certains modules y rangeaient un dictionnaire qu'ils modifiaient ensuite en cache. L'auteur résume l'intention ainsi : « Binary should store bytes, not anything ». Les modules concernés ont été basculés vers fields.Json2.
La deuxième (PR #244421, labellisée 19.3) introduit BinaryValue. Selon sa description, l'objet est chargé paresseusement et n'amène le contenu en mémoire qu'au moment où on le lit. Le stockage passe en octets bruts, ce qui représente un gain de 33 % par rapport au base64 d'après l'auteur, et le base64 ne sert plus qu'au transport RPC3.
La troisième (PR #266082, fusionnée le 24 août 2026) ajoute BinaryValue.filename et modifie la sérialisation RPC4. Entre les deux, la PR #245916, fusionnée le 11 mars 2026, a supprimé ir.attachment.datas au profit de raw5.
Ce que l'objet BinaryValue expose
La PR #244421 documente une API courte, qu'il vaut mieux connaître avant de toucher au code :
| Besoin | Avant (base64) | Avec BinaryValue |
|---|---|---|
| Lire les octets | base64.b64decode(rec.file) | rec.file.contents ou bytes(rec.file) |
| Obtenir du base64 | valeur brute du champ | rec.file.to_base64() |
| Connaître la taille | len() sur la chaîne, ou contexte bin_size | rec.file.size |
| Lire comme un fichier | io.BytesIO(base64.b64decode(...)) | rec.file.open() |
| Type MIME | détection manuelle | rec.file.mimetype |
| Tester la présence | if rec.file: | if rec.file: (une valeur vide reste fausse) |
BinaryValue hérite de collections.abc.Buffer. Vous pouvez donc le passer directement à hashlib.sha256(), memoryview() ou io.BytesIO(). La propriété size existe parce qu'un len() forcerait le chargement du contenu, ce qui annulerait l'intérêt du chargement paresseux.
Le piège de l'écriture
C'est en écriture que les modules spécifiques vont casser le plus discrètement. Un octet brut et une chaîne base64 ne se distinguent pas toujours, et la PR tranche de façon nette. Une valeur str est interprétée comme du base64 et décodée. Pour écrire des octets bruts dans un champ binaire classique, il faut les envelopper dans BinaryBytes, qui accepte n'importe quel buffer3. Seul le champ raw des pièces jointes accepte encore des bytes nus, puisque son usage ne laisse aucune ambiguïté.
Concrètement, un code qui faisait rec.file = base64.b64encode(pdf) produit un objet bytes, pas une chaîne. Ce cas passait sans bruit en version 17 ou 18 et mérite un test dédié dans chaque module.
Côté pièces jointes, la règle est simple. Partout où vous lisiez attachment.datas, vous lisez attachment.raw, et vous appelez attachment.raw.to_base64() si un service tiers attend vraiment du base645. Le contexte bin_size ne s'applique plus qu'à Model.read(), les autres champs binaires étant lus en entier.
Vos intégrations externes sont aussi concernées
C'est le point que beaucoup d'équipes vont découvrir en production. Depuis la PR #266082, un read RPC sur un champ binaire ne renvoie plus une chaîne. Il renvoie un dictionnaire avec content, filename et size, auquel un commit ultérieur a ajouté un checksum SHA-2564. Côté interface web, web_read omet même le contenu, que le navigateur récupère séparément et peut mettre en cache.
L'écriture reste compatible. Une chaîne base64 est toujours acceptée, et le nouveau format {filename, content} permet en plus de nommer le fichier. Quand le champ est stocké en pièce jointe, ce nom alimente ir.attachment.name, ce qui rend inutiles les champs x_filename que beaucoup de modèles traînent à côté de leurs binaires.
En revue, un contributeur a demandé si ce changement cassait l'API externe. L'auteur l'a reconnu pour la lecture, en expliquant que l'ancien format ne pouvait pas être étendu autrement4. Le conseil donné dans la discussion de la PR #245916 est clair : utiliser datas avant 20.0 et raw à partir de 20.05. Un connecteur qui parle à plusieurs versions d'Odoo doit donc tester le type renvoyé, chaîne ou dictionnaire, plutôt que supposer un format.
Une méthode d'audit en quatre recherches
Sur un dépôt de modules spécifiques, quatre recherches couvrent l'essentiel du risque :
b64decodeetb64encodeà proximité d'un champ Binary ou d'une pièce jointe.- Toutes les occurrences de
.datas, y compris dans les vues XML, les rapports QWeb et les tests. - Le contexte
bin_size, qui ne se comporte plus de la même façon. - Les appels RPC sortants ou entrants (scripts, middleware, ETL) qui lisent un champ binaire et s'attendent à une chaîne.
Le quatrième point est le plus sous-estimé, parce que ce code vit rarement dans le dépôt Odoo lui-même.
Notre lecture
Le changement est sain sur le fond. Stocker du base64 en base gonflait les volumes d'un tiers et imposait des conversions à chaque passage dans l'ORM, ce que la PR #244421 corrige. Le nom de fichier attaché à la valeur supprime aussi un motif de duplication que nous retrouvons dans presque tous les projets.
Le coût de migration est réel, et il est mal réparti. Le code Python d'un module se corrige en une journée avec les bonnes recherches. Les intégrations externes, elles, cassent en lecture sans prévenir et souvent chez un prestataire tiers. Avant une montée vers Odoo 20, nous inventorions chaque flux qui lit un fichier ou une pièce jointe par API, et nous le testons contre une base de recette. C'est moins visible que la refonte des droits d'accès, mais c'est là que les tickets du lendemain de bascule apparaîtront.
Sources
- Odoo Documentation 20.0, ORM Changelog : https://www.odoo.com/documentation/20.0/developer/reference/backend/orm/changelog.html
- GitHub odoo/odoo, PR #235832 : https://github.com/odoo/odoo/pull/235832
- GitHub odoo/odoo, PR #244421 : https://github.com/odoo/odoo/pull/244421
- GitHub odoo/odoo, PR #245916 : https://github.com/odoo/odoo/pull/245916
- GitHub odoo/odoo, PR #266082 : https://github.com/odoo/odoo/pull/266082
Footnotes
-
Le changelog ORM officiel de la documentation 20.0 liste ces évolutions pour les versions Odoo Online 19.2, 19.3 et pour la version 20.0. ↩
-
PR odoo/odoo #235832, « [IMP] orm, *: fields.Binary value is bytes », ouverte le 14 novembre 2025 et fusionnée le 23 janvier 2026. ↩
-
PR odoo/odoo #244421, « [IMP] core, *: fields.Binary return BinaryValue », labellisée 19.3. Le chiffre de 33 % est l'estimation de l'auteur de la PR. ↩ ↩2
-
PR odoo/odoo #266082, « [IMP] orm: BinaryValue.filename and RPC serialization of raw », labellisée 19.5, approuvée et fusionnée le 24 août 2026. ↩ ↩2 ↩3
-
PR odoo/odoo #245916, « [IMP] *: remove datas », fusionnée le 11 mars 2026. ↩ ↩2 ↩3