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:
- Centralizar la lógica de errores: Permite controlar y manejar errores desde una única estructura de control.
- Flexibilidad en las operaciones: Utiliza
RecordRef
para ser aplicable a diferentes tablas y operaciones. - 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! 😊