Caso de uso - Bproc - Alta de stock desde productos

Alta de stock mediante BPROC Manual con selección múltiple

Objetivo

Implementar un BPROC de tipo manual que permita realizar un alta de stock para un producto específico desde el Maestro de Productos, abriendo un formulario interactivo (pop-up) para ingresar la cantidad y seleccionar múltiples depósitos de destino.

Este caso demuestra cómo un BPROC de tipo manual puede capturar parámetros del usuario en tiempo de ejecución e invocar lógica personalizada mediante un Guest Code para impactar datos en el ERP.

Problema de negocio

El proceso tradicional de ajuste de stock obliga al operario a salir del Maestro de Productos, navegar hasta el módulo de inventarios, crear un documento de ajuste manual, buscar nuevamente el producto y cargar las cantidades por depósito.

Como consecuencia:

  • Se incrementa el tiempo operativo y los clics necesarios para realizar tareas simples de reabastecimiento.

  • Aumenta la posibilidad de cometer errores de tipeo al buscar el código del producto en otra pantalla.

  • Se ralentiza la gestión administrativa en el control diario de stock en góndolas o depósitos dinámicos.

Solución propuesta

Al estar posicionado sobre un producto en el maestro, el usuario ejecuta un BPROC Manual a través de un botón dedicado. El sistema despliega un formulario pop-up solicitando los parámetros de Cantidad, Depósito destino (de selección múltiple) y el Documento de ajuste preconfigurado.

Al confirmar, GO invoca un Guest Code que procesa estos parámetros junto con los datos del producto seleccionado, valida la información y genera de forma automática el documento de Ajuste de Stock Positivo en el ERP.

Arquitectura de la solución

Usuario (Maestro de Productos)

   ↓ (Clic en botón BPROC)

Formulario Pop-up (Cantidad / Selector Múltiple Depósitos)

BPROC Manual (Envío de Payload)

Guest Code (Procesamiento y validación)

API / Entidad (crear_entidad “AjusteStock”)

Componentes utilizados

Guest Code

El Guest Code contiene la lógica de negocio y la orquestación de llamadas. Su responsabilidad es:

  • Recibir la información enviada por el BPROC Manual (formulario de parámetros + producto).

  • Normalizar y validar los datos ingresados (ej. validar que la cantidad sea distinta de cero).

  • Resolver los IDs internos del selector múltiple de depósitos y del tipo de documento a sus códigos correspondientes en el ERP.

  • Construir la estructura del payload para la entidad de inventario e invocar la creación del documento.

  • Retornar un mensaje de éxito en formato HTML para ser mostrado en pantalla o el log de error correspondiente.

# Script para Finnegans GO Guest Code
# BProc Manual disparado desde el maestro Producto
# Crea un AjusteStock usando los params del formulario + datos del objeto disparador


import os
from datetime import datetime


import requests
from finnegans.api import ApiException, crear_entidad, get_go_base_url, obtener_entidad
from finnegans.bproc import BProcStatus
from finnegans.logging import logger
from finnegans.scripting import HTTPResponse, request


PARAM_DEPOSITO_DESTINO = "Deposito destino"
PARAM_DOCUMENTO = "Documento"




def _normalizar_depositos_ids(valor) -> list[str]:
    """Normaliza el selector múltiple de depósitos destino (valores = IDs)."""
    if valor is None:
        return []
    if isinstance(valor, str):
        return [dep.strip() for dep in valor.split(",") if dep.strip()]
    if isinstance(valor, (list, tuple)):
        return [str(dep).strip() for dep in valor if dep is not None and str(dep).strip()]
    return [str(valor).strip()] if str(valor).strip() else []




def _resolver_codigos_depositos(depositos_ids: list[str]) -> list[str]:
    """Resuelve una lista de IDs de depósito a sus códigos."""
    return [_resolver_codigo_deposito(deposito_id) for deposito_id in depositos_ids]




def _mensaje_exito(
    producto: str,
    cantidad,
    depositos_codigos: list[str],
    documento_codigo: str,
    numero_comprobante: str | None = None,
) -> str:
    deps = ", ".join(depositos_codigos)
    documento = documento_codigo
    if numero_comprobante:
        documento = f"{documento_codigo} - Nº {numero_comprobante}"


    return (
        "Se generó el ajuste de stock correctamente.<br><br>"
        f"<b>Producto:</b> {producto}<br>"
        f"<b>Cantidad:</b> {cantidad}<br>"
        f"<b>Depósito(s):</b> {deps}<br>"
        f"<b>Documento:</b> {documento}"
    )




def _respuesta(status: int, mensaje: str) -> HTTPResponse:
    """El BProc manual muestra el body de la respuesta en un popup de información."""
    return HTTPResponse(status, body=mensaje)




def _enviar_update(payload: dict, status: BProcStatus, observaciones: str) -> None:
    """Actualiza el monitor del BProc manual vía API."""
    event_id = payload.get("eventID")
    if not event_id:
        logger.warning("No se pudo actualizar el BProc: falta eventID")
        return


    access_token = os.getenv("access_token", "")
    if not access_token:
        logger.warning("No se pudo actualizar el BProc: falta access_token")
        return


    estado = 4 if status == BProcStatus.FinalizadoError else 3
    url = f"{get_go_base_url()}/api/1/teamplace/generic/bridge/custom/BProc/status"
    try:
        response = requests.post(
            url,
            params={"eventID": event_id, "access_token": access_token},
            json={
                "estadoEjecucion": estado,
                "progress": 100,
                "observaciones": observaciones,
            },
            timeout=30,
        )
        if not response.ok:
            logger.warning(
                f"Error al actualizar BProc: {response.status_code} - {response.text}"
            )
    except Exception as exc:
        logger.warning(f"Excepción al actualizar BProc: {exc}")




def extraer_params(payload: dict) -> dict:
    """Extrae los parámetros ingresados en el formulario del BProc manual."""
    params = payload.get("params") or {}
    depositos_ids = _normalizar_depositos_ids(params.get(PARAM_DEPOSITO_DESTINO))


    return {
        "cantidad": params.get("Cantidad"),
        "depositos": depositos_ids,
        "documento": params.get(PARAM_DOCUMENTO),
        "empresa": payload.get("sucursal"),
    }




def extraer_objeto(payload: dict) -> dict:
    """Extrae los datos del producto sobre el que se disparó el BProc."""
    obj = payload.get("object") or {}
    return {
        "codigo": obj.get("code") or obj.get("Codigo"),
        "id": obj.get("id"),
        "tipo": obj.get("type"),
    }




def _resolver_codigo(valor: str, dicc_alias: str, campo_adicional: str) -> str:
    """Resuelve un ID numérico a código; si ya es código lo devuelve tal cual."""
    valor_str = str(valor).strip()
    if not valor_str:
        raise ValueError(f"Valor vacío para diccionario {dicc_alias}")


    if not valor_str.isdigit():
        return valor_str


    access_token = os.getenv("access_token", "")
    if not access_token:
        raise ValueError("Variable de entorno access_token no configurada")


    url = f"{get_go_base_url()}/api/1/teamplace/filters"
    response = requests.get(
        url,
        params={
            "access_token": access_token,
            "diccAlias": dicc_alias,
            "camposAdicionales": campo_adicional,
            "filtroString": f"id:{valor_str}",
        },
        timeout=20,
    )
    if not response.ok:
        raise ValueError(
            f"No se pudo consultar {dicc_alias} {valor_str}: "
            f"{response.status_code} {response.text}"
        )


    data = response.json()
    if isinstance(data, list):
        for item in data:
            if str(item.get("id")) == valor_str:
                codigo = item.get("CODIGO") or item.get("codigo")
                if codigo:
                    return str(codigo)


    raise ValueError(f"No se encontró código para {dicc_alias} ID {valor_str}")




def _resolver_codigo_deposito(deposito_id: str) -> str:
    """Resuelve un ID de depósito a su código para la API AjusteStock."""
    return _resolver_codigo(deposito_id, "DEPOSITO", "BSDeposito.Codigo")




def _construir_items(
    producto_codigo: str,
    cantidad: float,
    depositos_ids: list[str],
    depositos_codigos: list[str],
) -> list[dict]:
    items = []
    for deposito_id, deposito_codigo in zip(depositos_ids, depositos_codigos):
        items.append(
            {
                "ProductoID": producto_codigo,
                "DepositoIDDestino": deposito_codigo,
                "CantidadStock1": cantidad,
                "CantidadWorkflow": cantidad,
                "Tipo": 0,
            }
        )
        logger.info(
            f"Item preparado | producto={producto_codigo} | "
            f"depósito={deposito_codigo} (id={deposito_id}) | cantidad={cantidad}"
        )
    return items




def _obtener_numero_comprobante(identificacion_externa: str) -> str | None:
    try:
        detalle = obtener_entidad("AjusteStock", identificacion_externa)
        if not isinstance(detalle, dict):
            return None
        numero = detalle.get("NumeroDocumento") or detalle.get("numeroDocumento")
        return str(numero).strip() if numero else None
    except ApiException as exc:
        logger.warning(f"No se pudo consultar el comprobante creado: {exc}")
        return None




def construir_ajuste(
    producto_codigo: str,
    cantidad,
    depositos_ids: list[str],
    documento: str,
    empresa: str,
) -> tuple[dict, str, list[str]]:
    """Arma el cuerpo del request para la API AjusteStock."""
    cantidad_float = float(cantidad)
    documento_codigo = _resolver_codigo(
        documento,
        "DOCUMENTOTIPO",
        "FAFTransaccionSubtipo.Codigo",
    )
    depositos_codigos = _resolver_codigos_depositos(depositos_ids)
    fecha = datetime.now().strftime("%Y-%m-%d")


    payload = {
        "TransaccionSubtipoID": documento_codigo,
        "EmpresaID": empresa,
        "Fecha": fecha,
        "FechaComprobante": fecha,
        "FechaBaseVencimiento": fecha,
        "Descripcion": (
            f"Alta de stock BProc - Producto {producto_codigo} - "
            f"{len(depositos_ids)} depósito(s)"
        ),
        "OperacionItems": _construir_items(
            producto_codigo, cantidad_float, depositos_ids, depositos_codigos
        ),
    }
    return payload, documento_codigo, depositos_codigos




def main():
    payload = request.json()


    params = extraer_params(payload)
    cantidad = params["cantidad"]
    depositos_ids = params["depositos"]
    documento = params["documento"]
    empresa = params["empresa"]


    obj = extraer_objeto(payload)
    producto_codigo = obj.get("codigo")


    logger.info(
        f"Iniciando AjusteStock | Producto: {producto_codigo} | "
        f"Cantidad: {cantidad} | Depósitos (ids): {depositos_ids} | Documento: {documento}"
    )


    campos_requeridos = {
        "Producto": producto_codigo,
        "Cantidad": cantidad,
        PARAM_DEPOSITO_DESTINO: depositos_ids if depositos_ids else None,
        "Documento": documento,
        "Empresa": empresa,
    }
    faltantes = [k for k, v in campos_requeridos.items() if v is None]
    if faltantes:
        msg = f"Faltan campos requeridos para el AjusteStock: {', '.join(faltantes)}"
        logger.error(msg)
        _enviar_update(payload, BProcStatus.FinalizadoError, msg)
        return _respuesta(400, msg)


    if float(cantidad) == 0:
        msg = "El parámetro 'Cantidad' debe ser distinto de cero"
        logger.error(msg)
        _enviar_update(payload, BProcStatus.FinalizadoError, msg)
        return _respuesta(400, msg)


    data_ajuste, documento_codigo, depositos_codigos = construir_ajuste(
        producto_codigo, cantidad, depositos_ids, documento, empresa
    )


    try:
        identificacion_externa = crear_entidad("AjusteStock", data_ajuste)
        numero_comprobante = _obtener_numero_comprobante(identificacion_externa)
        mensaje_pantalla = _mensaje_exito(
            producto_codigo,
            cantidad,
            depositos_codigos,
            documento_codigo,
            numero_comprobante,
        )
        mensaje_log = (
            f"Ajuste de stock dado de alta correctamente. "
            f"Producto {producto_codigo}, cantidad {cantidad}, "
            f"depósitos {depositos_codigos}, documento {documento_codigo}"
        )
        if numero_comprobante:
            mensaje_log += f", comprobante {numero_comprobante}"
        mensaje_log += "."
        logger.info(mensaje_log)
        _enviar_update(payload, BProcStatus.FinalizadoOk, mensaje_log)
        return _respuesta(200, mensaje_pantalla)


    except ApiException as e:
        msg = f"Error al crear AjusteStock: {e}"
        logger.error(msg)
        _enviar_update(payload, BProcStatus.FinalizadoError, msg)
        return _respuesta(502, msg)


    except ValueError as e:
        msg = str(e)
        logger.error(msg)
        _enviar_update(payload, BProcStatus.FinalizadoError, msg)
        return _respuesta(400, msg)


    except Exception as e:
        msg = f"Error inesperado: {e}"
        logger.error(msg)
        _enviar_update(payload, BProcStatus.FinalizadoError, msg)
        return _respuesta(500, msg)


API / Entidades consultadas

  • Filtros / Diccionarios (DEPOSITO y DOCUMENTOTIPO): Se utilizan consultas dinámicas para traducir los IDs numéricos de la interfaz gráfica a códigos de negocio legibles por el ERP.

  • Entidad AjusteStock: Utiliza las funciones (crear_entidad y obtener_entidad) para dar de alta el movimiento físico de mercadería en los depósitos seleccionados y recuperar el número de comprobante definitivo.

Configuración del BPROC

Información general

Campo
Valor
Nombre
Alta de Stock desde Producto
Código
AltaStockProducto
Tipo
Manual
Aplicación
Aplicación Custom
Descripción
Permite dar de alta stock en múltiples depósitos desde la ficha del producto

Configuración de parámetros del formulario

  • Cantidad: Parámetro numérico para indicar el volumen a ingresar.

  • Deposito destino: Parámetro de tipo Selector Múltiple vinculado al diccionario de depósitos.

  • Documento: Configurado internamente (en la rueda de configuración del parámetro) asignando por defecto el documento de Ajuste de Stock Positivo a generar.

Configuración de ejecución

El BPROC se configura para visualizarse como una acción manual disponible dentro del Maestro de Productos. La URL apunta directamente al endpoint configurado en el entorno de desarrollo para el script de Guest Code provisto.

Flujo de ejecución

  1. El usuario se posiciona sobre un producto en el maestro y presiona el botón del BPROC.

  2. Se abre un pop-up en pantalla solicitando la Cantidad y los Depósitos de destino.

  3. El usuario completa los datos y confirma la operación.

  4. GO construye un payload que incluye la información del producto (“object”) y las variables del formulario (“params”).

  5. Se invoca el Guest Code y este extrae el código del producto, la sucursal/empresa y los parámetros.

  6. El script valida que la cantidad no sea cero y resuelve los códigos de los depósitos seleccionados.

  7. Se ejecuta la creación de la entidad AjusteStock iterando las líneas por cada depósito parametrizado.

  8. El Guest Code actualiza el estado en el monitor de procesos de GO y devuelve un código de estado HTTP con el detalle del éxito o fracaso de la operación.

Responsabilidad de cada componente

GO

  • Detecta la acción del usuario en la interfaz gráfica.

  • Renderiza el pop-up dinámico con los parámetros configurados.

  • Construye el payload unificado y despacha la invocación al servicio.

BPROC

  • Define la disponibilidad del botón en el maestro de productos.

  • Estructura los campos del formulario interactivo (inputs, selectores múltiples).

  • Canaliza la respuesta del servicio externo para mostrarla en la interfaz del usuario.

Guest Code

  • Centraliza toda la inteligencia de la solución.

  • Realiza las validaciones de datos requeridos y lógica matemática (Cantidad $\neq 0$).

  • Transforma la lista de depósitos seleccionados en ítems individuales de operación.

  • Orquesta las llamadas de inserción al ERP y gestiona el manejo de excepciones.

API (AjusteStock)

  • Procesa el impacto contable y físico de las existencias en el inventario.

  • Devuelve el identificador único y el número de documento oficial asignado por el sistema.

Resultado

La operación se realiza en pocos segundos sin abandonar la ficha del producto analizado. Si el proceso termina de forma correcta, la solución despliega un pop-up donde se ingresa la cantidad y el/los depositos

y al aceptar

1 me gusta