Odoo · Modelado · Campos
Casi todo modelo de negocio tiene un estado: borrador, confirmado, cancelado; abierto, cerrado; en ruta, entregado. La decisión de cómo guardarlo parece trivial y condiciona mucho: si los valores se pueden traducir, si el usuario puede añadir uno nuevo, si el código puede confiar en ellos y qué pasa cuando una API externa envía un valor que nadie había visto antes.
Selection
Cuando el código depende de cada valor
Si tu código hace if trip.state == 'confirmed', el conjunto de valores lo define el código y debe ser un Selection: claves estables en inglés que no cambian nunca, y etiquetas traducibles que sí pueden cambiar. Es lo que usan sale.order, account.move y casi todos los flujos del core.
state = fields.Selection(
[
("draft", "Draft"),
("confirmed", "Confirmed"),
("done", "Done"),
("cancel", "Cancelled"),
],
default="draft", required=True, tracking=True, index=True,
)
Si otro módulo necesita añadir un valor, usa selection_add. En un campo almacenado debes indicar qué hacer con los registros que tengan ese valor cuando el módulo se desinstale:
class FleetTrip(models.Model):
_inherit = "fleet.trip"
state = fields.Selection(
selection_add=[("on_hold", "On Hold"), ("done",)],
ondelete={"on_hold": "set default"},
)
Many2one
Cuando los valores los gestiona el usuario
Si cada equipo quiere sus propias columnas en el kanban, o el responsable de calidad necesita añadir un motivo de rechazo sin pedir un desarrollo, los valores son datos, no código: van en un modelo de configuración y el estado es un Many2one. Es el patrón de las etapas de CRM y Proyecto. El precio es que el código ya no puede hacer if stage == 'won': necesita un atributo en la etapa (is_won, fold) que exprese el significado.
class FleetTripStage(models.Model):
_name = "fleet.trip.stage"
_description = "Trip Stage"
_order = "sequence, id"
name = fields.Char(required=True, translate=True)
sequence = fields.Integer(default=10)
fold = fields.Boolean()
is_closed = fields.Boolean(help="Trips in this stage count as finished.")
# En fleet.trip
stage_id = fields.Many2one("fleet.trip.stage", group_expand="_read_group_stage_ids")
stage_id.is_closed, nunca el nombre de la etapa. El nombre lo puede cambiar cualquier usuario, y además se traduce.
Char + Selection
Cuando el valor viene de una API externa
Los estados que envía un fabricante ("CLOSED", "AJAR", "FAULT_DETECTED") no los controlas tú. Un Selection estricto rechazaría el primer valor nuevo y rompería la sincronización. Lo que funciona es guardar el literal crudo en un Char, fiel a lo que dijo la API, y añadir al lado un Selection calculado mediante un mapeo tolerante, con un valor unknown para lo que no reconoce.
DOOR_STATUS = {"CLOSED": "closed", "OPEN": "open", "AJAR": "open"}
def map_status(mapping, raw):
if not raw:
return False
value = mapping.get(raw.upper())
if value is None:
_logger.info("Unknown status %r, mapped to 'unknown'", raw)
return "unknown"
return value
class VehicleDoor(models.Model):
_name = "vehicle.door"
_description = "Vehicle Door"
status_raw = fields.Char("Status (raw)", groups="base.group_no_one")
status = fields.Selection(
[("open", "Open"), ("closed", "Closed"), ("unknown", "Unknown")],
compute="_compute_status", store=True,
)
@api.depends("status_raw")
def _compute_status(self):
for door in self:
door.status = map_status(DOOR_STATUS, door.status_raw)
Centralizar el mapeo en un único sitio tiene un beneficio que no se ve hasta que falta: en un caso real, la lógica de automatizaciones ya sabía que AJAR significa «no cerrada», pero la ficha de la puerta solo conocía OPEN y CLOSED y mostraba «Desconocido». El conocimiento estaba en el repositorio, solo que en un sitio y no en el otro.
| Situación | Tipo | Ejemplo del core |
|---|---|---|
| El código depende de cada valor | Selection | sale.order.state |
| El usuario crea y ordena los valores | Many2one a un modelo de configuración | crm.lead.stage_id |
| El valor viene de un sistema externo | Char crudo + Selection calculado | (patrón de integración) |
| Un módulo añade valores a otro | selection_add + ondelete | sms añade Send SMS a ir.actions.server.state |
Resumen
Si el código decide los valores, Selection con claves estables. Si los decide el usuario, Many2one a un modelo de configuración con atributos que expresen el significado. Si los decide un tercero, guarda el literal crudo y tradúcelo con un mapeo tolerante y centralizado que nunca rompa la sincronización por un valor desconocido.