7 changed files with 1314 additions and 31 deletions
@ -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, {}) |
@ -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> |
@ -1,61 +1,271 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<!-- Form view for 'connection.api' model. --> |
|||
<!-- Form view mejorada para 'connection.api' --> |
|||
<record id="connection_api_view_form" model="ir.ui.view"> |
|||
<field name="name">connection.api.view.form</field> |
|||
<field name="model">connection.api</field> |
|||
<field name="arch" type="xml"> |
|||
<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> |
|||
<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 string="Resource"> |
|||
<field name="model_id" string="Model"/> |
|||
<group string="Model Configuration"> |
|||
<field name="model_id" options="{'no_create': True}"/> |
|||
<field name="description" placeholder="Describe the purpose of this API configuration..."/> |
|||
</group> |
|||
<group string="Methods"> |
|||
<field name="is_get"/> |
|||
<field name="is_post"/> |
|||
<field name="is_put"/> |
|||
<field name="is_delete"/> |
|||
<group string="HTTP Methods"> |
|||
<field name="is_get" string="GET (Read)"/> |
|||
<field name="is_post" string="POST (Create)"/> |
|||
<field name="is_put" string="PUT (Update)"/> |
|||
<field name="is_delete" string="DELETE"/> |
|||
</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 & 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> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
<!-- List view for 'connection.api' model. --> |
|||
|
|||
<!-- List view mejorada para 'connection.api' --> |
|||
<record id="connection_api_view_list" model="ir.ui.view"> |
|||
<field name="name">connection.api.view.list</field> |
|||
<field name="model">connection.api</field> |
|||
<field name="arch" type="xml"> |
|||
<list> |
|||
<field name="model_id" string="Model"/> |
|||
<list decoration-muted="not active" create="true" edit="true" delete="true"> |
|||
<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_post"/> |
|||
<field name="is_put"/> |
|||
<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> |
|||
</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"> |
|||
<field name="name">Rest API Records</field> |
|||
<field name="name">REST API Configuration</field> |
|||
<field name="type">ir.actions.act_window</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"> |
|||
<p class="o_view_nocontent_smiling_face"> |
|||
Create! |
|||
Configure your first REST API endpoint! |
|||
</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> |
|||
</record> |
|||
<!-- Menu items for the REST API. --> |
|||
|
|||
<!-- Menú items mejorados --> |
|||
<menuitem id="rest_api_root" |
|||
name="Rest API" |
|||
name="REST API" |
|||
sequence="10" |
|||
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" |
|||
action="rest_api_root_action" |
|||
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> |
|||
|
@ -1,18 +1,143 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<!-- Inherited user view for Adding API key. --> |
|||
<record id="view_users_form" model="ir.ui.view"> |
|||
<field name="name">view.users.form.inherit.rest.api.odoo</field> |
|||
<!-- Vista de usuario mejorada para gestión de API keys --> |
|||
<record id="view_users_form_api_enhanced" model="ir.ui.view"> |
|||
<field name="name">view.users.form.inherit.rest.api.enhanced</field> |
|||
<field name="inherit_id" ref="base.view_users_form"/> |
|||
<field name="model">res.users</field> |
|||
<field name="arch" type="xml"> |
|||
<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> |
|||
<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> |
|||
|
|||
<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 > 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> |
|||
</xpath> |
|||
</field> |
|||
</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> |
|||
|
Loading…
Reference in new issue