Browse Source

restapi changes

pull/401/head
bernat.roig 2 weeks ago
parent
commit
fc5a0ad7ab
  1. 3
      rest_api_odoo/__manifest__.py
  2. 1
      rest_api_odoo/controllers/__init__.py
  3. 35
      rest_api_odoo/controllers/rest_api_odoo.py
  4. 741
      rest_api_odoo/controllers/swagger_controller.py
  5. 176
      rest_api_odoo/views/api_dashboard_views.xml
  6. 250
      rest_api_odoo/views/connection_api_views.xml
  7. 135
      rest_api_odoo/views/res_users_views.xml

3
rest_api_odoo/__manifest__.py

@ -35,7 +35,8 @@
"data": [ "data": [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'views/res_users_views.xml', 'views/res_users_views.xml',
'views/connection_api_views.xml' 'views/connection_api_views.xml',
'views/api_dashboard_views.xml',
], ],
'images': ['static/description/banner.jpg'], 'images': ['static/description/banner.jpg'],
'license': 'LGPL-3', 'license': 'LGPL-3',

1
rest_api_odoo/controllers/__init__.py

@ -20,3 +20,4 @@
# #
############################################################################# #############################################################################
from . import rest_api_odoo from . import rest_api_odoo
from . import swagger_controller

35
rest_api_odoo/controllers/rest_api_odoo.py

@ -81,6 +81,9 @@ class RestApi(http.Controller):
request.session.uid = user.id request.session.uid = user.id
request.env.user = user request.env.user = user
# Actualizar estadísticas de uso
user.update_api_key_usage()
return True, user.id, None return True, user.id, None
except Exception as e: except Exception as e:
@ -118,7 +121,8 @@ class RestApi(http.Controller):
return None, "Modelo no encontrado" return None, "Modelo no encontrado"
api_config = request.env['connection.api'].sudo().search([ api_config = request.env['connection.api'].sudo().search([
('model_id', '=', model_obj.id) ('model_id', '=', model_obj.id),
('active', '=', True)
], limit=1) ], limit=1)
if not api_config: if not api_config:
@ -387,7 +391,7 @@ class RestApi(http.Controller):
return self._error_response(error_msg, 401) return self._error_response(error_msg, 401)
try: try:
api_configs = request.env['connection.api'].sudo().search([]) api_configs = request.env['connection.api'].sudo().search([('active', '=', True)])
models_data = [] models_data = []
for config in api_configs: for config in api_configs:
@ -403,10 +407,16 @@ class RestApi(http.Controller):
} }
models_data.append(model_info) models_data.append(model_info)
# Agregar información de documentación
base_url = request.env['ir.config_parameter'].sudo().get_param('web.base.url', 'http://localhost:8069')
response_data = { response_data = {
"success": True, "success": True,
"count": len(models_data), "count": len(models_data),
"data": models_data "data": models_data,
"documentation": {
"swagger_ui": f"{base_url}/api/v1/docs",
"openapi_spec": f"{base_url}/api/v1/openapi.json"
}
} }
return self._json_response(response_data) return self._json_response(response_data)
@ -414,3 +424,22 @@ class RestApi(http.Controller):
except Exception as e: except Exception as e:
_logger.error(f"Error listando modelos: {str(e)}") _logger.error(f"Error listando modelos: {str(e)}")
return self._error_response("Error interno del servidor", 500) return self._error_response("Error interno del servidor", 500)
@http.route(['/api', '/api/'], type='http', auth='none', methods=['GET'], csrf=False)
def api_root(self, **kw):
"""Endpoint raíz de la API que redirige a la documentación"""
base_url = request.env['ir.config_parameter'].sudo().get_param('web.base.url', 'http://localhost:8069')
api_info = {
"message": "Bienvenido a la REST API de Odoo",
"version": "1.0.0",
"documentation": f"{base_url}/api/v1/docs",
"endpoints": {
"auth": f"{base_url}/api/v1/auth",
"models": f"{base_url}/api/v1/models",
"docs": f"{base_url}/api/v1/docs",
"openapi": f"{base_url}/api/v1/openapi.json"
}
}
return self._json_response(api_info)

741
rest_api_odoo/controllers/swagger_controller.py

@ -0,0 +1,741 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Ayana KP (odoo@cybrosys.com)
# Modified by: [Tu nombre] - Agregado Swagger/OpenAPI Documentation
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (LGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
import json
from datetime import datetime
from odoo import http
from odoo.http import request
class SwaggerController(http.Controller):
"""Controlador para generar documentación Swagger/OpenAPI de la REST API"""
@http.route(
["/api/v1/docs", "/api/docs"], type="http", auth="none", methods=["GET"]
)
def swagger_ui(self, **kwargs):
"""Muestra la interfaz de Swagger UI"""
base_url = (
request.env["ir.config_parameter"]
.sudo()
.get_param("web.base.url", "http://localhost:8069")
)
swagger_html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Odoo REST API - Swagger Documentation</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
<style>
html {{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}}
*, *:before, *:after {{
box-sizing: inherit;
}}
body {{
margin:0;
background: #fafafa;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}}
#swagger-ui {{
max-width: 1200px;
margin: 0 auto;
}}
.topbar {{
background-color: #89bf04 !important;
}}
.topbar .download-url-wrapper .download-url-button {{
background-color: #7aa30a !important;
border-color: #7aa30a !important;
}}
.swagger-ui .info .title {{
color: #89bf04;
}}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {{
const ui = SwaggerUIBundle({{
url: '{base_url}/api/v1/openapi.json',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
validatorUrl: null,
tryItOutEnabled: true,
supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
onComplete: function() {{
console.log("Swagger UI loaded successfully");
}},
requestInterceptor: function(req) {{
// Agregar headers por defecto si no están presentes
if (!req.headers['Content-Type'] && req.method !== 'GET') {{
req.headers['Content-Type'] = 'application/json';
}}
return req;
}}
}});
window.ui = ui;
}}
</script>
</body>
</html>
"""
return request.make_response(
swagger_html, headers=[("Content-Type", "text/html; charset=utf-8")]
)
@http.route(["/api/v1/openapi.json"], type="http", auth="none", methods=["GET"])
def openapi_spec(self, **kwargs):
"""Genera la especificación OpenAPI/Swagger en formato JSON"""
base_url = (
request.env["ir.config_parameter"]
.sudo()
.get_param("web.base.url", "http://localhost:8069")
)
db_name = request.env.cr.dbname
# Obtener información de modelos configurados
api_configs = (
request.env["connection.api"].sudo().search([("active", "=", True)])
)
# Estructura base de OpenAPI 3.0
openapi_spec = {
"openapi": "3.0.3",
"info": {
"title": "Odoo REST API",
"description": f"""
## Odoo REST API Documentation
Esta es la documentación interactiva de la REST API para Odoo. La API permite realizar operaciones CRUD en los modelos configurados de Odoo.
### Autenticación
La API utiliza autenticación basada en API Key. Para obtener una API Key:
1. **Primer paso - Autenticarse:**
```bash
curl -X POST {base_url}/api/v1/auth \\
-H "Content-Type: application/json" \\
-d '{{"username": "tu_usuario", "password": "tu_contraseña", "database": "{db_name}"}}'
2. Usar la API Key en las requests:
- Agregar header: X-API-Key: tu_api_key_aqui
- O usar header: api-key: tu_api_key_aqui
Formatos de Respuesta
Todas las respuestas siguen un formato estandarizado:
Respuesta exitosa:
{{
"success": true,
"count": 10,
"data": [...]
}}
Respuesta de error:
{{
"error": true,
"message": "Descripción del error",
"status_code": 400,
"error_code": "ERROR_CODE"
}}
Campos Especiales
Fechas: Se devuelven en formato ISO 8601 (YYYY-MM-DDTHH:MM:SS)
Archivos binarios: Se codifican en Base64
Relaciones Many2one: Se devuelven como [id, "display_name"]
Relaciones One2many/Many2many: Se devuelven como arrays de IDs
Modelos Disponibles
{self._get_available_models_description()}
""",
"version": "1.0.0",
"contact": {"name": "API Support", "email": "support@example.com"},
"license": {
"name": "LGPL-3",
"url": "https://www.gnu.org/licenses/lgpl-3.0.html",
},
},
"servers": [
{"url": f"{base_url}/api/v1", "description": "Production server"}
],
"security": [{"ApiKeyAuth": []}],
"components": {
"securitySchemes": {
"ApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "API Key obtenida del endpoint de autenticación",
}
},
"schemas": {
"ErrorResponse": {
"type": "object",
"properties": {
"error": {"type": "boolean", "example": True},
"message": {
"type": "string",
"example": "Descripción del error",
},
"status_code": {"type": "integer", "example": 400},
"error_code": {"type": "string", "example": "ERROR_CODE"},
},
},
"AuthRequest": {
"type": "object",
"required": ["username", "password"],
"properties": {
"username": {"type": "string", "example": "admin"},
"password": {"type": "string", "example": "admin"},
"database": {"type": "string", "example": f"{db_name}"},
},
},
"AuthResponse": {
"type": "object",
"properties": {
"success": {"type": "boolean", "example": True},
"message": {
"type": "string",
"example": "Autenticación exitosa",
},
"data": {
"type": "object",
"properties": {
"user_id": {"type": "integer", "example": 2},
"username": {"type": "string", "example": "admin"},
"name": {
"type": "string",
"example": "Administrator",
},
"api_key": {
"type": "string",
"example": "abcd1234...",
},
"database": {
"type": "string",
"example": f"{db_name}",
},
},
},
},
},
"SuccessResponse": {
"type": "object",
"properties": {
"success": {"type": "boolean", "example": True},
"count": {"type": "integer", "example": 10},
"data": {"type": "array", "items": {"type": "object"}},
},
},
},
"responses": {
"UnauthorizedError": {
"description": "API Key missing or invalid",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/ErrorResponse"}
}
},
},
"NotFoundError": {
"description": "Resource not found",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/ErrorResponse"}
}
},
},
"ValidationError": {
"description": "Invalid request data",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/ErrorResponse"}
}
},
},
},
},
"paths": self._generate_paths(api_configs),
"tags": self._generate_tags(api_configs),
}
return request.make_response(
json.dumps(openapi_spec, indent=2),
headers=[("Content-Type", "application/json; charset=utf-8")],
)
def _get_available_models_description(self):
"""Genera descripción de modelos disponibles"""
api_configs = request.env["connection.api"].sudo().search([("active", "=", True)])
if not api_configs:
return "No hay modelos configurados actualmente."
description = "Los siguientes modelos están disponibles:\n\n"
for config in api_configs:
methods = []
if config.is_get:
methods.append("GET")
if config.is_post:
methods.append("POST")
if config.is_put:
methods.append("PUT")
if config.is_delete:
methods.append("DELETE")
description += f"- **{config.model_id.name}** (`{config.model_id.model}`) - Métodos: {', '.join(methods)}\n"
return description
def _generate_tags(self, api_configs):
"""Genera tags para agrupar endpoints"""
tags = [
{
"name": "Authentication",
"description": "Endpoints para autenticación y gestión de API keys",
},
{
"name": "System",
"description": "Endpoints del sistema (información de modelos disponibles)",
},
]
for config in api_configs:
tags.append(
{
"name": config.model_id.model,
"description": f"Operaciones CRUD para el modelo {config.model_id.name}",
}
)
return tags
def _generate_paths(self, api_configs):
"""Genera todos los paths/endpoints de la API"""
paths = {}
# Endpoint de autenticación
paths["/auth"] = {
"post": {
"tags": ["Authentication"],
"summary": "Authenticate user and get API key",
"description": "Autentica un usuario y devuelve una API key para usar en las demás requests",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/AuthRequest"}
}
},
},
"responses": {
"200": {
"description": "Autenticación exitosa",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/AuthResponse"}
}
},
},
"401": {"$ref": "#/components/responses/UnauthorizedError"},
},
"security": [],
}
}
# Endpoint de modelos disponibles
paths["/models"] = {
"get": {
"tags": ["System"],
"summary": "List available models",
"description": "Lista todos los modelos disponibles en la API con sus métodos permitidos",
"responses": {
"200": {
"description": "Lista de modelos disponibles",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/SuccessResponse"}
}
},
},
"401": {"$ref": "#/components/responses/UnauthorizedError"},
},
}
}
# Generar endpoints para cada modelo configurado
for config in api_configs:
model_name = config.model_id.model
model_display_name = config.model_id.name
# Path para operaciones de colección (GET all, POST)
collection_path = f"/{model_name}"
paths[collection_path] = {}
# Path para operaciones de item específico (GET one, PUT, DELETE)
item_path = f"/{model_name}/{{id}}"
paths[item_path] = {}
# Obtener campos del modelo
model_fields = self._get_model_fields_info(model_name)
# GET - Obtener todos los registros
if config.is_get:
paths[collection_path]["get"] = {
"tags": [model_name],
"summary": f"Get all {model_display_name} records",
"description": f"Obtiene todos los registros del modelo {model_display_name}",
"parameters": [
{
"name": "fields",
"in": "query",
"description": "Campos específicos a retornar (separados por comas)",
"schema": {"type": "string", "example": "id,name,email"},
}
],
"responses": {
"200": {
"description": "Lista de registros",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SuccessResponse"
}
}
},
},
"401": {"$ref": "#/components/responses/UnauthorizedError"},
},
}
# GET - Obtener registro específico
paths[item_path]["get"] = {
"tags": [model_name],
"summary": f"Get specific {model_display_name} record",
"description": f"Obtiene un registro específico del modelo {model_display_name}",
"parameters": [
{
"name": "id",
"in": "path",
"required": True,
"description": "ID del registro",
"schema": {"type": "integer"},
}
],
"responses": {
"200": {
"description": "Registro encontrado",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SuccessResponse"
}
}
},
},
"404": {"$ref": "#/components/responses/NotFoundError"},
"401": {"$ref": "#/components/responses/UnauthorizedError"},
},
}
# POST - Crear registro
if config.is_post:
paths[collection_path]["post"] = {
"tags": [model_name],
"summary": f"Create new {model_display_name} record",
"description": f"Crea un nuevo registro en el modelo {model_display_name}",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["values"],
"properties": {
"values": {
"type": "object",
"description": "Datos del registro a crear",
"properties": model_fields,
},
"fields": {
"type": "array",
"items": {"type": "string"},
"description": "Campos a retornar en la respuesta",
},
},
},
"example": self._get_model_example_data(
model_name, "create"
),
}
},
},
"responses": {
"201": {
"description": "Registro creado exitosamente",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SuccessResponse"
}
}
},
},
"400": {"$ref": "#/components/responses/ValidationError"},
"401": {"$ref": "#/components/responses/UnauthorizedError"},
},
}
# PUT - Actualizar registro
if config.is_put:
paths[item_path]["put"] = {
"tags": [model_name],
"summary": f"Update {model_display_name} record",
"description": f"Actualiza un registro existente del modelo {model_display_name}",
"parameters": [
{
"name": "id",
"in": "path",
"required": True,
"description": "ID del registro a actualizar",
"schema": {"type": "integer"},
}
],
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["values"],
"properties": {
"values": {
"type": "object",
"description": "Datos a actualizar",
"properties": model_fields,
},
"fields": {
"type": "array",
"items": {"type": "string"},
"description": "Campos a retornar en la respuesta",
},
},
},
"example": self._get_model_example_data(
model_name, "update"
),
}
},
},
"responses": {
"200": {
"description": "Registro actualizado exitosamente",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SuccessResponse"
}
}
},
},
"404": {"$ref": "#/components/responses/NotFoundError"},
"400": {"$ref": "#/components/responses/ValidationError"},
"401": {"$ref": "#/components/responses/UnauthorizedError"},
},
}
# DELETE - Eliminar registro
if config.is_delete:
paths[item_path]["delete"] = {
"tags": [model_name],
"summary": f"Delete {model_display_name} record",
"description": f"Elimina un registro del modelo {model_display_name}",
"parameters": [
{
"name": "id",
"in": "path",
"required": True,
"description": "ID del registro a eliminar",
"schema": {"type": "integer"},
}
],
"responses": {
"200": {
"description": "Registro eliminado exitosamente",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {"type": "boolean", "example": True},
"message": {
"type": "string",
"example": "Registro eliminado exitosamente",
},
"deleted_record": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"display_name": {"type": "string"},
},
},
},
}
}
},
},
"404": {"$ref": "#/components/responses/NotFoundError"},
"401": {"$ref": "#/components/responses/UnauthorizedError"},
},
}
return paths
def _get_model_fields_info(self, model_name):
"""Obtiene información de los campos de un modelo para la documentación"""
try:
model = request.env[model_name].sudo()
fields_info = {}
# Campos comunes que suelen existir
common_fields = {
"id": {
"type": "integer",
"description": "ID único del registro",
"readOnly": True,
},
"name": {"type": "string", "description": "Nombre"},
"display_name": {
"type": "string",
"description": "Nombre para mostrar",
"readOnly": True,
},
"create_date": {
"type": "string",
"format": "date-time",
"description": "Fecha de creación",
"readOnly": True,
},
"write_date": {
"type": "string",
"format": "date-time",
"description": "Fecha de última modificación",
"readOnly": True,
},
"active": {
"type": "boolean",
"description": "Indica si el registro está activo",
},
}
# Agregar campos comunes que existan en el modelo
for field_name, field_info in common_fields.items():
if hasattr(model, field_name):
fields_info[field_name] = field_info
return fields_info
except Exception:
# Si hay error, retornar estructura básica
return {
"id": {"type": "integer", "description": "ID único del registro"},
"display_name": {
"type": "string",
"description": "Nombre para mostrar",
},
}
def _get_model_example_data(self, model_name, operation):
"""Genera ejemplos de datos para cada modelo"""
examples = {
"res.partner": {
"create": {
"values": {
"name": "Nuevo Cliente",
"email": "cliente@ejemplo.com",
"phone": "+34123456789",
"is_company": False,
},
"fields": ["id", "name", "email", "phone"],
},
"update": {
"values": {"phone": "+34987654321", "street": "Calle Nueva 123"},
"fields": ["id", "name", "phone", "street"],
},
},
"product.product": {
"create": {
"values": {
"name": "Nuevo Producto",
"list_price": 99.99,
"default_code": "PROD001",
},
"fields": ["id", "name", "list_price", "default_code"],
},
"update": {
"values": {
"list_price": 89.99,
"description": "Descripción actualizada",
},
"fields": ["id", "name", "list_price", "description"],
},
},
}
# Retornar ejemplo específico o genérico
return examples.get(
model_name,
{
"create": {
"values": {"name": "Nuevo Registro"},
"fields": ["id", "name", "display_name"],
},
"update": {
"values": {"name": "Registro Actualizado"},
"fields": ["id", "name", "display_name"],
},
},
).get(operation, {})

176
rest_api_odoo/views/api_dashboard_views.xml

@ -0,0 +1,176 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Modelo para el dashboard de la API -->
<record id="api_dashboard_action" model="ir.actions.client">
<field name="name">API Dashboard</field>
<field name="tag">api_dashboard</field>
</record>
<!-- Template para el dashboard de la API -->
<template id="api_dashboard_template">
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h3 class="card-title mb-0">
<i class="fa fa-code"/> Odoo REST API Dashboard
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8">
<h4>Bienvenido a la REST API de Odoo</h4>
<p class="lead">
Esta API te permite integrar aplicaciones externas con Odoo
usando estándares REST y autenticación basada en API Keys.
</p>
<div class="row mt-4">
<div class="col-md-6">
<div class="card border-success">
<div class="card-body text-center">
<i class="fa fa-book fa-3x text-success mb-3"/>
<h5>Documentación Interactiva</h5>
<p>Explora y prueba todos los endpoints disponibles</p>
<a href="/api/v1/docs" target="_blank" class="btn btn-success">
Ver Swagger UI
</a>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-info">
<div class="card-body text-center">
<i class="fa fa-cogs fa-3x text-info mb-3"/>
<h5>Configuración de Modelos</h5>
<p>Gestiona qué modelos están disponibles en la API</p>
<a href="/web#action=rest_api_root_action" class="btn btn-info">
Configurar API
</a>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5><i class="fa fa-info-circle"/> Información Rápida</h5>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li><strong>Base URL:</strong> <code id="base-url">/api/v1</code></li>
<li><strong>Autenticación:</strong> API Key</li>
<li><strong>Formato:</strong> JSON</li>
<li><strong>Versión:</strong> 1.0.0</li>
</ul>
<hr/>
<h6><i class="fa fa-external-link"/> Enlaces Útiles</h6>
<div class="list-group list-group-flush">
<a href="/api" target="_blank" class="list-group-item list-group-item-action">
<i class="fa fa-home"/> API Root
</a>
<a href="/api/v1/openapi.json" target="_blank" class="list-group-item list-group-item-action">
<i class="fa fa-file-code-o"/> OpenAPI Spec
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5><i class="fa fa-rocket"/> Comenzar Rápidamente</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>1. Obtener API Key</h6>
<pre class="bg-light p-3"><code>curl -X POST <span id="auth-url">/api/v1/auth</span> \
-H "Content-Type: application/json" \
-d '{
"username": "tu_usuario",
"password": "tu_contraseña"
}'</code></pre>
</div>
<div class="col-md-6">
<h6>2. Usar la API</h6>
<pre class="bg-light p-3"><code>curl -X GET <span id="models-url">/api/v1/models</span> \
-H "X-API-Key: tu_api_key_aqui"</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Actualizar URLs con la URL base actual
document.addEventListener('DOMContentLoaded', function() {
const baseUrl = window.location.origin;
document.getElementById('base-url').textContent = baseUrl + '/api/v1';
document.getElementById('auth-url').textContent = baseUrl + '/api/v1/auth';
document.getElementById('models-url').textContent = baseUrl + '/api/v1/models';
});
</script>
</template>
<!-- Acción para el dashboard principal -->
<record id="api_main_dashboard_action" model="ir.actions.act_url">
<field name="name">API Documentation</field>
<field name="url">/api/v1/docs</field>
<field name="target">new</field>
</record>
<!-- Vista para estadísticas de la API -->
<record id="api_stats_view" model="ir.ui.view">
<field name="name">api.stats.view</field>
<field name="model">connection.api</field>
<field name="arch" type="xml">
<list create="false" edit="false" delete="false">
<field name="display_name"/>
<field name="model_id"/>
<field name="active"/>
<field name="is_get"/>
<field name="is_post"/>
<field name="is_put"/>
<field name="is_delete"/>
<field name="max_records_limit"/>
</list>
</field>
</record>
<!-- Acción para estadísticas -->
<record id="api_stats_action" model="ir.actions.act_window">
<field name="name">API Statistics</field>
<field name="res_model">connection.api</field>
<field name="view_mode">list</field>
<field name="view_id" ref="api_stats_view"/>
<field name="context">{'search_default_active': 1}</field>
</record>
<!-- Menú principal actualizado con el dashboard -->
<menuitem id="rest_api_dashboard_menu"
name="API Dashboard"
parent="rest_api_root"
action="api_main_dashboard_action"
sequence="50"/>
<menuitem id="rest_api_stats_menu"
name="API Statistics"
parent="rest_api_root"
action="api_stats_action"
sequence="35"
groups="base.group_system"/>
</odoo>

250
rest_api_odoo/views/connection_api_views.xml

@ -1,61 +1,271 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Form view for 'connection.api' model. --> <!-- Form view mejorada para 'connection.api' -->
<record id="connection_api_view_form" model="ir.ui.view"> <record id="connection_api_view_form" model="ir.ui.view">
<field name="name">connection.api.view.form</field> <field name="name">connection.api.view.form</field>
<field name="model">connection.api</field> <field name="model">connection.api</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form>
<header>
<button name="toggle_active" type="object"
string="Activate" class="btn-primary"
invisible="active"/>
<button name="toggle_active" type="object"
string="Deactivate" class="btn-secondary"
invisible="not active"/>
<button name="action_test_api_endpoint" type="object"
string="Test Endpoint" class="btn-info"
invisible="not model_id"/>
<field name="active" widget="boolean_toggle"
options="{'autosave': False}"/>
</header>
<sheet> <sheet>
<div class="oe_title">
<h1>
<field name="display_name" readonly="1"/>
</h1>
<h3>
<field name="api_endpoint" readonly="1"
widget="url" invisible="not api_endpoint"/>
</h3>
</div>
<group> <group>
<group string="Resource"> <group string="Model Configuration">
<field name="model_id" string="Model"/> <field name="model_id" options="{'no_create': True}"/>
<field name="description" placeholder="Describe the purpose of this API configuration..."/>
</group> </group>
<group string="Methods"> <group string="HTTP Methods">
<field name="is_get"/> <field name="is_get" string="GET (Read)"/>
<field name="is_post"/> <field name="is_post" string="POST (Create)"/>
<field name="is_put"/> <field name="is_put" string="PUT (Update)"/>
<field name="is_delete"/> <field name="is_delete" string="DELETE"/>
</group> </group>
</group> </group>
<notebook>
<page string="Field Configuration" name="fields">
<group>
<group string="Allowed Fields">
<field name="allowed_fields"
placeholder="field1, field2, field3... (leave empty for all fields)"
widget="text"/>
</group>
<group string="Forbidden Fields">
<field name="forbidden_fields"
widget="text"/>
</group>
</group>
</page>
<page string="Security &amp; Limits" name="security">
<group>
<group string="Request Limits">
<field name="max_records_limit"/>
<field name="require_record_id_for_write"/>
</group>
</group>
</page>
<page string="API Information" name="api_info">
<group>
<group string="Endpoint Information">
<field name="api_endpoint" readonly="1" widget="url"/>
</group>
<group string="Documentation">
<div>
<a href="/api/v1/docs" target="_blank" class="btn btn-info">
<i class="fa fa-book"/> View Swagger Documentation
</a>
</div>
</group>
</group>
<div class="alert alert-info" role="alert" invisible="not api_endpoint">
<h4>Usage Examples:</h4>
<p><strong>Get all records:</strong><br/>
<code>GET <field name="api_endpoint" readonly="1"/></code></p>
<p><strong>Get specific record:</strong><br/>
<code>GET <field name="api_endpoint" readonly="1"/>/123</code></p>
<p><strong>Create record:</strong><br/>
<code>POST <field name="api_endpoint" readonly="1"/></code></p>
<p><strong>Update record:</strong><br/>
<code>PUT <field name="api_endpoint" readonly="1"/>/123</code></p>
<p><strong>Delete record:</strong><br/>
<code>DELETE <field name="api_endpoint" readonly="1"/>/123</code></p>
<p><em>Remember to include your API Key in the header: <code>X-API-Key: your_api_key_here</code></em></p>
</div>
</page>
</notebook>
</sheet> </sheet>
</form> </form>
</field> </field>
</record> </record>
<!-- List view for 'connection.api' model. -->
<!-- List view mejorada para 'connection.api' -->
<record id="connection_api_view_list" model="ir.ui.view"> <record id="connection_api_view_list" model="ir.ui.view">
<field name="name">connection.api.view.list</field> <field name="name">connection.api.view.list</field>
<field name="model">connection.api</field> <field name="model">connection.api</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<list> <list decoration-muted="not active" create="true" edit="true" delete="true">
<field name="model_id" string="Model"/> <field name="active" invisible="1"/>
<field name="display_name"/>
<field name="model_id"/>
<field name="is_get" widget="boolean_toggle"/>
<field name="is_post" widget="boolean_toggle"/>
<field name="is_put" widget="boolean_toggle"/>
<field name="is_delete" widget="boolean_toggle"/>
<field name="max_records_limit"/>
<field name="active" widget="boolean_toggle"/>
<button name="action_test_api_endpoint" type="object"
string="Test" icon="fa-external-link"
title="Test API Endpoint"/>
<button name="toggle_active" type="object"
string="Toggle Active" icon="fa-toggle-on"
title="Activate/Deactivate"/>
</list>
</field>
</record>
<!-- Vista Kanban para 'connection.api' -->
<record id="connection_api_view_kanban" model="ir.ui.view">
<field name="name">connection.api.view.kanban</field>
<field name="model">connection.api</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile" create="true" edit="true">
<field name="display_name"/>
<field name="model_id"/>
<field name="active"/>
<field name="is_get"/> <field name="is_get"/>
<field name="is_post"/> <field name="is_post"/>
<field name="is_put"/> <field name="is_put"/>
<field name="is_delete"/> <field name="is_delete"/>
</list> <field name="api_endpoint"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click #{record.active.raw_value ? '' : 'o_kanban_card_disabled'}">
<div class="o_kanban_content">
<div class="o_kanban_primary_left">
<div class="o_primary">
<strong><t t-esc="record.display_name.value"/></strong>
</div>
<div class="o_secondary">
<i class="fa fa-cube" title="Model"/>
<t t-esc="record.model_id.value"/>
</div>
</div>
<div class="o_kanban_primary_right">
<div class="badge-container">
<span t-if="record.is_get.raw_value" class="badge badge-success">GET</span>
<span t-if="record.is_post.raw_value" class="badge badge-primary">POST</span>
<span t-if="record.is_put.raw_value" class="badge badge-info">PUT</span>
<span t-if="record.is_delete.raw_value" class="badge badge-danger">DELETE</span>
</div>
</div>
</div>
<div class="o_kanban_footer">
<div class="o_kanban_footer_left">
<span t-if="record.active.raw_value" class="badge badge-success">Active</span>
<span t-if="!record.active.raw_value" class="badge badge-secondary">Inactive</span>
</div>
<div class="o_kanban_footer_right">
<button name="action_test_api_endpoint" type="object"
class="btn btn-sm btn-outline-primary"
title="Test Endpoint">
<i class="fa fa-external-link"/>
</button>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- Vista de búsqueda para 'connection.api' -->
<record id="connection_api_view_search" model="ir.ui.view">
<field name="name">connection.api.view.search</field>
<field name="model">connection.api</field>
<field name="arch" type="xml">
<search>
<field name="model_id" string="Model"/>
<field name="display_name" string="Name"/>
<separator/>
<filter name="active" string="Active" domain="[('active', '=', True)]"/>
<filter name="inactive" string="Inactive" domain="[('active', '=', False)]"/>
<separator/>
<filter name="get_enabled" string="GET Enabled" domain="[('is_get', '=', True)]"/>
<filter name="post_enabled" string="POST Enabled" domain="[('is_post', '=', True)]"/>
<filter name="put_enabled" string="PUT Enabled" domain="[('is_put', '=', True)]"/>
<filter name="delete_enabled" string="DELETE Enabled" domain="[('is_delete', '=', True)]"/>
<separator/>
<group expand="0" string="Group By">
<filter name="group_by_model" string="Model" context="{'group_by': 'model_id'}"/>
<filter name="group_by_active" string="Status" context="{'group_by': 'active'}"/>
</group>
</search>
</field> </field>
</record> </record>
<!-- Action for 'connection.api' model with List and form views. -->
<!-- Acción mejorada para 'connection.api' -->
<record id="rest_api_root_action" model="ir.actions.act_window"> <record id="rest_api_root_action" model="ir.actions.act_window">
<field name="name">Rest API Records</field> <field name="name">REST API Configuration</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="res_model">connection.api</field> <field name="res_model">connection.api</field>
<field name="view_mode">list,form</field> <field name="view_mode">kanban,list,form</field>
<field name="context">{'search_default_active': 1}</field>
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
Create! Configure your first REST API endpoint!
</p> </p>
<p>
Create API configurations to expose Odoo models through REST endpoints.
You can control which HTTP methods are allowed and configure field access.
</p>
</field>
</record>
<!-- Acción para crear configuraciones por defecto -->
<record id="action_create_default_api_configs" model="ir.actions.server">
<field name="name">Create Default API Configurations</field>
<field name="model_id" ref="model_connection_api"/>
<field name="state">code</field>
<field name="code">
created = model.create_default_configurations()
if created:
message = f"Created {len(created)} default API configurations: {', '.join([c.display_name for c in created])}"
else:
message = "No new configurations created. Default configurations already exist."
action = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'message': message,
'type': 'success' if created else 'info',
'sticky': False,
}
}
</field> </field>
</record> </record>
<!-- Menu items for the REST API. -->
<!-- Menú items mejorados -->
<menuitem id="rest_api_root" <menuitem id="rest_api_root"
name="Rest API" name="REST API"
sequence="10" sequence="10"
web_icon="rest_api_odoo,static/description/icon.png"/> web_icon="rest_api_odoo,static/description/icon.png"/>
<menuitem id="rest_api_details_root"
name="Rest API" <menuitem id="rest_api_config_menu"
name="API Configuration"
parent="rest_api_root" parent="rest_api_root"
action="rest_api_root_action" action="rest_api_root_action"
sequence="10"/> sequence="10"/>
<menuitem id="rest_api_create_defaults_menu"
name="Create Default Configs"
parent="rest_api_root"
action="action_create_default_api_configs"
sequence="20"/>
</odoo> </odoo>

135
rest_api_odoo/views/res_users_views.xml

@ -1,18 +1,143 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Inherited user view for Adding API key. --> <!-- Vista de usuario mejorada para gestión de API keys -->
<record id="view_users_form" model="ir.ui.view"> <record id="view_users_form_api_enhanced" model="ir.ui.view">
<field name="name">view.users.form.inherit.rest.api.odoo</field> <field name="name">view.users.form.inherit.rest.api.enhanced</field>
<field name="inherit_id" ref="base.view_users_form"/> <field name="inherit_id" ref="base.view_users_form"/>
<field name="model">res.users</field> <field name="model">res.users</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//page[@name='access_rights']" position="after"> <xpath expr="//page[@name='access_rights']" position="after">
<page string="API" name="rest-api"> <page string="REST API" name="rest-api" groups="base.group_system">
<group> <group>
<field name="api_key" groups="base.group_user"/> <group string="API Key Management">
<field name="api_key" readonly="1" password="True"
placeholder="No API key generated yet"/>
<field name="api_key_created" readonly="1"/>
<field name="api_key_expiry" readonly="1"/>
</group>
<group string="Usage Statistics">
<field name="api_requests_count" readonly="1"/>
<field name="api_key_last_used" readonly="1"/>
</group>
</group> </group>
<div class="oe_button_box" name="button_box_api">
<button name="generate_api_key" type="object"
string="Generate API Key"
class="btn-primary"
invisible="api_key"
confirm="This will generate a new API key. Continue?"/>
<button name="regenerate_api_key" type="object"
string="Regenerate API Key"
class="btn-warning"
invisible="not api_key"
confirm="This will invalidate the current API key and generate a new one. All applications using the current key will stop working. Continue?"/>
<button name="revoke_api_key" type="object"
string="Revoke API Key"
class="btn-danger"
invisible="not api_key"
confirm="This will permanently revoke the API key. All applications using this key will stop working. Continue?"/>
</div>
<div class="alert alert-info" role="alert" invisible="not api_key">
<h4><i class="fa fa-info-circle"/> API Usage Instructions</h4>
<p><strong>1. Authentication:</strong> Include your API key in the request header:</p>
<pre><code>X-API-Key: <field name="api_key" nolabel="1" readonly="1"/></code></pre>
<p><strong>2. Available Endpoints:</strong></p>
<ul>
<li><code>GET /api/v1/models</code> - List available models</li>
<li><code>GET /api/v1/{model}</code> - Get all records</li>
<li><code>GET /api/v1/{model}/{id}</code> - Get specific record</li>
<li><code>POST /api/v1/{model}</code> - Create new record</li>
<li><code>PUT /api/v1/{model}/{id}</code> - Update record</li>
<li><code>DELETE /api/v1/{model}/{id}</code> - Delete record</li>
</ul>
<p><strong>3. Example cURL request:</strong></p>
<pre><code>curl -X GET http://your-odoo-server/api/v1/res.partner \
-H "X-API-Key: <field name="api_key" nolabel="1" readonly="1"/>" \
-H "Content-Type: application/json"</code></pre>
</div>
<div class="alert alert-warning" role="alert" invisible="api_key">
<h4><i class="fa fa-warning"/> No API Key Generated</h4>
<p>Click "Generate API Key" to create an API key for this user.
The API key is required to authenticate REST API requests.</p>
</div>
<div class="alert alert-danger" role="alert" invisible="not api_key_expiry or api_key_expiry &gt; context_today()">
<h4><i class="fa fa-exclamation-triangle"/> API Key Expired</h4>
<p>This API key has expired and will not work for authentication.
Please generate a new API key.</p>
</div>
</page> </page>
</xpath> </xpath>
</field> </field>
</record> </record>
<!-- Vista de lista mejorada para usuarios con información de API -->
<record id="view_users_tree_api_info" model="ir.ui.view">
<field name="name">view.users.tree.api.info</field>
<field name="inherit_id" ref="base.view_users_tree"/>
<field name="model">res.users</field>
<field name="arch" type="xml">
<field name="login_date" position="after">
<field name="api_key" string="Has API Key"
widget="boolean" optional="hide"/>
<field name="api_requests_count" string="API Requests"
optional="hide"/>
<field name="api_key_last_used" string="API Last Used"
optional="hide"/>
</field>
</field>
</record>
<!-- Acción del servidor para limpiar API keys expiradas -->
<record id="action_cleanup_expired_api_keys" model="ir.actions.server">
<field name="name">Cleanup Expired API Keys</field>
<field name="model_id" ref="base.model_res_users"/>
<field name="state">code</field>
<field name="code">
cleaned_count = model.cleanup_expired_api_keys()
message = f"Cleaned {cleaned_count} expired API keys." if cleaned_count > 0 else "No expired API keys found."
action = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'message': message,
'type': 'success' if cleaned_count > 0 else 'info',
'sticky': False,
}
}
</field>
</record>
<!-- Menú para gestión de API keys (solo para administradores) -->
<menuitem id="rest_api_users_menu"
name="API Users Management"
parent="rest_api_root"
action="base.action_res_users"
sequence="30"
groups="base.group_system"/>
<menuitem id="rest_api_cleanup_menu"
name="Cleanup Expired Keys"
parent="rest_api_root"
action="action_cleanup_expired_api_keys"
sequence="40"
groups="base.group_system"/>
<!-- Cron job para limpiar API keys expiradas automáticamente -->
<record id="ir_cron_cleanup_expired_api_keys" model="ir.cron">
<field name="name">Cleanup Expired API Keys</field>
<field name="model_id" ref="base.model_res_users"/>
<field name="state">code</field>
<field name="code">model.cleanup_expired_api_keys()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="active">True</field>
</record>
</odoo> </odoo>

Loading…
Cancel
Save