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"?> |
<?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 & 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> |
||||
|
@ -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 > 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…
Reference in new issue