Dynamics 365 Business Central

Coding, Ideas

Asignación automática de almacén a cliente.

Hablemos un poco de automatización de operaciones en Business Central.

El escenario es: se quiere integrar un portal de ventas (ecommerce le llaman) a Business Central y uno de los problemas tiene que ver con el almacén del cual se surtirá la mercancía en función a la dirección del cliente.

Link al video en Youtube

https://youtu.be/Ox9heMTsNnE

El enfoque del usuario es que se debe de publicar los almacenes disponibles para que el cliente escoja de cual quiere que se le envíe la mercancía. También se debe revisar si el producto tiene existencias o no en dicho almacén y finalmente, si no hay un almacén disponible, entonces redirigir al cliente con un distribuidor.

En Business Central se puede definir cuál es el almacén principal de envíos a cliente especificándolo en la ficha del cliente.

Analizando el proceso, venderle a un cliente en línea implica enviarle la mercancía a un domicilio especifico desde el almacén mas cercano para minimizar los costos operativos de la empresa. Esto choca completamente con darle al cliente la opción de que el escoja el almacén de envío ya que no esta en su interés que sea el más económico o cercano, francamente al cliente no debería importarle eso.

En la ficha de cliente tenemos un comportamiento muy específico que implica asignar datos en función a un campo, el código postal.

field(91; "Post Code"; Code[20])
        {
            Caption = 'Post Code';
            TableRelation = IF ("Country/Region Code" = CONST('')) "Post Code"
            ELSE
            IF ("Country/Region Code" = FILTER(<> '')) "Post Code" WHERE("Country/Region Code" = FIELD("Country/Region Code"));
            ValidateTableRelation = false;

            trigger OnLookup()
            begin
                OnBeforeLookupPostCode(Rec, PostCode);

                PostCode.LookupPostCode(City, "Post Code", County, "Country/Region Code");

                OnAfterLookupPostCode(Rec, xRec, PostCode);
            end;

            trigger OnValidate()
            var
                IsHandled: Boolean;
            begin
                IsHandled := false;
                OnBeforeValidatePostCode(Rec, PostCode, CurrFieldNo, IsHandled);
                if not IsHandled then
                    PostCode.ValidatePostCode(City, "Post Code", County, "Country/Region Code", (CurrFieldNo <> 0) and GuiAllowed);

                OnAfterValidatePostCode(Rec, xRec);
            end;
        }

Al darse de alta el cliente en el sitio de comercio, se solicitan datos como su nombre, dirección donde reside o donde recibirá la mercancía, datos fiscales y al momento de pagar, se procesa la información para poder generar en Business Central el pedido y poder enviarlo y facturarlo.

¿Qué proceso se sigue para esto?

En el conector de Shopify de Microsoft que existe, el proceso indica que se sincronizan los clientes y los pedidos. Al sincronizar los pedidos, el sistema valida que el cliente exista, y gracias a una configuración, si no existe lo crea sincronizando los clientes de Shopify “jalando” los datos y creando al cliente.

Al momento de crear dicho cliente, llena datos, y uno de ellos es el código postal, lógicamente al llenar el dato, se ejecuta el validate del campo y busca la información relacionada y la aplica.

¿Y si agregamos un campo en la tabla de códigos postales y luego lo adjuntamos al proceso de validate?

Extendamos la tabla de códigos postales

/// <summary>
/// TableExtension ShipLocExt (ID 50100) extends Record Post Code.
/// </summary>
tableextension 50100 ShipLocExt extends "Post Code"
{
    fields
    {
        field(50100; ShipLoc; Code[10])
        {
            Caption = 'Shipment Location';
            DataClassification = CustomerContent;
            TableRelation = Location.Code;
        }
    }
}

Ahora procedemos a extender la página relacionada para poder asignar los almacenes

/// <summary>
/// PageExtension ShipLocPostCodesExt (ID 50100) extends Record Post Codes.
/// </summary>
pageextension 50100 ShipLocPostCodesExt extends "Post Codes"
{
    layout
    {
        addlast(Control1)
        {
            field(ShipLoc; Rec.ShipLoc)
            {
                ApplicationArea = all;
            }
        }
    }
}

Y ahora viene el suscriptor para lo cual creamos una codeunit donde pondremos el código de suscripción a los eventos

/// <summary>
/// Codeunit ShipLocExtSubscribers (ID 50100).
/// </summary>
codeunit 50100 ShipLocExtSubscribers
{
    [EventSubscriber(ObjectType::Table, Database::Customer, 'OnAfterValidatePostCode', '', true, true)]
    local procedure AsignLoc(var Customer: Record Customer)
    var
        PostCode: Record "Post Code";
    begin
        PostCode.Reset();
        PostCode.SetRange(Code, Customer."Post Code");
        if PostCode.FindFirst() then begin
            Customer."Location Code" := PostCode.ShipLoc;
            Customer.Modify(true);
        end;
    end;

    [EventSubscriber(ObjectType::Table, Database::"Ship-to Address", 'OnAfterValidatePostCode', '', true, true)]
    local procedure StAAsignLoc(var ShipToAddress: Record "Ship-to Address"; var PostCode: Record "Post Code")
    begin
        PostCode.Reset();
        PostCode.SetRange(Code, ShipToAddress."Post Code");
        if PostCode.FindFirst() then begin
            ShipToAddress."Location Code" := PostCode.ShipLoc;
            ShipToAddress.Modify(true);
        end;
    end;
}

Se agrega el suscriptor a la tabla de Ship-To Address lo que permite que cuando cambiemos la dirección de envío, se actualice el almacén correspondiente, esto funciona tanto en la ficha de cliente como en el pedido de venta

Entonces, ¿cuál sería el beneficio de este código?

En el sitio o portal de venta se crearán los clientes y se pedirá la información de la dirección de entrega, el campo de Código Postal debería ser obligatorio en el portal de ventas y recordemos que, en México, esta información es obligatoria para la facturación, en caso de que el cliente no pueda deducir el gasto o no le interese, como sea debería ser necesario poner el código postal.

Esta información será insertada en BC al crear el cliente y las direcciones de envío.

Luego, al crear el pedido, el proceso de integración seria:

  • Crear cabecera de pedido
  • Elegir al cliente con lo que se valida la información de la ficha del cliente y asigna el almacén de envío.
  • Si la dirección de envío es otra a la “principal”, al elegir la dirección de envío se actualizará automáticamente el almacén.

Con esto cubrimos la necesidad de “asignar” un almacén de envío desde la alta del cliente. Ahora bien, ¿qué más podríamos hacer con esta idea? Pensemos en rutas de entrega por códigos postales, usemos la misma tabla de extensión de códigos postales para agregar la ruta y liguemos ese campo a una tabla de rutas.

/// <summary>
/// Table Shipping Routes (ID 50100).
/// </summary>
table 50100 "Shipping Routes"
{
    Caption = 'Shipping Routes';
    DataClassification = CustomerContent;

    fields
    {
        field(1; "Code"; Code[20])
        {
            Caption = 'Code';
            DataClassification = CustomerContent;
        }
        field(2; Description; Text[100])
        {
            Caption = 'Description';
            DataClassification = CustomerContent;
        }
        field(3; "Shipping Agent"; Code[20])
        {
            Caption = 'Shipping Agent';
            DataClassification = CustomerContent;
            TableRelation = "Shipping Agent".Code;
        }
    }
    keys
    {
        key(PK; "Code")
        {
            Clustered = true;
        }
    }
}

Y creamos la pagina

/// <summary>
/// Page Route Shipping Agent (ID 50100).
/// </summary>
page 50100 "Route Shipping Agent"
{
    ApplicationArea = All;
    Caption = 'Route Shipping Agent';
    PageType = List;
    SourceTable = "Shipping Routes";
    UsageCategory = Lists;

    layout
    {
        area(content)
        {
            repeater(General)
            {
                field("Code"; Rec."Code")
                {
                    ToolTip = 'Specifies the value of the Code field.';
                }
                field(Description; Rec.Description)
                {
                    ToolTip = 'Specifies the value of the Description field.';
                }
                field("Shipping Agent"; Rec."Shipping Agent")
                {
                    ToolTip = 'Specifies the value of the Shipping Agent field.';
                }
            }
        }
    }
}

Luego agregamos el campo a la extensión previa

/// <summary>
/// TableExtension ShipLocExt (ID 50100) extends Record Post Code.
/// </summary>
tableextension 50100 ShipLocExt extends "Post Code"
{
    fields
    {
        field(50100; ShipLoc; Code[10])
        {
            Caption = 'Shipment Location';
            DataClassification = CustomerContent;
            TableRelation = Location.Code;
        }
        field(50101; ShippingRoute; Code[20])
        {
            Caption = 'Shipping Route';
            DataClassification = CustomerContent;
            TableRelation = "Shipping Routes".Code;
        }
    }
}

Y a la página

/// <summary>
/// PageExtension ShipLocPostCodesExt (ID 50100) extends Record Post Codes.
/// </summary>
pageextension 50100 ShipLocPostCodesExt extends "Post Codes"
{
    layout
    {
        addlast(Control1)
        {
            field(ShipLoc; Rec.ShipLoc)
            {
                ApplicationArea = all;
            }
            field(ShippingRoute; Rec.ShippingRoute)
            {
                ApplicationArea = All;
            }
        }
    }
}

Ya que estamos en eso, creamos nuestros equipos de transporte como transportistas y agreguemos esto a la tabla de ruta, es decir, asignemos que transporte usaremos por ruta y en base a eso poder programar los envíos.

Ya con esto podemos sacar el reporte de Estado envío almacén con lo que aparecería el desglose de los pedidos y sus líneas que debemos de cargar al transporte. Lógicamente tenemos que ajustar la codeunit de suscripción.

/// <summary>
/// Codeunit ShipLocExtSubscribers (ID 50100).
/// </summary>
codeunit 50100 ShipLocExtSubscribers
{
    [EventSubscriber(ObjectType::Table, Database::Customer, 'OnAfterValidatePostCode', '', true, true)]
    local procedure AsignLoc(var Customer: Record Customer)
    var
        PostCode: Record "Post Code";
    begin
        PostCode.Reset();
        PostCode.SetRange(Code, Customer."Post Code");
        if PostCode.FindFirst() then begin
            Customer."Location Code" := PostCode.ShipLoc;
            Customer."Shipping Agent Code" := GetShipAgnt(PostCode.ShippingRoute);
            Customer.Modify(true);
        end;
    end;

    [EventSubscriber(ObjectType::Table, Database::"Ship-to Address", 'OnAfterValidatePostCode', '', true, true)]
    local procedure StAAsignLoc(var ShipToAddress: Record "Ship-to Address"; var PostCode: Record "Post Code")
    begin
        PostCode.Reset();
        PostCode.SetRange(Code, ShipToAddress."Post Code");
        if PostCode.FindFirst() then begin
            ShipToAddress."Location Code" := PostCode.ShipLoc;
            ShipToAddress."Shipping Agent Code" := GetShipAgnt(PostCode.ShippingRoute);
            ShipToAddress.Modify(true);
        end;
    end;

    local procedure GetShipAgnt(var ShipRoutCode: Code[20]): Code[20]
    var
        ShipRoutes: Record "Shipping Routes";
    begin
        ShipRoutes.Reset();
        ShipRoutes.SetRange(Code, ShipRoutCode);
        if ShipRoutes.FindFirst() then
            exit(ShipRoutes."Shipping Agent");
    end;
}

Listo, Business Central ahora puede asignar un almacén de envío en función al código postal de la dirección del cliente, también se puede definir una ruta y en base a eso, se asigna el transporte que deberá llevarlo lo cual aparece en los envíos.

¿Qué haría falta?

  • Cargar todos los códigos postales a los cuales se puede enviar.
  • Asignar los almacenes de envío y las rutas así como los transportes.
  • agregar un control de errores al asignar los códigos postales, por ejemplo, si no se atiende un código postal, simplemente dar un error de ‘ We dont ship to that address’ o algo similar.

Leave a Reply