En muchos equipos de trabajo, antes de ejecutar una tarea es necesario que una o varias personas la aprueben. Este proceso, que suele gestionarse de forma manual o por email, puede integrarse perfectamente dentro de Redmine.
En esta píldora te voy a enseñar dos formas de implementar un flujo de aprobación dentro de Redmine:
- Una versión básica, secuencial y fácil de implantar.
- Una versión avanzada, más potente, que permite múltiples aprobaciones en paralelo.
Aprobación básica y secuencial
Esta versión es útil cuando solo una persona tiene que revisar la tarea antes de que continúe su curso.
¿Cómo funciona?
- El responsable de la tarea la pasa a estado “Pendiente de aprobar”.
- Asigna la tarea a la persona que debe evaluarla.
- Esa persona, tras revisar, cambia el estado a:
- “Aprobada”, si da el visto bueno.
- “Rechazada”, si requiere correcciones.
- Si la tarea fue rechazada, vuelve al creador o responsable original, quien puede corregirla y volver a asignarla para aprobación.
Ventajas
- Rápido de implementar.
- Sin necesidad de plugins adicionales.
- Perfecto para flujos simples o tareas poco críticas.
Aprobación avanzada con múltiples usuarios
Cuando una tarea requiere ser aprobada por varias personas a la vez, es mejor optar por un enfoque más estructurado y automatizado. Aquí entra en juego el poder de dos plugins:
- View customize: para añadir botones y lógica personalizada.
- Custom Workflows: para definir reglas automáticas de transición de estados.
Objetivo
- Cada aprobador recibe su propia subtarea.
- Todos deben aprobar (o alguno puede rechazar).
- El estado de la tarea principal cambia automáticamente.
- El proceso se puede relanzar si la aprobación no se produjo en un principio.
Implementación del proceso de aprobación
1. Campo personalizado “Aprobadores”
Creamos un campo personalizado en Redmine de tipo usuario múltiple, llamado “Aprobadores”, donde seleccionaremos a las personas que deben validar la tarea. Ese campo puede mostrar sólo los usuarios de un determinado perfil, por ejemplo, los de perfil “aprobador”.
2. Botón “Lanzar proceso de aprobación”
Con View Customize, añadimos un botón visible en la tarea:

Este botón, al pulsarse:
- Lee el campo “Aprobadores”.
- Crea una subtarea por cada aprobador seleccionado, de tipo “Aprobación” y en estado “Pendiente”.
- Asigna cada subtarea al aprobador correspondiente.
- Cambia el estado de la tarea principal a “Pendiente de aprobar”.
3. Revisión por los aprobadores
Cada usuario ve su subtarea de aprobación y cambia su estado a:
- “Aprobada” si está de acuerdo.
- “Rechazada” si no aprueba.
4. Evaluación automática de la tarea padre a aprobar o rechazar
Con el plugin Custom Workflows, definimos reglas como éstas:
- Si todos aprueban sus subtareas, la tarea principal pasa a estado “En marcha”.
- Si alguno rechaza, la tarea principal vuelve a estado “Pendiente”.
5. Relanzar el proceso
Si hubo rechazo, el responsable puede hacer ajustes y volver a pulsar el botón para relanzar el proceso de aprobación. Esto reinicia todas las subtareas de aprobación y repite el ciclo.
Una vez aprobada la tarea padre, el botón de lanzar las tareas de aprobación desaparece de la página de detalle de la tarea:

Qué procedimiento elegir
| Escenario | Opción recomendada |
|---|---|
| Una sola persona aprueba | Versión básica |
| Varias personas deben revisar | Versión avanzada |
| Necesitas trazabilidad de cada aprobación | Versión avanzada |
| Flujo simple y rápido de implantar | Versión básica |
Código Javascript para el botón (plugin View Customize)
A continuación os pongo un ejemplo de cómo podría ser el código javascript para el botón:
$(function () {
// IDs de elementos personalizados según tu instalación
const issueId = ViewCustomize.context.issue.id;
const projectId = ViewCustomize.context.project.identifier;
const apiKey = ViewCustomize.context.user.apiKey;
const ID_CAMPO_APROBADORES = 91; // Campo personalizado de tipo usuario múltiple
const ID_TRACKER_APROBACION = 21; // Tipo de tracker para las subtareas de aprobación
const ID_ESTADO_PENDIENTE = 19; // Estado requerido para mostrar el botón y para reiniciar subtareas
// Función principal que se ejecuta al cargar la página
async function init() {
try {
const issue = await fetchIssue(issueId);
// Solo mostrar el botón si la tarea principal está en estado 'Pendiente'
if (!issue.status || issue.status.id !== ID_ESTADO_PENDIENTE) {
console.log("La tarea no está en estado 'Pendiente'. No se muestra el botón.");
return;
}
// Obtener los usuarios seleccionados en el campo "Aprobadores"
const aprobadores = getSelectedUsers();
if (aprobadores.length === 0) {
console.log("No hay usuarios seleccionados en el campo Aprobadores.");
return;
}
// Crear el botón y añadirlo al DOM
const button = $('<a class="icon icon-add" href="#" style="color:#0051cc;">Lanzar proceso de aprobación</a>');
$('#issue_tree').before($('<p>').append(button));
// Acción al hacer clic en el botón
button.on('click', async function (e) {
e.preventDefault();
if (!confirm('¿Deseas lanzar el proceso de aprobación para los usuarios seleccionados?')) return;
try {
// Para cada aprobador, crear subtarea o reiniciarla si ya existe
await Promise.all(
aprobadores.map(async (user) => {
const subject = `Aprobación ${issue.subject} para ${user.name}`;
const existing = issue.children?.find((child) => child.subject === subject);
if (existing) {
// Si ya existe la subtarea, se reinicia su estado a "Pendiente"
await updateIssueStatus(existing.id, ID_ESTADO_PENDIENTE);
} else {
// Si no existe, se crea una nueva subtarea de tipo "Aprobación"
await createSubtask({
subject,
parent_issue_id: issueId,
assigned_to_id: user.id,
tracker_id: ID_TRACKER_APROBACION,
status_id: ID_ESTADO_PENDIENTE,
priority_id: issue.priority?.id,
project_id: issue.project.id
});
}
})
);
// Finalmente, asegurarse de que la tarea principal está en estado "Pendiente"
await updateIssueStatus(issueId, ID_ESTADO_PENDIENTE);
// Recargar la página para reflejar los cambios
location.reload();
} catch (error) {
console.error("Error durante el lanzamiento del proceso de aprobación:", error);
alert("Hubo un error al lanzar el proceso de aprobación.");
}
});
} catch (error) {
console.error("Error al inicializar el script:", error);
}
}
/**
* Extrae los usuarios seleccionados en el campo personalizado de tipo usuario múltiple.
* Devuelve una lista de objetos con ID y nombre.
*/
function getSelectedUsers() {
return $(`.cf_${ID_CAMPO_APROBADORES} .value a`).map(function () {
const name = $(this).text();
const id = $(this).attr('href')?.match(/\/users\/(\d+)/)?.[1];
return id ? { id, name } : null;
}).get();
}
/**
* Obtiene los detalles completos de una tarea, incluyendo subtareas (children),
* mediante la API REST de Redmine.
*/
async function fetchIssue(id) {
return $.ajax({
url: `/issues/${id}.json?include=children`,
method: 'GET',
headers: {
'X-Redmine-API-Key': apiKey
}
}).then(res => res.issue);
}
/**
* Actualiza el estado de una tarea dada (por su ID) al estado especificado.
*/
async function updateIssueStatus(issueId, statusId) {
return $.ajax({
type: 'PUT',
url: `/issues/${issueId}.json`,
headers: { 'X-Redmine-API-Key': apiKey },
contentType: 'application/json',
data: JSON.stringify({ issue: { status_id: statusId } })
});
}
/**
* Crea una nueva subtarea en el proyecto actual con los datos especificados.
*/
async function createSubtask(issueData) {
return $.ajax({
type: 'POST',
url: `/issues.json`,
headers: { 'X-Redmine-API-Key': apiKey },
contentType: 'application/json',
data: JSON.stringify({ issue: issueData })
});
}
// Llamada inicial del script
init();
});
Código Ruby para las acciones automatizadas sobre la tarea padre (plugin Custom Workflows)
Una vez que los aprobadores han completado sus subtareas, lo ideal es que la tarea principal se actualice automáticamente, sin necesidad de intervención manual.
Esto se puede lograr gracias al plugin Custom Workflows, añadiendo un pequeño bloque Ruby en la sección #after_save de un nuevo custom workflow:
# after_save
# Solo actuar si esta tarea es una subtarea de tipo Aprobación y tiene padre
if issue.tracker_id == 21 && issue.parent
parent = issue.parent
# Filtrar las subtareas de tipo Aprobación
subtareas_aprobacion = parent.children.select { |c| c.tracker_id == 21 }
# Solo cambiar el estado si TODAS están aprobadas
if subtareas_aprobacion.all? { |s| s.status.name == 'Aprobada' }
parent.init_journal(User.current, "Todas las subtareas de aprobación están aprobadas. Cambiando estado a 'En marcha'.")
parent.status_id = 20 # ID del estado 'En marcha'
parent.save
end
end
¿Qué hace este código?
- Se ejecuta cada vez que se guarda una subtarea de tipo Aprobación.
- Comprueba si todas las subtareas de tipo Aprobación están en estado “Aprobada”.
- Si es así, cambia el estado de la tarea padre a “En marcha” (o el que elijas).
- Si alguna ha sido rechazada, no hace falta hacer nada, ya que la tarea padre permanece en estado “Pendiente” hasta que se vuelva a lanzar el proceso.
Con este paso, el proceso de aprobación queda 100% automatizado: desde la generación de subtareas hasta la activación automática de la tarea principal cuando todos los responsables han aprobado.
Conclusión
Con este enfoque, puedes convertir Redmine en una herramienta mucho más potente para la gestión de tareas críticas que requieren validación. Ya sea mediante un flujo sencillo o un proceso de aprobación avanzado totalmente automatizado, tienes el control total sin salir de la plataforma.
Si te ha gustado este post, por favor, no dudes en compartirlo en redes sociales para que más gente conozca las bondades de Redmine 😉
