erp-core/docs/01-analisis-referencias/odoo/odoo-base-analysis.md

20 KiB

Análisis del Módulo Base de Odoo

Módulo: base Versión Odoo: 18.0 Community Prioridad: P0 (Crítico) Mapeo MGN: MGN-001 (Fundamentos), MGN-002 (Empresas), MGN-003 (Catálogos)


Descripción del Módulo

El módulo base de Odoo es el módulo fundacional que todos los demás módulos requieren. Proporciona:

  • Sistema de usuarios y autenticación
  • Gestión de empresas (multi-company)
  • Catálogos maestros (países, monedas, UdM)
  • Sistema de partners (clientes, proveedores, empleados, etc.)
  • Gestión de permisos (grupos, reglas de acceso)
  • Infraestructura base del ORM

Es el equivalente al "core" de un ERP.


Modelos de Datos Principales

1. res.users (Usuarios)

Tabla: res_users

Campos clave:

login = fields.Char(required=True)                    # Email/username
password = fields.Char()                              # Hash bcrypt
partner_id = fields.Many2one('res.partner')           # Vinculado a partner
company_id = fields.Many2one('res.company')           # Empresa principal
company_ids = fields.Many2many('res.company')         # Empresas permitidas
groups_id = fields.Many2many('res.groups')            # Grupos de permisos
active = fields.Boolean(default=True)                 # Usuario activo/inactivo
share = fields.Boolean()                              # Usuario portal (limitado)

Relaciones:

  • 1:1 con res.partner (cada usuario es un partner)
  • N:M con res.groups (un usuario tiene múltiples grupos)
  • N:1 con res.company (empresa principal)
  • N:M con res.company (empresas permitidas)

Funciones destacables:

  • authenticate() - Autenticación de usuario
  • check_credentials() - Validación de password
  • change_password() - Cambio de contraseña
  • has_group() - Verificar si usuario pertenece a grupo

Aplicabilidad MGN-001:

  • Replicar estructura en auth.users
  • Añadir tenant_id para multi-tenancy
  • JWT en lugar de sesiones Odoo

2. res.groups (Grupos de Permisos)

Tabla: res_groups

Campos clave:

name = fields.Char(required=True, translate=True)
category_id = fields.Many2one('ir.module.category')   # Categoría del grupo
implied_ids = fields.Many2many('res.groups')          # Grupos heredados
users = fields.Many2many('res.users')                 # Usuarios del grupo
model_access = fields.One2many('ir.model.access')     # Permisos de modelos
rule_groups = fields.Many2many('ir.rule')             # Record rules

Patrón de herencia de grupos:

Grupo "User: All Documents"
  ↓ implica
Grupo "User: Own Documents Only"

Aplicabilidad MGN-001:

  • Sistema RBAC basado en grupos
  • Grupos jerárquicos con herencia
  • Implementar en auth.roles + auth.role_hierarchy

3. ir.model.access (Permisos de Modelo)

Tabla: ir_model_access

Campos clave:

name = fields.Char(required=True)
model_id = fields.Many2one('ir.model', required=True) # Modelo protegido
group_id = fields.Many2one('res.groups')              # Grupo (null = todos)
perm_read = fields.Boolean(default=False)             # Permiso READ
perm_write = fields.Boolean(default=False)            # Permiso WRITE
perm_create = fields.Boolean(default=False)           # Permiso CREATE
perm_unlink = fields.Boolean(default=False)           # Permiso DELETE

Ejemplo:

# ir.model.access.csv
"access_sale_order_salesman","sale.order salesman","model_sale_order","sales_team.group_sale_salesman",1,1,1,0
# Grupo 'salesman' puede READ, WRITE, CREATE pero NO DELETE sale.order

Aplicabilidad MGN-001:

  • Implementar en auth.model_permissions
  • Permisos CRUD por modelo y rol
  • Tabla: role_id, resource, read, write, create, delete

4. ir.rule (Record Rules - RLS)

Tabla: ir_rule

Campos clave:

name = fields.Char(required=True)
model_id = fields.Many2one('ir.model', required=True)
groups = fields.Many2many('res.groups')               # Grupos aplicables
domain_force = fields.Text(required=True)             # Dominio (filtro)
perm_read = fields.Boolean(default=True)
perm_write = fields.Boolean(default=True)
perm_create = fields.Boolean(default=True)
perm_unlink = fields.Boolean(default=True)

Ejemplo:

# Record rule: "Sale Order: See own orders"
domain_force = [('user_id', '=', user.id)]
# Un vendedor solo ve sus propias órdenes de venta

Aplicabilidad MGN-001:

  • Implementar con PostgreSQL RLS (Row Level Security)
  • Políticas por tenant, por usuario, por rol
  • Ejemplo: CREATE POLICY tenant_isolation ON sales.orders USING (tenant_id = current_setting('app.tenant_id')::int);

5. res.company (Empresas)

Tabla: res_company

Campos clave:

name = fields.Char(required=True)
partner_id = fields.Many2one('res.partner')           # Partner vinculado
currency_id = fields.Many2one('res.currency')         # Moneda principal
logo = fields.Binary()
email = fields.Char()
phone = fields.Char()
parent_id = fields.Many2one('res.company')            # Empresa padre (holdings)
child_ids = fields.One2many('res.company')            # Subsidiarias

Aplicabilidad MGN-002:

  • Implementar en core.companies
  • Soporte para holdings (parent_id)
  • Vinculado a tenant_id en multi-tenancy

6. res.partner (Partners - Contactos Universales)

Tabla: res_partner

Campos clave:

name = fields.Char(required=True)
email = fields.Char()
phone = fields.Char()
mobile = fields.Char()
street = fields.Char()
city = fields.Char()
state_id = fields.Many2one('res.country.state')
country_id = fields.Many2one('res.country')
zip = fields.Char()
vat = fields.Char()                                   # Tax ID / RFC
is_company = fields.Boolean()                         # ¿Es empresa o persona?
customer_rank = fields.Integer()                      # ¿Es cliente?
supplier_rank = fields.Integer()                      # ¿Es proveedor?
employee = fields.Boolean()                           # ¿Es empleado?
user_id = fields.Many2one('res.users')                # Usuario vinculado
parent_id = fields.Many2one('res.partner')            # Empresa padre
child_ids = fields.One2many('res.partner')            # Contactos hijos

Patrón Universal de Partner: Odoo usa res.partner para:

  • Clientes
  • Proveedores
  • Empleados
  • Empresas
  • Contactos de empresas
  • Direcciones de entrega

Aplicabilidad MGN-003:

  • Considerar tabla universal core.partners o separar en customers, vendors, contacts
  • Ventaja Odoo: un solo modelo reduce duplicación
  • Desventaja: tabla grande, queries complejas
  • Recomendación: Adoptar patrón Odoo pero con vistas SQL por tipo

7. res.currency (Monedas)

Tabla: res_currency

Campos clave:

name = fields.Char(required=True)                     # Código ISO (USD, MXN, EUR)
symbol = fields.Char(required=True)                   # $, €, £
rate = fields.Float(digits=(12, 6))                   # Tasa de cambio
rounding = fields.Float(digits=(12, 6))               # Redondeo (0.01)
active = fields.Boolean(default=True)

Aplicabilidad MGN-003:

  • Catálogo maestro esencial
  • Implementar en core.currencies
  • Tabla de tasas de cambio históricas: core.currency_rates

8. res.country (Países)

Tabla: res_country

Campos clave:

name = fields.Char(required=True, translate=True)
code = fields.Char(required=True, size=2)             # ISO 3166-1 alpha-2
phone_code = fields.Integer()                         # Código telefónico
currency_id = fields.Many2one('res.currency')
state_ids = fields.One2many('res.country.state')      # Estados/provincias

Aplicabilidad MGN-003:

  • Catálogo maestro
  • Incluir en core.countries
  • Seed data con países ISO

9. uom.uom (Unidades de Medida)

Tabla: uom_uom

Campos clave:

name = fields.Char(required=True)
category_id = fields.Many2one('uom.category')         # Categoría (Length, Weight, Time)
factor = fields.Float(required=True)                  # Factor de conversión
uom_type = fields.Selection([
    ('bigger', 'Bigger than reference'),
    ('reference', 'Reference Unit'),
    ('smaller', 'Smaller than reference')
])
rounding = fields.Float(default=0.01)

Ejemplo de conversión:

Categoría: Weight
- kg (reference, factor=1.0)
- ton (bigger, factor=1000.0)    # 1 ton = 1000 kg
- g (smaller, factor=0.001)      # 1 g = 0.001 kg

Aplicabilidad MGN-003:

  • Esencial para inventario y compras
  • Implementar en core.units_of_measure + core.uom_categories

Lógica de Negocio Destacable

1. Sistema de Autenticación

Archivo: odoo/addons/base/models/res_users.py

def _check_credentials(self, password, user_agent_env):
    """ Check password and raise if invalid """
    assert password
    self.env.cr.execute(
        "SELECT COALESCE(password, '') FROM res_users WHERE id=%s",
        [self.env.uid]
    )
    [hashed] = self.env.cr.fetchone()
    valid, replacement = self._crypt_context().verify_and_update(password, hashed)
    if replacement is not None:
        self._set_encrypted_password(self.env.uid, replacement)
    if not valid:
        raise AccessDenied()

Patrones observados:

  • Hash de password con bcrypt
  • Verificación segura con verify_and_update
  • Actualización automática de hash si algoritmo cambió

Aplicabilidad MGN-001:

  • Usar bcrypt en Node.js
  • JWT para tokens en lugar de sesiones
  • Refresh tokens

2. Multi-Company Access Control

Archivo: odoo/addons/base/models/res_users.py

@api.depends('groups_id')
def _compute_share(self):
    for user in self:
        user.share = not any(group.category_id.xml_id == 'base.module_category_user_type'
                              for group in user.groups_id)

def _get_company(self):
    return self.env.company

company_id = fields.Many2one('res.company', default=_get_company, required=True)
company_ids = fields.Many2many('res.company', compute='_compute_company_ids', store=True)

Patrones observados:

  • Usuario tiene una empresa principal (company_id)
  • Usuario puede acceder a múltiples empresas (company_ids)
  • Record rules filtran registros por company_id

Aplicabilidad MGN-002:

  • En multi-tenancy schema-based: cada tenant = 1 schema
  • Dentro de un tenant, soporte multi-company con company_id
  • RLS policies filtran por tenant y company

3. Sistema de Permisos en Cascada

Lógica:

  1. Se evalúan ir.model.access (permisos de modelo por grupo)
  2. Si el usuario tiene permiso de modelo, se evalúan ir.rule (record rules)
  3. Las record rules se combinan con OR entre grupos del usuario
  4. Si no hay record rules, el usuario ve todos los registros (si tiene permiso de modelo)

Ejemplo:

# Usuario pertenece a grupos: 'Sales User', 'Project Manager'

# ir.model.access
# 'Sales User' → puede READ sale.order
# 'Project Manager' → puede READ sale.order

# ir.rule (Sales User)
domain = [('user_id', '=', user.id)]  # Solo sus propias órdenes

# ir.rule (Project Manager)
domain = [('project_id.user_id', '=', user.id)]  # Órdenes de sus proyectos

# Resultado: El usuario ve registros que cumplen:
# (user_id = X) OR (project_id.user_id = X)

Aplicabilidad MGN-001:

  • Implementar con PostgreSQL RLS
  • Múltiples policies se combinan con OR
  • Granularidad: modelo → record → field

Patrones Arquitectónicos Observados

1. Partner como Entidad Universal

Patrón:

res.partner (Tabla universal para contactos)
    ↓
Usado por:
- res.users (usuario es un partner)
- sale.order (customer_id → res.partner)
- purchase.order (vendor_id → res.partner)
- hr.employee (employee_id → res.partner)
- res.company (company es un partner)

Ventajas:

  • Un solo lugar para datos de contacto
  • Evita duplicación de direcciones, emails, teléfonos
  • Fácil vincular cualquier módulo a partners

Desventajas:

  • Tabla muy grande
  • Queries complejas (filtrar por customer_rank, supplier_rank, etc.)

Recomendación MGN-003: Adoptar con vistas SQL

CREATE VIEW customers AS SELECT * FROM core.partners WHERE customer_rank > 0;
CREATE VIEW vendors AS SELECT * FROM core.partners WHERE supplier_rank > 0;

2. Herencia de Grupos

Patrón:

groups_id = fields.Many2many('res.groups')
implied_ids = fields.Many2many('res.groups')  # Grupos heredados

# Grupo "Sales Manager" implica "Sales User"
# Si asignas "Sales Manager" a un usuario, automáticamente tiene "Sales User"

Aplicabilidad MGN-001:

  • Implementar tabla auth.role_hierarchy
  • Consultas recursivas para obtener roles heredados

3. Soft Delete (archive)

Patrón:

active = fields.Boolean(default=True)

def toggle_active(self):
    for record in self:
        record.active = not record.active
  • En lugar de DELETE, se hace UPDATE active = false
  • Permite recuperar registros borrados
  • Queries por defecto filtran active = true

Aplicabilidad MGN (todos):

  • Implementar campo active en todas las tablas
  • Soft delete en lugar de hard delete

APIs y Endpoints Estándar

Odoo usa XML-RPC y JSON-RPC para APIs. No es RESTful puro.

Endpoints típicos:

/web/session/authenticate       # Login
/web/dataset/call_kw            # Llamar métodos de modelos
/web/dataset/search_read        # Búsqueda + lectura

Métodos estándar de modelo:

  • create(vals) - Crear registro
  • write(vals) - Actualizar registro
  • unlink() - Eliminar registro
  • read(fields) - Leer campos
  • search(domain) - Buscar registros
  • search_read(domain, fields) - Buscar + leer

Aplicabilidad MGN: No copiar

  • ERP Genérico usará REST APIs (ADR-008)
  • Endpoints estándar: GET /api/v1/users, POST /api/v1/users, etc.

Integraciones con Otros Módulos

base es requerido por TODOS los módulos Odoo.

Dependencias inversas (quién usa base):

  • sale usa res.partner (clientes)
  • purchase usa res.partner (proveedores)
  • account usa res.currency, res.company
  • hr usa res.partner (empleados)
  • stock usa uom.uom (unidades de medida)
  • project usa res.users (asignación de tareas)

Funcionalidades Relevantes para ERP Genérico

Funcionalidades P0 (Críticas)

  1. Autenticación de usuarios

    • Login con email/password
    • Hash bcrypt
    • Sesiones/tokens
  2. Gestión de usuarios

    • Crear, editar, desactivar usuarios
    • Reseteo de contraseña
    • Perfil de usuario vinculado a partner
  3. Sistema RBAC (Roles & Permissions)

    • Grupos de permisos
    • Herencia de grupos
    • Permisos CRUD por modelo
    • Record rules (RLS)
  4. Multi-company

    • Gestión de múltiples empresas
    • Usuarios con acceso a múltiples empresas
    • Cambio de empresa en contexto
  5. Catálogos maestros

    • Países y estados
    • Monedas y tasas de cambio
    • Unidades de medida
    • Partners (clientes, proveedores, contactos)

Funcionalidades P1 (Importantes)

  1. Portal de usuarios externos

    • Usuarios tipo "portal" (share=True)
    • Acceso limitado
    • Vistas específicas
  2. Gestión de direcciones

    • Direcciones de facturación/entrega
    • Vinculadas a partners
  3. Configuración de empresa

    • Logo, datos fiscales
    • Moneda principal
    • Parámetros del sistema

Funcionalidades P2 (Opcionales)

  1. Multi-idioma

    • Traducciones de catálogos
    • UI multiidioma
  2. Gestión de bancos

    • Cuentas bancarias de empresa
    • Cuentas bancarias de partners

Mapeo a Módulos MGN

MGN-001: Fundamentos

De base:

  • res.usersauth.users
  • res.groupsauth.roles
  • ir.model.accessauth.model_permissions
  • ir.rule → PostgreSQL RLS policies

Funcionalidades:

  • RF-AUTH-001: Autenticación JWT
  • RF-AUTH-002: Gestión de usuarios
  • RF-AUTH-003: Sistema RBAC
  • RF-AUTH-004: Multi-tenancy

MGN-002: Empresas y Organizaciones

De base:

  • res.companycore.companies

Funcionalidades:

  • RF-COMP-001: Gestión de empresas
  • RF-COMP-002: Multi-company
  • RF-COMP-003: Configuración de empresa

MGN-003: Catálogos Maestros

De base:

  • res.partnercore.partners
  • res.currencycore.currencies
  • res.countrycore.countries
  • res.country.statecore.states
  • uom.uomcore.units_of_measure
  • uom.categorycore.uom_categories

Funcionalidades:

  • RF-CAT-001: Gestión de partners (clientes, proveedores, contactos)
  • RF-CAT-002: Catálogo de monedas y tasas
  • RF-CAT-003: Catálogo de países y estados
  • RF-CAT-004: Catálogo de unidades de medida

Recomendaciones de Implementación

1. Sistema de Autenticación

Odoo usa: Sesiones con cookies

ERP Genérico debe usar: JWT (ADR-001)

// Login endpoint
POST /api/v1/auth/login
Request: { email, password }
Response: {
  accessToken: "jwt...",
  refreshToken: "jwt...",
  user: { id, email, name, roles }
}

2. Sistema RBAC

Implementación sugerida:

Tablas:

-- auth.roles (equivalente a res.groups)
CREATE TABLE auth.roles (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  description TEXT,
  category VARCHAR(50),
  parent_role_id INT REFERENCES auth.roles(id),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- auth.user_roles (N:M)
CREATE TABLE auth.user_roles (
  user_id INT REFERENCES auth.users(id),
  role_id INT REFERENCES auth.roles(id),
  PRIMARY KEY (user_id, role_id)
);

-- auth.model_permissions (equivalente a ir.model.access)
CREATE TABLE auth.model_permissions (
  id SERIAL PRIMARY KEY,
  role_id INT REFERENCES auth.roles(id),
  resource VARCHAR(100) NOT NULL,  -- 'users', 'products', 'orders', etc.
  can_read BOOLEAN DEFAULT FALSE,
  can_write BOOLEAN DEFAULT FALSE,
  can_create BOOLEAN DEFAULT FALSE,
  can_delete BOOLEAN DEFAULT FALSE,
  UNIQUE (role_id, resource)
);

RLS (Row Level Security):

-- Política de aislamiento por tenant
CREATE POLICY tenant_isolation ON auth.users
USING (tenant_id = current_setting('app.tenant_id')::int);

-- Política: usuarios solo ven su propio registro
CREATE POLICY own_user_only ON auth.users
FOR SELECT
USING (id = current_setting('app.user_id')::int);

3. Catálogos Maestros

Implementación sugerida:

// Seed data de países (ISO 3166-1)
// Importar desde https://restcountries.com/
const countries = [
  { code: 'MX', name: 'México', phone_code: 52, currency_code: 'MXN' },
  { code: 'US', name: 'United States', phone_code: 1, currency_code: 'USD' },
  // ...
];

// Seed data de monedas (ISO 4217)
const currencies = [
  { code: 'MXN', name: 'Peso Mexicano', symbol: '$', decimals: 2 },
  { code: 'USD', name: 'US Dollar', symbol: '$', decimals: 2 },
  { code: 'EUR', name: 'Euro', symbol: '€', decimals: 2 },
  // ...
];

4. Partners (Patrón Universal)

Opción 1: Tabla única (patrón Odoo)

CREATE TABLE core.partners (
  id SERIAL PRIMARY KEY,
  tenant_id INT NOT NULL,
  name VARCHAR(200) NOT NULL,
  email VARCHAR(100),
  phone VARCHAR(20),
  is_customer BOOLEAN DEFAULT FALSE,
  is_vendor BOOLEAN DEFAULT FALSE,
  is_employee BOOLEAN DEFAULT FALSE,
  is_company BOOLEAN DEFAULT FALSE,
  parent_id INT REFERENCES core.partners(id),
  -- ... más campos
);

CREATE VIEW customers AS
SELECT * FROM core.partners WHERE is_customer = TRUE;

CREATE VIEW vendors AS
SELECT * FROM core.partners WHERE is_vendor = TRUE;

Opción 2: Tablas separadas

CREATE TABLE core.partners (
  id SERIAL PRIMARY KEY,
  tenant_id INT NOT NULL,
  name VARCHAR(200) NOT NULL,
  -- ... campos comunes
);

CREATE TABLE sales.customers (
  id INT PRIMARY KEY REFERENCES core.partners(id),
  -- campos específicos de clientes
);

CREATE TABLE purchase.vendors (
  id INT PRIMARY KEY REFERENCES core.partners(id),
  -- campos específicos de proveedores
);

Recomendación: Opción 1 (patrón Odoo) por simplicidad


Conclusión

El módulo base de Odoo es una referencia excelente para:

  • Sistema de autenticación robusto
  • RBAC granular con herencia de roles
  • Multi-company bien implementado
  • Catálogos maestros completos
  • Patrón de partner universal

Prioridad de implementación en MGN:

  1. MGN-001 (Fundamentos): Sistema auth, usuarios, RBAC
  2. MGN-003 (Catálogos): Monedas, países, UoM, partners
  3. MGN-002 (Empresas): Multi-company, configuración

Nivel de reutilización: 90% de los patrones son aplicables


Fecha: 2025-11-23 Analista: Architecture-Analyst Estado: Análisis completo