Cómo realizar un proceso de aprobación o rechazo en Redmine

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?

  1. El responsable de la tarea la pasa a estado “Pendiente de aprobar”.
  2. Asigna la tarea a la persona que debe evaluarla.
  3. Esa persona, tras revisar, cambia el estado a:
    • “Aprobada”, si da el visto bueno.
    • “Rechazada”, si requiere correcciones.
  4. 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:

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

EscenarioOpción recomendada
Una sola persona apruebaVersión básica
Varias personas deben revisarVersión avanzada
Necesitas trazabilidad de cada aprobaciónVersión avanzada
Flujo simple y rápido de implantarVersió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 😉

Sobre el autor

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *