Se rendre au contenu

Champs non-relationnels Odoo 19 : Char, Float, Date, Html, Monetary…

Bloc 3 · Framework ORM — Article 3/8 Champs non-relationnels Odoo 19 : Char, Float, Date, Html, Monetary… Odoo propose 12 types de champs scalaires .
26 avril 2026 par
Champs non-relationnels Odoo 19 : Char, Float, Date, Html, Monetary…
B.Mustapha

Bloc 3 · Framework ORM — Article 3/8

Champs non-relationnels Odoo 19 : Char, Float, Date, Html, Monetary…

Odoo propose 12 types de champs scalaires. Chacun a ses paramètres, ses pièges et son comportement en base PostgreSQL. On les ajoute tous au module odooskills_helpdesk.

~16 minutes de lecture

Ce que tu vas apprendre

Texte

Char, Text, Html — différences, paramètres clés, stockage PG.

Nombres

Integer, Float, Monetary et le piège currency_field.

Temps

Date vs Datetime : UTC en base, fuseau utilisateur à l'affichage.

Fichiers

Binary, Imageattachment=True, max_width, stockage ir.attachment.

Prérequis
  • Avoir lu T09 — Attributs de modèles
  • Le module odooskills_helpdesk installé (version 19.0.1.1.0 minimum)
  • Un environnement Odoo 19 fonctionnel
Le module fil rouge — Cet article fait passer odooskills_helpdesk de la version 19.0.1.1.0 à 19.0.1.2.0. On ajoute 12 champs scalaires au modèle helpdesk.ticket.

Les types de champs scalaires, en un coup d'œil

Chaque type Odoo correspond à un type PostgreSQL précis. La colonne Odoo Field est ce que tu écris en Python ; la colonne PG Type est ce qui est stocké en base.

Champs non-relationnels Odoo 19 → PostgreSQL Odoo Field PG Type Paramètres clés Remarques v19 Char VARCHAR size=N, required, tracking Pas de size → VARCHAR illimité Text TEXT translate=True (i18n) Textarea dans les vues Form Html TEXT sanitize=True (défaut) WYSIWYG Odoo, XSS filtré auto Integer INTEGER default=0, group_operator Jamais NULL si default=0 Float NUMERIC digits=(précision, décimales) digits=(6,2) → max 9999.99 Monetary NUMERIC currency_field='currency_id' ⚠ currency_field OBLIGATOIRE Boolean BOOLEAN default=False Jamais NULL (False si omis) Selection VARCHAR selection=[...], tracking tracking trace chaque changement Date DATE fields.Date.today() YYYY-MM-DD, pas d'heure Datetime TIMESTAMP fields.Datetime.now() ⚠ UTC en base, TZ à l'affichage Binary BYTEA / ir.attachment attachment=True (recommandé) Sans attachment → dans la colonne PG Image BYTEA / ir.attachment max_width=N, max_height=N Redimensionnement auto au save Texte Nombres Booléen / Liste Temps Fichiers

Paramètres communs à tous les champs

Avant de détailler chaque type, voici les paramètres que tu rencontreras sur n'importe quel champ Odoo :

name = fields.Char(
    string='Libellé affiché',   # Label dans les vues (défaut : nom de l'attribut)
    required=True,               # Champ obligatoire (NOT NULL côté ORM)
    readonly=True,               # Non modifiable par l'utilisateur
    copy=False,                  # Exclu lors du duplicate d'un record
    default='valeur',            # Valeur par défaut (scalaire ou callable)
    index=True,                  # Crée un index PG (btree) sur la colonne
    tracking=True,               # Trace les changements dans le chatter mail
    help='Texte d\'aide tooltip', # Tooltip dans les formulaires
)
Astuce default — Pour un default dynamique (date du jour, utilisateur courant…), passe un callable :
default=fields.Date.today (sans parenthèses) ou default=lambda self: self.env.user.id

1. Char — texte court

fields.Char stocke une chaîne courte en VARCHAR PostgreSQL. Sans paramètre size, la colonne est illimitée.

name      = fields.Char(string='Sujet',     required=True, tracking=True)
reference = fields.Char(string='Référence', copy=False,    index=True)

Le paramètre size est facultatif :

# Limité à 50 caractères en BDD
code = fields.Char(string='Code interne', size=50)
Piègesize n'est pas validé côté Python avant l'INSERT ; la troncature se fait au niveau PostgreSQL et lève une exception value too long for type character varying(N). Préfère une contrainte @api.constrains si tu veux un message d'erreur propre.

2. Text et Html — texte long

Les deux sont stockés en TEXT PostgreSQL, mais l'interface les traite différemment :

  • Text → textarea simple, pas de formatage
  • Html → éditeur WYSIWYG Odoo (Quill/OWL) avec gras, listes, liens…
# Texte brut — note de résolution sans mise en forme
resolution_note = fields.Text(string='Note de résolution')

# HTML riche — corps du ticket avec formatage
body_html = fields.Html(string='Corps HTML', sanitize=True)
Sécurité sanitize=True — Ce paramètre (activé par défaut sur Html) filtre les balises et attributs HTML dangereux (<script>, gestionnaires d'événements…). Ne le désactive que si tu maîtrises entièrement la source du contenu.

3. Integer et Float — nombres

# Integer → INTEGER PG, jamais NULL si default=0
incident_count = fields.Integer(string='Nb incidents liés', default=0)

# Float → NUMERIC PG avec précision contrôlée
# digits=(total_chiffres, decimales)  →  (6, 2) = max 9999.99
hours_spent = fields.Float(string='Heures passées', digits=(6, 2))

Différence Float vs Integer en PG

En Python, un champ Integer retourne toujours 0 quand il est vide (jamais False ou None). Un champ Float retourne 0.0.

group_operator — Les champs numériques ont un paramètre group_operator qui définit l'agrégation dans les vues groupées : 'sum' (défaut), 'avg', 'min', 'max', 'count'.
hours_spent = fields.Float(..., group_operator='sum')

4. Monetary — montant en devise

Monetary est un Float enrichi : il connaît sa devise et formate l'affichage (1 500,00 €). En base, c'est un NUMERIC, mais Odoo utilise la précision définie par la devise (ex. 2 décimales pour EUR).

Règle absolue — Un champ Monetary doit toujours être accompagné d'un champ currency_id de type Many2one('res.currency', ...) et pointer dessus via currency_field='currency_id'. Sans ça, l'interface affiche une erreur et le widget ne formate rien.
# Le montant — lié à la devise via currency_field
estimated_cost = fields.Monetary(
    string='Coût estimé',
    currency_field='currency_id',   # NOM de l'attribut Python, pas le champ PG
)

# Le Many2one devise — obligatoire pour Monetary
currency_id = fields.Many2one(
    comodel_name='res.currency',
    string='Devise',
    default=lambda self: self.env.company.currency_id,
)

Le default lambda récupère la devise de la société courante au moment de la création du record. Si tu travailles en multi-sociétés, l'utilisateur peut changer la devise manuellement dans le formulaire.

5. Boolean et Selection

Boolean

Un booléen en Odoo n'est jamais NULL en base. Si tu oublies default, Odoo insère False automatiquement. Il s'affiche sous forme de case à cocher dans les vues.

is_urgent = fields.Boolean(string='Urgent', default=False, tracking=True)

Selection

Une liste fermée de couples (valeur_stockée, libellé). La valeur stockée (clé technique) est ce qui va en base — garde-la courte et stable. Le libellé est traduit par Odoo.

channel = fields.Selection(
    selection=[
        ('email',  'Email'),
        ('phone',  'Téléphone'),
        ('portal', 'Portail'),
        ('chat',   'Chat'),
    ],
    string='Canal',
    tracking=True,   # chaque changement de canal apparaît dans le chatter
)
Ajouter des valeurs dynamiques — En v19, tu peux passer une méthode comme selection='_selection_channel' pour construire la liste à la volée. Utile si les valeurs dépendent du contexte ou de la configuration.

6. Date et Datetime

La distinction principale : Date stocke juste le jour (YYYY-MM-DD), Datetime stocke date + heure.

# Date : uniquement YYYY-MM-DD, pas de fuseau horaire
deadline = fields.Date(string='Échéance', tracking=True)

# Datetime : date + heure UTC en base
# Odoo convertit automatiquement vers le fuseau de l'utilisateur à l'affichage
resolved_at = fields.Datetime(string='Résolu le', readonly=True)
UTC en base, TZ à l'affichage — Odoo stocke toujours les Datetime en UTC dans PostgreSQL (TIMESTAMP WITHOUT TIME ZONE). La conversion vers le fuseau de l'utilisateur se fait côté client. En Python (méthodes server-side), les datetime sont donc en UTC :
from odoo import fields

# Bonne pratique : utiliser fields.Datetime.now() côté serveur
self.resolved_at = fields.Datetime.now()   # retourne datetime UTC

# Comparer avec une date du jour
today = fields.Date.today()                # retourne date YYYY-MM-DD

Exemple pratique : marquer un ticket comme résolu

def action_close(self):
    """Passe le ticket en 'done' et enregistre la date/heure de résolution."""
    self.write({
        'state': 'done',
        'resolved_at': fields.Datetime.now(),
    })

7. Binary et Image — fichiers

Binary

Stocke n'importe quel fichier binaire. Deux modes :

  • Sans attachment=True → données encodées en base64 directement dans la colonne PG (BYTEA). Déconseillé pour les gros fichiers.
  • Avec attachment=True → données déportées dans ir.attachment, la colonne PG ne contient qu'une référence. C'est le mode recommandé.
# Fichier binaire — stocké dans ir.attachment (hors colonne PG)
screenshot = fields.Binary(
    string='Capture d\'écran',
    attachment=True,   # recommandé pour tout fichier > quelques Ko
)

Image

Image est une spécialisation de Binary qui ajoute le redimensionnement automatique : à chaque sauvegarde, Odoo redimensionne l'image si elle dépasse max_width × max_height.

avatar = fields.Image(
    string='Avatar',
    max_width=256,    # largeur max en pixels
    max_height=256,   # hauteur max en pixels
    # attachment=True est implicite pour Image
)
Piège Image — Le redimensionnement se déclenche au save du record, pas à l'affichage. Si tu oublies max_width/max_height, des photos haute résolution (5 Mo+) seront stockées telles quelles et ralentiront le chargement des listes. Pour les avatars et thumbnails, 128×128 ou 256×256 est suffisant.

Image avec variantes tailles

Le pattern natif Odoo pour les images avec plusieurs tailles (utilisé dans res.partner) :

image_1920 = fields.Image(max_width=1920, max_height=1920)
image_128  = fields.Image(related='image_1920', max_width=128,  max_height=128,  store=True)
image_256  = fields.Image(related='image_1920', max_width=256,  max_height=256,  store=True)

Mise en pratique — helpdesk_ticket.py complet

Voici le fichier complet après T10, version 19.0.1.2.0 :

from odoo import models, fields


class HelpdeskTicket(models.Model):
    _name = 'helpdesk.ticket'
    _description = 'Ticket Helpdesk'
    _inherit = ['mail.thread', 'mail.activity.mixin', 'odooskills.helpdesk.mixin']
    _order = 'priority desc, create_date desc'
    _rec_name = 'name'

    _unique_reference = models.Constraint(
        'UNIQUE (reference)',
        "La référence du ticket doit être unique.",
    )

    # ── Champs de base (T08/T09) ──────────────────────────────────────────────
    name = fields.Char(string='Sujet', required=True, tracking=True)
    reference = fields.Char(string='Référence', copy=False, index=True)
    description = fields.Text(string='Description')
    category_id = fields.Many2one(
        comodel_name='helpdesk.ticket.category',
        string='Catégorie',
        ondelete='restrict',
    )
    partner_id = fields.Many2one('res.partner', string='Client')
    state = fields.Selection(
        selection=[
            ('new', 'Nouveau'),
            ('in_progress', 'En cours'),
            ('done', 'Résolu'),
        ],
        default='new',
        required=True,
        tracking=True,
    )

    # ── Champs non-relationnels (T10) ─────────────────────────────────────────

    deadline = fields.Date(string='Échéance', tracking=True)
    resolved_at = fields.Datetime(string='Résolu le', readonly=True)
    hours_spent = fields.Float(string='Heures passées', digits=(6, 2))
    incident_count = fields.Integer(string='Nb incidents liés', default=0)
    is_urgent = fields.Boolean(string='Urgent', default=False, tracking=True)

    channel = fields.Selection(
        selection=[
            ('email', 'Email'),
            ('phone', 'Téléphone'),
            ('portal', 'Portail'),
            ('chat', 'Chat'),
        ],
        string='Canal',
        tracking=True,
    )

    resolution_note = fields.Text(string='Note de résolution')
    body_html = fields.Html(string='Corps HTML', sanitize=True)
    screenshot = fields.Binary(string="Capture d'écran", attachment=True)
    avatar = fields.Image(string='Avatar', max_width=256, max_height=256)

    estimated_cost = fields.Monetary(
        string='Coût estimé',
        currency_field='currency_id',
    )
    currency_id = fields.Many2one(
        comodel_name='res.currency',
        string='Devise',
        default=lambda self: self.env.company.currency_id,
    )

Upgrade du module

On passe de 19.0.1.1.0 à 19.0.1.2.0 dans le manifest :

'version': '19.0.1.2.0',

Puis on met à jour :

./odoo-bin -c config/odoo.conf -u odooskills_helpdesk -d ta_base --stop-after-init

Odoo détecte automatiquement les nouveaux champs et crée les colonnes PG (ALTER TABLE helpdesk_ticket ADD COLUMN ...). Aucune migration manuelle n'est nécessaire pour des ajouts de champs.

Vérification rapide — Après le -u, tu peux vérifier la structure de la table en psql :
\d helpdesk_ticket

Récapitulatif — choisir le bon type

Besoin Champ Odoo Piège principal
Texte court (nom, code…)Charsize tronque côté PG sans message propre
Texte long sans formatageText
Texte riche (gras, listes…)HtmlDésactiver sanitize = risque XSS
EntierIntegerRetourne 0, jamais None
DécimalFloatSans digits, arrondi imprécis possible
Montant en deviseMonetarycurrency_field obligatoire
Vrai/FauxBooleanJamais NULL, toujours False si omis
Liste ferméeSelectionChanger une clé = migration des données existantes
Date seuleDateNe pas y mettre une heure
Date + heureDatetimeUTC en base, convertir avec fields.Datetime.now()
Fichier quelconqueBinary + attachment=TrueSans attachment=True → stocké en colonne BYTEA
Image avec resize autoImageOublier max_width/height = images pleine taille stockées

Prochain article — T11

On aborde les champs relationnels : Many2one, One2many, Many2many. Comment relier les modèles entre eux et quand utiliser chaque type de relation.

Télécharger le guide technique Odoo 19 (PDF gratuit)
Attributs de modèles Odoo 19 : _order, _rec_name, Constraint
Bloc 3 · Framework ORM — Article 2/8 Attributs de modèles Odoo 19 : _order, _rec_name, Constraint Au-delà des trois classes de base, un modèle Odoo se…