Implementación de Try/Catch Avanzado en Navision

En este post te explico cómo implementar una funcionalidad de try/catch avanzado en Navision, útil para gestionar errores en versiones antiguas donde no existe esta característica de forma nativa.

Cuando trabajo con Navision, sobre todo en versiones más antiguas, me encuentro con una gran limitación: no existe un sistema nativo de try/catch para gestionar errores de manera adecuada. Por eso, he desarrollado una funcionalidad que permite emular un comportamiento similar al try/catch que conocemos en otros lenguajes de programación. Esta funcionalidad centraliza la gestión de errores y aplica diferentes acciones dependiendo de la operación que se esté ejecutando, como la inserción, modificación, eliminación o validación de registros. He añadido las que mas uso, y las tengo centralizadas en una única codeunit.

Este post te guiará paso a paso en la implementación de esta funcionalidad, explicando cada parte del código y cómo se aplica a escenarios prácticos.

¡Vamos manos a la obra! 🤩

Estructura General del Try/Catch

El corazón de esta implementación es el manejo de las diferentes funciones a través del FunctionID. Según el valor de este identificador, se invocará una función diferente: insertar, modificar, eliminar registros o realizar procesos más complejos como movimientos de ajuste en almacenes.

OBJECT Codeunit 50000 TryCatchAvanzado
{
  OBJECT-PROPERTIES
  {
    Date=01/11/21;
    Time=17:55:57;
    Modified=Yes;
    Version List=UTILS;
  }
  PROPERTIES
  {
    OnRun=BEGIN
            CLEARLASTERROR;

            Char13 := 13;
            Char10 := 10;
            SaltoLinea:=FORMAT(Char13) + FORMAT(Char10);

            CASE FunctionID OF
              1 : InsertRec;
              2 : ModifyRec;
              3 : DeleteRec;
              4 : ValidateRec;
              5 : CreateWarehouseAdjustmentMovement;
              6 : CalculateAdjustmentWarehouse;
              7 : CreateWarehouseAdjustmentMovement; // Función ReverseCreateWarehouseMovements
              8 : CalculateAdjustmentWarehouse; // Función ReverseCalculateAdjustmentWarehouse
              9 : CreateItemJournal;
              10 : CreateItemJournal; // Función ReverseCreateItemJournal
              11 : SendSalesApprovalRequest;
              12 : JournalReclassificationWarehouse;
              13 : Post80;
              14 : ReleaseSalesHeader;
              15 : CreateInvoiceLinesFromShipment;
              16 : CreateCreditMemoLinesFromReturnReceipt;
            END;
          END;
  }

Funciones CRUD

Estas funciones manejan las operaciones básicas de inserción, modificación, eliminación y validación de registros. Utilizan RecordRef y FieldRef, lo que les da flexibilidad para trabajar con cualquier tabla en Navision.

    LOCAL PROCEDURE InsertRec();
    BEGIN
      RefRef.INSERT(TRUE);
    END;

    LOCAL PROCEDURE ModifyRec();
    BEGIN
      RefRef.MODIFY(TRUE);
    END;

    LOCAL PROCEDURE DeleteRec();
    VAR
      Item : Record 27;
    BEGIN
      RefRef.DELETE(TRUE);
    END;

    LOCAL PROCEDURE ValidateRec();
    VAR
      Item : Record 27;
    BEGIN
      Field_Ref.VALIDATE(Value);

      IF NOT RefRef.INSERT(TRUE) THEN
        RefRef.MODIFY(TRUE);
    END;

Estas funciones son clave porque permiten manejar registros de manera flexible. Dependiendo del FunctionID, se ejecuta una de estas operaciones sobre los registros de la base de datos.

Creación de Líneas de Diario de Producto

A continuación, el código que crea líneas de diario de producto, con validación de los datos esenciales como número de artículo, ubicación y cantidad. Posteriormente, las líneas se insertan y se contabilizan.

    LOCAL PROCEDURE CreateItemJournal();
    VAR
      ItemJournalLine : Record 83;
      TypeMov : Integer;
      CreateReservEntry : Codeunit 99000830;
      ItemJnlPostBatch : Codeunit 23;
      QtySeguimiento : Decimal;
      QtyBaseSeguimiento : Decimal;
      TipoSeguimiento : Integer;
      Subtipo : Integer;
      l_PostingDate : Date;
      TempItemJournalLine : TEMPORARY Record 83;
    BEGIN
      CLEAR(ItemJournalLine);
      l_PostingDate := WORKDATE;
      WITH ItemJournalLine DO BEGIN
        RESET;
        SETRANGE("Journal Template Name", JournalTemplateName);
        SETRANGE("Journal Batch Name", JournalBatchName);
        IF FINDFIRST THEN
          DELETEALL;

        IF IsNegativeMov THEN BEGIN
          TypeMov := "Entry Type"::"Negative Adjmt.";
          TipoSeguimiento := 3;
          Subtipo := 3;
        END ELSE BEGIN
          TypeMov := "Entry Type"::"Positive Adjmt.";
          TipoSeguimiento := 3;
          Subtipo := 2;
        END;
        INIT;
        "Journal Template Name" := JournalTemplateName;
        "Journal Batch Name" := JournalBatchName;

        SetUpNewLine(TempItemJournalLine);

        VALIDATE("Item No.", ItemNo);
        VALIDATE("Location Code", LocationCode);
        VALIDATE("Bin Code", BinCode);
        VALIDATE(Quantity, Qty);

        "Posting Date" := l_PostingDate;
        "Entry Type" := TypeMov;
        "Document Date" := l_PostingDate;
        Description := Desc;

        QtySeguimiento := Quantity;
        QtyBaseSeguimiento := "Quantity (Base)";
        IF IsNegativeMov THEN BEGIN
          QtySeguimiento := QtySeguimiento;
          QtyBaseSeguimiento := QtyBaseSeguimiento;
        END;

        IF "Document No." = ''  THEN
          "Document No." := DocNo;

        INSERT;

        CLEAR(CreateReservEntry);
        CreateReservEntry.CreateReservEntryFor(DATABASE::"Item Journal Line", //Numero tabla
                                              Subtipo, // Tipos documento ejemplo 0 = oferta
                                              "Journal Template Name" ,  //No doc o Journal Template Name
                                              "Journal Batch Name",   // blanco o Journal Batch Name
                                              0, // Para el numero de linea de la orden de produccion
                                              "Line No.", //no linea
                                              1,// cantidad por unidad de medida
                                              QtySeguimiento,  // cantidad
                                              QtyBaseSeguimiento, // candidad base
                                              '', //Serie
                                              LotNo); // Lote No

        CreateReservEntry.SetDates(0D, ExpirationDate);

        CreateReservEntry.CreateEntry(ItemJournalLine."Item No.",
                                      '', //variante
                                      "Location Code", // location
                                      Description,
                                      l_PostingDate, //ExpectedReceiptDate
                                      l_PostingDate, //ShipmentDate
                                      0, // No lo se poner 0
                                      TipoSeguimiento); //0=Reservation,1=Tracking,2=Surplus,3=Prospect

        IF NoMovToLiquidate <> 0 THEN BEGIN
          IF IsNegativeMov THEN BEGIN
            CreateReservEntry.SetApplyToEntryNo(NoMovToLiquidate);  //negativo
          END ELSE BEGIN
            CreateReservEntry.SetApplyFromEntryNo(NoMovToLiquidate);  //positivo
          END;
        END;

        CLEAR(ItemJnlPostBatch);
        ItemJnlPostBatch.RUN(ItemJournalLine);
      END;
    END;

Ajustes de Inventario en Almacenes

Esta función maneja los ajustes de inventario en almacenes avanzados. Permite gestionar los movimientos de ajuste para productos con control de lotes y caducidades.

    LOCAL PROCEDURE CreateWarehouseAdjustmentMovement();
    VAR
      WarehouseJournalLine : Record 7311;
      xLoc : Record 14;
      xBin : Record 7354;
      xItem : Record 27;
      WhseJnlReg : Codeunit 7301;
      WarehouseSetup : Record 5769;
      CreateReservEntry : Codeunit 99000830;
    BEGIN
      IF ExpirationDate = 0D THEN
        ExpirationDate := SearchExpirationDate(ItemNo,VariantCode,LotNo);

      CLEAR(WhseJnlReg);
      xLoc.GET(LocationCode);
      xItem.GET(ItemNo);

      WITH WarehouseJournalLine DO BEGIN
        INIT;
        "Item No." := xItem."No.";
        "Unit of Measure Code" := xItem."Base Unit of Measure";
        "Location Code" := xLoc.Code;

        "Entry Type" := "Entry Type"::Movement;
        Quantity :=Qty;
        "Qty. (Base)" := QtyBase;
        "Qty. per Unit of Measure" := 1;
        "Whse. Document No." := DocNo;
        "Registering Date" := WORKDATE;
        "Lot No." := LotNo;
        "Expiration Date" := ExpirationDate;

        Description := Desc;
        "Reason Code" := ReasonCode;

        IF Quantity < 0 THEN
          xBin.GET(xLoc.Code,BinCode)
        ELSE
          xBin.GET(xLoc.Code,xLoc."Adjustment Bin Code");

        "From Bin Type Code" := xBin."Bin Type Code";
        "From Zone Code" := xBin."Zone Code";
        "From Bin Code" := xBin.Code;

        IF Quantity < 0 THEN
          xBin.GET(xLoc.Code,xLoc."Adjustment Bin Code")
        ELSE
          xBin.GET(xLoc.Code,BinCode);

        "To Zone Code" := xBin."Zone Code";
        "To Bin Code" := xBin.Code;

        WhseJnlReg.RUN(WarehouseJournalLine);
      END;
    END;

Calcular ajustes de almacén

El código incluye la creación de ajustes en almacenes, reclasificación de diarios, envío de aprobaciones de ventas, entre otros. Aquí hay algunas más que vale la pena destacar.

Este procedimiento calcula ajustes de inventario en el almacén y actualiza las fechas de caducidad en los registros de reserva si es necesario.

Esta función es fundamental para la creación y contabilización de líneas en el diario de productos, gestionando tanto los movimientos negativos como los positivos de inventario.

    LOCAL PROCEDURE CalculateAdjustmentWarehouse();
    VAR
      ItemJournalLine : Record 83;
      CalcWhseAdjmt : Report 7315;
      Item : Record 27;
      ItemJnlPostBatch : Codeunit 23;
      ReservationEntry : Record 337;
    BEGIN
      IF ExpirationDate = 0D THEN
        ExpirationDate := SearchExpirationDate(ItemNo,VariantCode,LotNo);

      WITH ItemJournalLine DO BEGIN
        SETRANGE("Journal Template Name", JournalTemplateName);
        SETRANGE("Journal Batch Name", JournalBatchName);
        IF NOT ISEMPTY THEN
          DELETEALL(TRUE);

        INIT;
        "Journal Template Name" := JournalTemplateName;
        "Journal Batch Name" := JournalBatchName;

        Item.RESET;
        Item.SETFILTER("Location Filter", LocationCode);
        IF LotNo <> '' THEN
          Item.SETFILTER("Lot No. Filter",LotNo);
        Item.SETRANGE("No.", ItemNo);
        Item.FINDFIRST;

        CLEAR(CalcWhseAdjmt);
        CalcWhseAdjmt.SetItemJnlLine(ItemJournalLine);
        CalcWhseAdjmt.InitializeRequest(WORKDATE, WhseDocumentNo);
        CalcWhseAdjmt.SetHideValidationDialog(FALSE);
        CalcWhseAdjmt.USEREQUESTPAGE(FALSE);
        CalcWhseAdjmt.SETTABLEVIEW(Item);
        CalcWhseAdjmt.RUN;

        IF FINDSET THEN
          REPEAT
            IF ExpirationDate <> 0D THEN BEGIN
              ReservationEntry.SETRANGE("Item No.", ItemNo);
              ReservationEntry.SETRANGE("Location Code", LocationCode);
              ReservationEntry.SETRANGE("Source ID", "Journal Template Name");
              ReservationEntry.SETRANGE("Source Batch Name", "Journal Batch Name");
              ReservationEntry.SETRANGE("Source Ref. No.",ItemJournalLine."Line No.");
              IF ReservationEntry.FINDSET THEN
                REPEAT
                  IF ReservationEntry."Expiration Date" <> 0D THEN BEGIN
                    ReservationEntry."Expiration Date" := ExpirationDate;
                    ReservationEntry.MODIFY;
                  END;
                UNTIL ReservationEntry.NEXT=0;
            END;
            ItemJournalLine."Reason Code" := ReasonCode;
            ItemJournalLine.MODIFY;
          UNTIL ItemJournalLine.NEXT=0;

        IF ItemJournalLine.FINDFIRST THEN BEGIN
          CLEAR(ItemJnlPostBatch);
          ItemJnlPostBatch.RUN(ItemJournalLine);
        END;
      END;
    END;

Reverso de la Creación de Líneas de Diario

Para revertir una línea de diario creada anteriormente, esta función invierte las cantidades y vuelve a ejecutar el proceso.

    PROCEDURE ReverseCreateItemJournal();
    BEGIN
      FunctionID := 10;
      Qty := -Qty;
      QtyBase := -QtyBase;
    END;

Enviar Solicitud de Aprobación de Ventas

Aunque esta función aún no está implementada, se incluye como una referencia para solicitudes de aprobación de ventas futuras.

    LOCAL PROCEDURE SendSalesApprovalRequest();
    BEGIN
      ERROR('funcionalidad aun no creada para 2018');
    END;

Reclasificación en el Diario de Almacén

Esta función maneja el proceso de reclasificación en el diario de almacén. Permite modificar la ubicación, el lote y la caducidad de los productos en el inventario.

    LOCAL PROCEDURE JournalReclassificationWarehouse();
    VAR
      WarehouseJournalLine : Record 7311;
      xBin : Record 7354;
      xItem : Record 27;
      WhseJnlRegisterBatch : Codeunit 7304;
    BEGIN
      IF ExpirationDate = 0D THEN
        ExpirationDate := SearchExpirationDate(ItemNo,VariantCode,LotNo);
      CLEAR(WhseJnlRegisterBatch);
      xItem.GET(ItemNo);

      IF Qty < 0 THEN
        ERROR('La cantidad no puede ser negativa');

      WITH WarehouseJournalLine DO BEGIN
        RESET;
        SETRANGE("Journal Template Name", JournalTemplateName);
        SETRANGE("Journal Batch Name", JournalBatchName);
        IF NOT ISEMPTY THEN
          DELETEALL;

        INIT;
        "Journal Template Name" := JournalTemplateName;
        "Journal Batch Name" := JournalBatchName;
        "Item No." := xItem."No.";
        "Unit of Measure Code" := xItem."Base Unit of Measure";
        "Location Code":= LocationCode;

        "Entry Type" := "Entry Type"::Movement;
        Quantity :=Qty;
        "Qty. (Base)" := QtyBase;
        "Qty. per Unit of Measure" := 1;
        "Whse. Document No." := DocNo;
        "Lot No." := LotNo;
        "New Lot No." := NewLotNo;
        "Expiration Date" := ExpirationDate;
        "New Expiration Date" := NewExpirationDate;
        Description := Desc;
        "Reason Code" := ReasonCode;

        xBin.GET(LocationCode,BinCode);
        "From Bin Type Code" := xBin."Bin Type Code";
        "From Zone Code" := xBin."Zone Code";
        "From Bin Code" := xBin.Code;

        xBin.GET(LocationCode,ToBinCode);
        "To Zone Code" := xBin."Zone Code";
        "To Bin Code" := xBin.Code;

        INSERT;

        WhseJnlRegisterBatch.RUN(WarehouseJournalLine);
      END;
    END;

Registrar el Pedido de Venta

La función Post80 ejecuta el proceso de contabilización de un pedido de venta utilizando la codeunit estándar 80.

    LOCAL PROCEDURE Post80();
    BEGIN
      CODEUNIT.RUN(80,SalesHeader);
    END;

Lanzar un Pedido de Venta

Esta función permite lanzar un pedido de venta. Utiliza la codeunit estándar 414 para realizar la lanzar documentos de ventas.

    LOCAL PROCEDURE ReleaseSalesHeader();
    VAR
      ReleaseSalesDoc : Codeunit 414;
    BEGIN
      ReleaseSalesDoc.PerformManualRelease(SalesHeader);
    END;

Crear Líneas de Factura desde el Envío

Esta función es útil cuando se necesita generar las líneas de factura a partir de las líneas de envío ya registradas.

    LOCAL PROCEDURE CreateInvoiceLinesFromShipment();
    VAR
      SalesGetShpt : Codeunit 64;
    BEGIN
      CLEAR(SalesGetShpt);
      SalesGetShpt.SetSalesHeader(SalesHeader);
      SalesGetShpt.CreateInvLines(SalesShipmentLine);
    END;

Crear Líneas de Abono desde el Albarán de Devolución

Esta función genera las líneas de abono a partir de las líneas de devolución ya registradas.

    LOCAL PROCEDURE CreateCreditMemoLinesFromReturnReceipt();
    VAR
      SalesGetReturnReceipts : Codeunit 6638;
    BEGIN
      CLEAR(SalesGetReturnReceipts);
      SalesGetReturnReceipts.SetSalesHeader(SalesHeader);
      SalesGetReturnReceipts.CreateInvLines(ReturnReceiptLine);
    END;

Beneficios y Aplicaciones Prácticas

Este código proporciona una forma robusta de manejar errores en Navision, donde no existe un sistema nativo de try/catch en las versiones antiguas. Implementando esta solución puedes:

  1. Centralizar la lógica de errores: Permite controlar y manejar errores desde una única estructura de control.
  2. Flexibilidad en las operaciones: Utiliza RecordRef para ser aplicable a diferentes tablas y operaciones.
  3. Mayor estabilidad en los procesos: Previene fallos inesperados del sistema, garantizando una mejor gestión de los errores.

Conclusión

Este código de try/catch avanzado permite mejorar la gestión de errores en Navision, sobre todo en las versiones antiguas donde no se dispone de un mecanismo nativo para tal fin. Gracias a esta implementación, puedes manejar los errores de manera centralizada, flexible y eficiente.

Si quieres ver el código completo, está en GitHub.

¡Espero que este post te haya sido útil! 😊

Share your love

Leave a Reply

Your email address will not be published. Required fields are marked *