Envíos de Correos Avanzados en Business Central: Personalización y Automatización

Aprende a configurar envíos de correos avanzados en Business Central, integrando plantillas, idiomas y adjuntos personalizados.

En el entorno empresarial actual, la eficiencia en la comunicación es clave, y Dynamics 365 Business Central ofrece herramientas poderosas para lograrlo. Si bien en un blog anterior se explicó cómo gestionar los envíos de correos electrónicos a nivel básico, este post se enfoca en cómo llevar esa funcionalidad al siguiente nivel. Aquí te mostraré cómo extender las capacidades de Business Central para gestionar mejor la selección de informes y personalizar los correos electrónicos enviados desde la plataforma.

Este desarrollo permite enviar correos electrónicos utilizando plantillas configurables en varios idiomas, personalizando tanto el asunto como el cuerpo del mensaje según las necesidades del usuario. Además, ofrece la opción de adjuntar documentos en distintos idiomas, asegurando que cada destinatario reciba la información en su idioma preferido. Con esta funcionalidad, podrás adaptar las comunicaciones automatizadas de tu empresa para que sean más efectivas y alineadas con los requerimientos específicos de cada situación.

¡Vamos manos a la obra! 🤤

Report Selections

En esta sección, te mostraré cómo extender la tabla y la página de selección de informes para personalizar el envío de correos electrónicos en Business Central. Estas extensiones permiten gestionar plantillas de correo, definir idiomas, y configurar el asunto y el cuerpo de los correos, así como adjuntar documentos de manera automatizada y personalizada.

Añadiendo Potencia a la Tabla de Selección de Informes

La tableextension 60046 "Report Selections" añade nuevos campos que te permiten controlar cómo se usarán los informes en los correos electrónicos. Estos campos son esenciales para personalizar el asunto, el cuerpo del correo y para gestionar adjuntos en diferentes idiomas.

tableextension 60046 "Report Selections" extends "Report Selections"
{
    fields
    {
        //crs-al disable
        field(60000; "Use for Email Subject"; Boolean)
        {
            Caption = 'Use for Email Subject', Comment = 'ESP="Uso para el asunto del correo electrónico"';
            DataClassification = CustomerContent;

            trigger OnValidate()
            begin
                if not Rec."Use for Email Subject" then
                    Rec.Validate("Subject Layout Code", '');
            end;
        }

Aquí se ha añadido el campo "Use for Email Subject" como un campo booleano que permite al usuario definir si un informe específico será utilizado para generar el asunto del correo electrónico. Si este campo está activado (true), el sistema utilizará el diseño especificado en el campo "Subject Layout Code" para construir el asunto del correo. En caso contrario, se vaciará el código del diseño para el asunto. Esto da la flexibilidad de decidir si un informe específico debe influir en el asunto del correo, o no.

        field(60001; "Subject Layout Descr."; Text[250])
        {
            CalcFormula = lookup("Custom Report Layout".Description where(Code = field("Subject Layout Code")));
            Caption = 'Email Subject Layout Description', Comment = 'ESP="Descripción del diseño del asunto del correo electronico"';
            Editable = false;
            FieldClass = FlowField;

            trigger OnLookup()
            var
                CustomReportLayout: Record "Custom Report Layout";
            begin
                if Rec."Subject Layout Type" = Rec."Subject Layout Type"::"Custom Report Layout" then
                    if CustomReportLayout.LookupLayoutOK("Report ID") then
                        Rec.Validate("Subject Layout Code", CustomReportLayout.Code);
            end;
        }

El campo "Subject Layout Descr." es un campo de solo lectura que proporciona una descripción del diseño del asunto del correo electrónico, según el código de diseño especificado en "Subject Layout Code". Este campo se calcula automáticamente (es un FlowField) utilizando una fórmula que busca la descripción en la tabla de diseños de informes personalizados (Custom Report Layout). Además, si el usuario necesita seleccionar o cambiar el diseño del asunto, puede hacerlo mediante un lookup, que abrirá una lista de diseños disponibles. Esto asegura que siempre se está utilizando el diseño correcto y evita errores manuales.

        field(60002; "Subject Layout Code"; Code[20])
        {
            Caption = 'Email Subject Layout Code', Comment = 'ESP="Código de diseño de informe personalizado del asunto"';
            DataClassification = CustomerContent;
            TableRelation = if ("Subject Layout Type" = const("Custom Report Layout")) "Custom Report Layout".Code where(Code = field("Subject Layout Code"),
                                                                                                                           "Report ID" = field("Report ID"))
            else
            if ("Subject Layout Type" = const("HTML Layout")) "O365 HTML Template".Code;

            trigger OnValidate()
            begin
                CalcFields(Rec."Subject Layout Descr.");
            end;
        }

El campo "Subject Layout Code" almacena el código del diseño que se utilizará para el asunto del correo electrónico. Dependiendo del tipo de diseño seleccionado (Custom Report Layout o HTML Layout), este campo se relaciona con la tabla correspondiente (Custom Report Layout o O365 HTML Template). Esta flexibilidad permite utilizar tanto informes personalizados como plantillas HTML estándar, dependiendo de las necesidades del usuario. Al validar este campo, se recalcula la descripción del diseño para asegurar que la información mostrada sea siempre correcta.

        field(60003; "Subject Layout Type"; Enum "Email Body Layout Type")
        {
            Caption = 'Email Body Layout Type', Comment = 'ESP="Tipo de diseño del asunto del correo"';
            DataClassification = CustomerContent;
            InitValue = "Custom Report Layout";
        }
        field(60004; "Language Code"; Code[10])
        {
            Caption = 'Language Code', Comment = 'ESP="Cód. idioma"';
            DataClassification = CustomerContent;
            TableRelation = Language;
        }
        field(60006; "Mail Only Option"; Boolean)
        {
            Caption = 'Mail Only Option', Comment = 'ESP="Opción solo para correos"';
            DataClassification = CustomerContent;
        }
        //crs-al enable
    }

    keys
    {
        key(FK01; "Use for Email Body") { }
        key(FK02; "Use for Email Subject") { }
    }
}

“Subject Layout Type”: Este campo permite seleccionar el tipo de diseño que se utilizará para el asunto del correo electrónico. Se ha configurado un valor predeterminado que es "Custom Report Layout", lo que indica que, por defecto, se utilizarán diseños personalizados.

“Language Code”: Este campo permite especificar el código de idioma que se utilizará para el informe. Esto es particularmente útil cuando se necesitan enviar correos electrónicos en diferentes idiomas, asegurando que el contenido se adapte al destinatario.

“Mail Only Option”: Este campo booleano indica si el informe es solo para correos electrónicos. Al activarlo, se excluyen estos informes de otras operaciones que no estén relacionadas con el envío de correos.

Con esto, la extensión de la tabla Report Selections queda completa, proporcionando todas las herramientas necesarias para personalizar y adaptar los correos electrónicos enviados desde Business Central.

Mejorando la Página de Selección de Informes de Ventas

Una vez que la tabla ha sido extendida, el siguiente paso es hacer esos nuevos campos visibles y configurables para los usuarios mediante una extensión de la página correspondiente. Aquí es donde entra en juego la pageextension 60025 "ReportSelectionSales".

pageextension 60025 "ReportSelectionSales" extends "Report Selection - Sales"
{
    layout
    {
        modify("EmailLayoutCaption")
        {
            Visible = false;
        }
        modify("Email Body Layout Description")
        {
            Visible = true;
        }

Este fragmento comienza por modificar la disposición de la página. Se oculta el campo "EmailLayoutCaption" que probablemente no sea relevante en el nuevo contexto, y se asegura de que la descripción del diseño del cuerpo del correo ("Email Body Layout Description") sea visible para el usuario, permitiendo una fácil configuración y revisión.

        addafter("Use for Email Attachment")
        {
            field("Language Code"; Rec."Language Code")
            {
                ToolTip = 'Specifies the value of the field', comment = 'ESP="Especifica el valor del campo"';
                ApplicationArea = All;
            }
        }
        addbefore("Use for Email Body")
        {
            field("Mail Only Option"; Rec."Mail Only Option")
            {
                ToolTip = 'Specifies the value of the field', comment = 'ESP="Especifica el valor del campo"';
                ApplicationArea = All;
            }
            field("Use for Email Subject"; Rec."Use for Email Subject")
            {
                ToolTip = 'Specifies the value of the field', comment = 'ESP="Especifica el valor del campo"';
                ApplicationArea = All;
            }
        }

Aquí se añaden nuevos campos a la página, permitiendo al usuario configurar el idioma ("Language Code"), definir si un informe es solo para correos electrónicos ("Mail Only Option"), y activar o desactivar el uso del informe para el asunto del correo ("Use for Email Subject"). Estas opciones proporcionan un control granular sobre cómo se manejarán los correos electrónicos, lo que es especialmente útil en entornos donde se maneja una gran cantidad de informes y plantillas.

        addbefore("Email Body Layout Description")
        {
            field("Subject Layout Descr."; Rec."Subject Layout Descr.")
            {
                ToolTip = 'Specifies the value of the field', comment = 'ESP="Especifica el valor del campo"';
                ApplicationArea = All;

                trigger OnDrillDown()
                var
                    CustomReportLayout: Record "Custom Report Layout";
                begin
                    if CustomReportLayout.LookupLayoutOK(Rec."Report ID") then
                        Rec.Validate("Subject Layout Code", CustomReportLayout.Code);
                end;
            }
        }
    }
}

Finalmente, se añade el campo "Subject Layout Descr." justo antes de la descripción del diseño del cuerpo del correo. Esto permite al usuario visualizar y seleccionar fácilmente el diseño que se utilizará para el asunto del correo, asegurando que toda la información relevante esté claramente accesible y organizada en la página.

Con estas extensiones, no solo se mejora la funcionalidad de envío de correos electrónicos, sino que también se asegura que los usuarios puedan configurar y personalizar estos envíos de manera intuitiva y eficiente.

Report Selection Warehouse

En esta sección, te mostraré cómo extender la tabla y la página de selección de informes específicamente para la gestión de almacenes en Business Central. Estas extensiones permiten una configuración detallada de los correos electrónicos relacionados con operaciones de almacén, permitiendo personalizar tanto el contenido como los adjuntos que se envían, todo adaptado al idioma y formato requerido.

El Poder del Asunto: Personalizando el Asunto del Correo

La primera parte de la tableextension 60047 "Report Selection Warehouse" se centra en cómo personalizar el asunto del correo electrónico cuando se envían informes relacionados con operaciones de almacén.

tableextension 60047 "Report Selection Warehouse" extends "Report Selection Warehouse"
{
    fields
    {
        field(60000; "Use for Email Subject"; Boolean)
        {
            Caption = 'Use for Email Subject', Comment = 'ESP="Uso para el asunto del correo electrónico"';
            DataClassification = CustomerContent;

            trigger OnValidate()
            begin
                if not Rec."Use for Email Subject" then
                    Rec.Validate("Subject Layout Code", '');
            end;
        }

Este campo booleano, "Use for Email Subject", permite al usuario especificar si un informe en particular debe ser utilizado para generar el asunto del correo electrónico. Si está activado (true), el sistema utilizará el diseño especificado en el campo "Subject Layout Code" para crear el asunto. Si se desactiva, el código del diseño del asunto se vacía, asegurando que no se utilice este informe para ese propósito.

        field(60001; "Subject Layout Descr."; Text[250])
        {
            CalcFormula = lookup("Custom Report Layout".Description where(Code = field("Subject Layout Code")));
            Caption = 'Email Subject Layout Description', Comment = 'ESP="Descripción del diseño del asunto del correo electronico"';
            Editable = false;
            FieldClass = FlowField;

            trigger OnLookup()
            var
                CustomReportLayout: Record "Custom Report Layout";
            begin
                if Rec."Subject Layout Type" = Rec."Subject Layout Type"::"Custom Report Layout" then
                    if CustomReportLayout.LookupLayoutOK("Report ID") then
                        Rec.Validate("Subject Layout Code", CustomReportLayout.Code);
            end;
        }

El campo "Subject Layout Descr." es un FlowField que proporciona una descripción del diseño del asunto basado en el código seleccionado. Esto asegura que los usuarios puedan ver fácilmente qué diseño se está utilizando, lo que reduce la posibilidad de errores. Además, el trigger OnLookup() facilita la selección de un diseño adecuado al abrir una lista de diseños disponibles cuando es necesario.

Configurando el Cuerpo del Correo: Diseños Personalizados

Continuando con la extensión, se abordan los campos que controlan cómo se configurará el cuerpo del correo electrónico.

        field(60002; "Subject Layout Code"; Code[20])
        {
            Caption = 'Email Subject Layout Code', Comment = 'ESP="Código de diseño de informe personalizado del asunto"';
            DataClassification = CustomerContent;
            TableRelation = if ("Subject Layout Type" = const("Custom Report Layout")) "Custom Report Layout".Code where(Code = field("Subject Layout Code"),
                                                                                                                           "Report ID" = field("Report ID"))
            else
            if ("Subject Layout Type" = const("HTML Layout")) "O365 HTML Template".Code;

            trigger OnValidate()
            begin
                CalcFields(Rec."Subject Layout Descr.");
            end;
        }

El campo "Subject Layout Code" permite seleccionar el diseño específico que se utilizará para el asunto del correo. Dependiendo del tipo de diseño seleccionado (Custom Report Layout o HTML Layout), este campo se relaciona con la tabla correspondiente. Esto otorga flexibilidad al usuario para elegir entre diferentes tipos de diseños, dependiendo del contexto y el formato del informe.

        field(60003; "Subject Layout Type"; Enum "Email Body Layout Type")
        {
            Caption = 'Email Body Layout Type', Comment = 'ESP="Tipo de diseño del asunto del correo"';
            DataClassification = CustomerContent;
            InitValue = "Custom Report Layout";
        }
        field(60004; "Language Code"; Code[10])
        {
            Caption = 'Language Code', Comment = 'ESP="Cód. idioma"';
            TableRelation = Language;
        }

Aquí se configuran dos campos adicionales:

“Subject Layout Type”: Este campo enum permite seleccionar el tipo de diseño para el asunto, con un valor inicial predeterminado de "Custom Report Layout".

“Language Code”: Este campo define el código de idioma para el informe, asegurando que el correo electrónico se envíe en el idioma preferido del destinatario. Esto es especialmente útil en entornos multilingües.

Adjuntos Inteligentes: Configurando los Adjuntos del Correo

La siguiente sección de la tabla se enfoca en cómo se manejarán los archivos adjuntos dentro de los correos electrónicos.

        field(60010; "Use for Email Attachment"; Boolean)
        {
            Caption = 'Use for Email Attachment', Comment = 'ESP="Usar para los datos adjuntos de correo electrónico"';
            DataClassification = CustomerContent;
            InitValue = true;

            trigger OnValidate()
            begin
                if not "Use for Email Body" then begin
                    "Email Body Layout Code" := '';
                    "Email Body Layout Name" := '';
                end;
            end;
        }

        field(60011; "Use for Email Body"; Boolean)
        {
            Caption = 'Use for Email Body', Comment = 'ESP="Usar para el cuerpo del correo electrónico"';
            DataClassification = CustomerContent;

            trigger OnValidate()
            begin
                if not "Use for Email Body" then begin
                    "Email Body Layout Code" := '';
                    "Email Body Layout Name" := '';
                end;
            end;
        }

“Use for Email Attachment”: Este campo permite decidir si un informe será utilizado como archivo adjunto en el correo electrónico. Si no se va a usar para el cuerpo del correo, los campos relacionados con el diseño del cuerpo se vacían automáticamente.

“Use for Email Body”: Similar al campo anterior, pero enfocado en la inclusión del informe como parte del cuerpo del correo electrónico. Si se desactiva, los campos relacionados se vacían, evitando posibles errores en la configuración del correo.

Diseño del Cuerpo del Correo: Flexibilidad Total

La extensión sigue con campos que permiten una configuración detallada del cuerpo del correo, ofreciendo flexibilidad en la selección y uso de diseños personalizados.

        field(60020; "Email Body Layout Code"; Code[20])
        {
            Caption = 'Email Body Custom Layout Code', Comment = 'ESP="Código del diseño personalizado del correo electrónico"';
            DataClassification = CustomerContent;
            TableRelation = if ("Email Body Layout Type" = const("Custom Report Layout")) "Custom Report Layout".Code where(Code = field("Email Body Layout Code"), "Report ID" = field("Report ID"), "Built-In" = const(false))
            else
            if ("Email Body Layout Type" = const("HTML Layout")) "O365 HTML Template".Code;

            trigger OnValidate()
            begin
                if "Email Body Layout Code" <> '' then begin
                    Testfield("Use for Email Body", true);
                    "Email Body Layout Name" := '';
                end;
                Calcfields("EmailBodyLayoutDescription");
            end;
        }

Este campo, "Email Body Layout Code", almacena el código del diseño que se utilizará para el cuerpo del correo. Al igual que con el asunto, la relación con diferentes tablas permite una selección personalizada según el tipo de diseño seleccionado.

        field(60021; "EmailBodyLayoutDescription"; Text[250])
        {
            CalcFormula = lookup("Custom Report Layout".Description where(Code = field("Email Body Layout Code")));
            Caption = 'Email Body Custom Layout Description', Comment = 'ESP="Descripción del diseño del cuerpo del correo electrónico"';
            Editable = false;
            FieldClass = Flowfield;

            trigger OnLookup()
            var
                CustomReportLayout: Record "Custom Report Layout";
            begin
                if "Email Body Layout Type" = "Email Body Layout Type"::"Custom Report Layout" then
                    if CustomReportLayout.LookupLayoutOK("Report ID") then
                        Validate("Email Body Layout Code", CustomReportLayout.Code);
            end;
        }
        field(60022; "Email Body Layout Type"; Enum "Email Body Layout Type")
        {
            Caption = 'Email Body Layout Type', Comment = 'ESP="Tipo de diseño del cuerpo del correo"';
            DataClassification = CustomerContent;
        }

El campo "EmailBodyLayoutDescription" proporciona una descripción del diseño seleccionado para el cuerpo del correo, calculado automáticamente, asegurando que siempre se muestre la información correcta y actualizada. Además, el campo "Email Body Layout Type" permite al usuario definir el tipo de diseño a utilizar, ofreciendo flexibilidad adicional.

Integración con el Informe: Selección y Validación del Diseño

Finalmente, la extensión aborda cómo se relacionan estos campos con el diseño del informe en su totalidad.

        field(60023; "Email Body Layout Name"; Text[250])
        {
            Caption = 'Email Body Layout Name', Comment = 'ESP="Nombre de diseño del cuerpo del correo electrónico"';
            DataClassification = CustomerContent;
            TableRelation = "Report Layout List".Name where("Report ID" = field("Report ID"));

            trigger OnLookup()
            var
                Report

LayoutList: Record "Report Layout List";
                ReportManagement: Codeunit ReportManagement;
                Handled: Boolean;
            begin
                ReportLayoutList.SetRange("Report ID", Rec."Report ID");
                ReportManagement.OnSelectReportLayout(ReportLayoutList, Handled);
                if not Handled then
                    exit;
                "Email Body Layout Name" := ReportLayoutList.Name;
                "Email Body Layout AppID" := ReportLayoutList."Application ID";
            end;

            trigger OnValidate()
            var
                ReportLayoutList: Record "Report Layout List";
            begin
                if "Email Body Layout Name" <> '' then begin
                    "Use for Email Body" := true;
                    "Email Body Layout Code" := '';
                    "EmailBodyLayoutDescription" := '';
                    ReportLayoutList.SetRange(Name, "Email Body Layout Name");
                    ReportLayoutList.SetRange("Report ID", Rec."Report ID");
                    if not IsNullGuid("Email Body Layout AppID") then
                        ReportLayoutList.SetRange("Application ID", "Email Body Layout AppID");
                    if not ReportLayoutList.FindFirst() then begin
                        ReportLayoutList.SetRange("Application ID");
                        ReportLayoutList.FindFirst();
                    end;
                    if IsNullGuid("Email Body Layout AppID") then
                        Rec."Email Body Layout AppID" := ReportLayoutList."Application ID";
                end;
            end;
        }

“Email Body Layout Name”: Este campo permite seleccionar el nombre del diseño que se utilizará para el cuerpo del correo, relacionado con el informe en cuestión. El sistema ofrece la posibilidad de elegir entre varios diseños disponibles en la lista de diseños de informes (Report Layout List).

        field(60024; "Email Body Layout AppID"; Guid)
        {
            Caption = 'Email Body Layout AppID', Comment = 'ESP="Id. de aplicación de diseño del cuerpo del correo electrónico"';
            DataClassification = CustomerContent;
            TableRelation = "Report Layout List"."Application ID" where("Report ID" = field("Report ID"));
        }

        field(60025; "Email Body Layout Caption"; Text[250])
        {
            Caption = 'Email Body Layout', Comment = 'ESP="Diseño del cuerpo del correo electrónico"';
            FieldClass = FlowField;
            CalcFormula = lookup("Report Layout List".Caption where("Report ID" = field("Report ID"), Name = field("Email Body Layout Name")));

            trigger OnLookup()
            var
                ReportLayoutList: Record "Report Layout List";
                ReportManagement: Codeunit ReportManagement;
                Handled: Boolean;
            begin
                ReportLayoutList.SetRange("Report ID", Rec."Report ID");
                ReportManagement.OnSelectReportLayout(ReportLayoutList, Handled);
                if not Handled then
                    exit;
                "Email Body Layout Name" := ReportLayoutList.Name;
                "Email Body Layout AppID" := ReportLayoutList."Application ID";
            end;
        }

Estos campos complementan la configuración del cuerpo del correo, permitiendo asociar un diseño con un AppID específico y proporcionando una descripción del diseño seleccionado para una mayor claridad.

Finalizando la Configuración con Opciones de Informe

El último conjunto de campos y procedimientos se centra en cómo gestionar y seleccionar los diseños de informe completos, asegurando que todo el correo electrónico esté perfectamente alineado con los requerimientos del usuario.

        field(60030; "Report Layout Name"; Text[250])
        {
            Caption = 'Report Layout name', Comment = 'ESP="Tipo de diseño de informe"';
            DataClassification = CustomerContent;
            TableRelation = "Report Layout List".Name where("Report ID" = field("Report ID"));

            trigger OnLookup()
            var
                ReportLayoutList: Record "Report Layout List";
                ReportManagement: Codeunit ReportManagement;
                Handled: Boolean;
            begin
                ReportLayoutList.SetRange("Report ID", Rec."Report ID");
                ReportManagement.OnSelectReportLayout(ReportLayoutList, Handled);
                if not Handled then
                    exit;
                "Report Layout Name" := ReportLayoutList.Name;
                "Report Layout AppID" := ReportLayoutList."Application ID";
            end;

            trigger OnValidate()
            var
                ReportLayoutList: Record "Report Layout List";
            begin
                if "Report Layout Name" <> '' then begin
                    "Use for Email Attachment" := true;
                    ReportLayoutList.SetRange(Name, "Report Layout Name");
                    ReportLayoutList.SetRange("Report ID", Rec."Report ID");
                    if not IsNullGuid("Report Layout AppID") then
                        ReportLayoutList.SetRange("Application ID", "Report Layout AppID");
                    if not ReportLayoutList.FindFirst() then begin
                        ReportLayoutList.SetRange("Application ID");
                        ReportLayoutList.FindFirst();
                    end;
                    if IsNullGuid("Report Layout AppID") then
                        Rec."Report Layout AppID" := ReportLayoutList."Application ID";
                end;
            end;
        }

El campo "Report Layout Name" permite al usuario seleccionar el diseño específico que se utilizará para el informe. Este nombre de diseño está relacionado con el informe específico y el AppID correspondiente, lo que facilita la gestión y aseguramiento de que el informe correcto se adjunta y se visualiza adecuadamente en el correo.

        field(60031; "Report Layout AppID"; Guid)
        {
            Caption = 'Report Layout App ID', Comment = 'ESP="Id. de aplicación de diseño de informe"';
            DataClassification = CustomerContent;
            Editable = false;
        }

        field(60032; "Report Layout Caption"; Text[250])
        {
            Caption = 'Report Layout', Comment = 'ESP="Diseño de informe"';
            FieldClass = FlowField;
            CalcFormula = lookup("Report Layout List".Caption where("Report ID" = field("Report ID"), Name = field("Report Layout Name")));

            trigger OnLookup()
            var
                ReportLayoutList: Record "Report Layout List";
                ReportManagement: Codeunit ReportManagement;
                Handled: Boolean;
            begin
                ReportLayoutList.SetRange("Report ID", Rec."Report ID");
                ReportManagement.OnSelectReportLayout(ReportLayoutList, Handled);
                if not Handled then
                    exit;
                "Report Layout Name" := ReportLayoutList.Name;
                "Report Layout AppID" := ReportLayoutList."Application ID";
                if "Report Layout Name" <> '' then
                    "Use for Email Attachment" := true;
            end;
        }
        field(60033; "Mail Only Option"; Boolean)
        {
            Caption = 'Mail Only Option', Comment = 'ESP="Opción solo para correos"';
            DataClassification = CustomerContent;
        }
    }

    keys
    {
        key(FK01; "Use for Email Body") { }
        key(FK02; "Use for Email Subject") { }
    }
}

El Procedimiento DrillDownToSelectLayout: Seleccionando el Diseño Correcto

El procedimiento DrillDownToSelectLayout es un elemento clave que facilita la selección del diseño correcto para los informes que se enviarán por correo electrónico. Este procedimiento permite al usuario realizar una selección precisa del diseño desde una lista disponible, asegurando que se utilice el formato adecuado para cada situación.

    procedure DrillDownToSelectLayout(var SelectedLayoutName: Text[250]; var SelectedLayoutAppID: Guid)
    var
        ReportLayoutListSelection: Record "Report Layout List";
        ReportManagementCodeunit: Codeunit ReportManagement;
        IsReportLayoutSelected: Boolean;
    begin
        ReportLayoutListSelection.SetRange("Report ID", Rec."Report ID");
        ReportManagementCodeunit.OnSelectReportLayout(ReportLayoutListSelection, IsReportLayoutSelected);
        if IsReportLayoutSelected then begin
            SelectedLayoutName := ReportLayoutListSelection."Name";
            SelectedLayoutAppID := ReportLayoutListSelection."Application ID";
        end;
    end;

Este procedimiento funciona de la siguiente manera:

  • Establece un Rango de Selección: Filtra la lista de diseños disponibles para el Report ID específico que está en uso. Esto significa que el usuario solo verá los diseños relevantes para el informe actual.
  • Utiliza el Codeunit ReportManagement: Este codeunit maneja la lógica necesaria para seleccionar un diseño de informe. Se llama al evento OnSelectReportLayout, que permite seleccionar un diseño adecuado de la lista filtrada.
  • Devuelve los Resultados: Si se selecciona un diseño, se actualizan las variables SelectedLayoutName y SelectedLayoutAppID con los valores correspondientes. Estos valores se utilizarán para configurar el informe que se enviará por correo electrónico.

Este procedimiento es vital para asegurar que cada correo electrónico enviado utilice el diseño correcto, manteniendo la coherencia y la calidad en la comunicación con los destinatarios.

Personalizando la Página de Selección de Informes de Almacén

Con los nuevos campos de la tabla ya configurados, es importante exponer estas opciones a los usuarios en la interfaz de Business Central. Esto se logra mediante la pageextension 60026 "ReportSelectionWarehouse".

pageextension 60026 "ReportSelectionWarehouse" extends "Report Selection - Warehouse"
{
    layout
    {
        addlast("Control1")
        {
            field("Mail Only Option"; Rec."Mail Only Option")
            {
                ToolTip = 'Specifies the value of the field', comment = 'ESP="Especifica el valor del campo"';
                ApplicationArea = All;
            }
            field("Use for Email Subject"; Rec."Use for Email Subject")
            {


 ToolTip = 'Specifies the value of the field', comment = 'ESP="Especifica el valor del campo"';
                ApplicationArea = All;
            }

Este código empieza añadiendo campos clave a la página, como "Mail Only Option" y "Use for Email Subject". Estos campos permiten a los usuarios configurar si los informes deben ser utilizados exclusivamente para correos electrónicos y si deben ser utilizados en el asunto del correo, respectivamente. Estos ajustes son esenciales para personalizar cómo se gestionan los correos electrónicos relacionados con los informes de almacén.

            field("Use for Email Body"; Rec."Use for Email Body")
            {
                ToolTip = 'Specifies the value of the field', comment = 'ESP="Especifica el valor del campo"';
                ApplicationArea = All;
            }
            field("Use for Email Attachment"; Rec."Use for Email Attachment")
            {
                ToolTip = 'Specifies the value of the field', comment = 'ESP="Especifica el valor del campo"';
                ApplicationArea = All;
            }
            field("Language Code"; Rec."Language Code")
            {
                ToolTip = 'Specifies the value of the field', comment = 'ESP="Especifica el valor del campo"';
                ApplicationArea = All;
            }

“Use for Email Body” y “Use for Email Attachment”: Estos campos permiten configurar si el informe se utilizará como cuerpo o como adjunto en el correo electrónico.

“Language Code”: Permite especificar el idioma en el que se enviará el correo, asegurando que se adapte al destinatario.

            field("ReportLayoutCaption"; Rec."Report Layout Caption")
            {
                ToolTip = 'Specifies the Name of the report layout that is used.', comment = 'ESP="Especifica el nombre del diseño del informe que se utiliza."';
                ApplicationArea = All;

                trigger OnDrillDown()
                begin
                    Rec.DrillDownToSelectLayout(Rec."Report Layout Name", Rec."Report Layout AppID");
                    CurrPage.Update(true);
                end;
            }
            field("Subject Layout Descr."; Rec."Subject Layout Descr.")
            {
                ToolTip = 'Specifies the value of the field', comment = 'ESP="Especifica el valor del campo"';
                ApplicationArea = All;

                trigger OnDrillDown()
                var
                    CustomReportLayout: Record "Custom Report Layout";
                begin
                    if CustomReportLayout.LookupLayoutOK(Rec."Report ID") then
                        Rec.Validate("Subject Layout Code", CustomReportLayout.Code);
                end;
            }

Finalmente, los campos "ReportLayoutCaption" y "Subject Layout Descr." proporcionan una descripción y un acceso rápido a la selección de diseños de informe y de asunto de correo. Esto facilita a los usuarios la visualización y selección del diseño adecuado directamente desde la página de selección de informes de almacén.

            field("Email Body Layout Description"; Rec."EmailBodyLayoutDescription")
            {
                ToolTip = 'Specifies the value of the field', comment = 'ESP="Especifica el valor del campo"';
                ApplicationArea = All;

                trigger OnDrillDown()
                var
                    CustomReportLayout: Record "Custom Report Layout";
                begin
                    if CustomReportLayout.LookupLayoutOK(Rec."Report ID") then
                        Rec.Validate("Email Body Layout Code", CustomReportLayout.Code);
                end;
            }
        }
    }
}

El campo "Email Body Layout Description" permite al usuario acceder y seleccionar el diseño del cuerpo del correo de manera similar, asegurando que la presentación del contenido del correo sea coherente y esté alineada con las expectativas del destinatario.

Con estas extensiones, la personalización y la automatización del envío de correos electrónicos en el contexto de almacenes en Business Central se vuelven más accesibles, permitiendo a los usuarios configurar y gestionar eficientemente sus comunicaciones electrónicas en múltiples idiomas y formatos.

Gestión Avanzada de Envío de Correos: Codeunit 60007 “Mgt. Send Mail”

La codeunit 60007 "Mgt. Send Mail" en Dynamics 365 Business Central es una pieza fundamental para gestionar el envío avanzado de correos electrónicos, permitiendo personalizar tanto el asunto, el cuerpo del mensaje como los adjuntos. A continuación, se detalla el código de esta codeunit, explicando cada una de las funciones y su utilidad en un entorno empresarial.

Funciones de Envío de Correos

Esta sección se enfoca en las funciones principales que manejan el envío de correos electrónicos en diferentes escenarios.

procedure SendSimpleEmail(RecVariant: Variant; NewToRecipients: Text; IsSendDirectly: Boolean; Body: Text; Subject: Text) EmailAction: Enum "Email Action";
begin
    Clear(EmailMessage);

    SetToRecipients(NewToRecipients);
    CreateMail(RecVariant, IsSendDirectly);
    SetBody(Body);
    SetSubject(Subject);
    EmailAction := SendEmail();
end;

SendSimpleEmail: Esta función se encarga de enviar un correo simple, donde se especifica directamente el cuerpo y el asunto del correo, así como los destinatarios. Es ideal para casos donde se necesita enviar un mensaje rápido y directo, sin adjuntos complejos ni plantillas.

procedure SendEmailReportSelection(RecVariant: Variant; NewReportSelectionUsage: Enum "Report Selection Usage"; IsSendDirectly: Boolean; FileName: Text[250]) EmailAction: Enum "Email Action";
begin
    // REPORT SELECTION
    Clear(EmailMessage);

    CreateMail(RecVariant, IsSendDirectly);
    SetReportSelectionUsage(NewReportSelectionUsage);
    AddSubjectReportSelections();
    AddBodyReportSelections();
    if SendSeparateAttachments then
        AddSeparateAttachments(RecVariant, OptionAttachment::ReportSelections)
    else
        AddAttachmentReportSelections(FileName);

    EmailAction := SendEmail();
end;

SendEmailReportSelection: Esta función permite enviar un correo que incluye un informe seleccionado. Aquí, se configura el uso de la selección del informe y se genera tanto el asunto como el cuerpo del correo a partir del informe seleccionado. Además, se gestionan los adjuntos, que pueden enviarse como archivos separados o combinados.

procedure SendEmailReportSelectionWarehouse(RecVariant: Variant; NewSelectionWarehouseUsage: Enum "Report Selection Warehouse Usage"; IsSendDirectly: Boolean; FileName: Text[250]) EmailAction: Enum "Email Action";
begin
    // REPORT SELECTION WAREHOUSE
    Clear(EmailMessage);

    CreateMail(RecVariant, IsSendDirectly);
    SetReportSelectionWarehouseUsage(NewSelectionWarehouseUsage);
    AddSubjectReportSelectionsWarehouse();
    AddBodyReportSelectionWarehouse();
    if SendSeparateAttachments then
        AddSeparateAttachments(RecVariant, OptionAttachment::ReportSelectionWarehouse)
    else
        AddAttachmentReportSelectionWarehouse(FileName);

    EmailAction := SendEmail();
end;

SendEmailReportSelectionWarehouse: Similar a la función anterior, pero enfocada en la gestión de informes de almacén. Esta función maneja el envío de correos que contienen informes relacionados con la logística de almacenes, asegurando que el contenido del correo refleje correctamente las operaciones del almacén.

procedure SendEmail() EmailAction: Enum "Email Action";
var
    IsSend: Boolean;
begin
    if RecipientsCC <> '' then
        AddRecipientsCC(RecipientsCC);

    if ToRecipients <> '' then
        AddRecipients(ToRecipients);

    if SendDirectly then begin
        IsSend := Email.Send(EmailMessage);

        EmailAction := EmailAction::Discarded;
        if IsSend then
            EmailAction := EmailAction::Sent;
    end else
        EmailAction := Email.OpenInEditorModally(EmailMessage);
end;

SendEmail: Esta función es responsable de enviar el correo. Puede enviarse directamente o abrirse en el editor de correos para su revisión antes de enviarlo. Además, se maneja el caso de agregar destinatarios en copia (CC) si se especifican.

Funciones para Crear Correos

Estas funciones se encargan de preparar el correo antes de su envío, configurando los aspectos básicos como el destinatario, el asunto, y el cuerpo del mensaje.

procedure CreateMail(RecVariant: Variant; IsSendDirectly: Boolean)
begin
    if not IsModifyGetTable then begin
        Clear(RecordRef);
        RecordRef.GetTable(RecVariant);
    end;

    CreateMail(IsSendDirectly);
end;

procedure CreateMail(IsSendDirectly: Boolean)
begin
    SendDirectly := IsSendDirectly;

    // Crear el correo base
    EmailMessage.Create(ToRecipients, SubjectTxt, BodyText, true);
end;

CreateMail: Existen dos sobrecargas de esta función. La primera toma un Variant como parámetro, lo convierte en una referencia de registro y luego llama a la segunda función, que crea el correo base con la información del destinatario, el asunto y el cuerpo. Esta función es crucial para inicializar el correo antes de añadir más detalles o enviar el mensaje.

procedure CreateFileName() ReturnValue: Text[250]
begin
    // Función para extraer nombre del fichero estándar
    ReturnValue := CopyStr(CreateTextFromRecord() + ' ' + GetDocumentNo(RecordRef), 1, 250);
end;

CreateFileName: Esta función genera un nombre de archivo estándar para los adjuntos del correo, combinando el texto generado a partir del registro y el número del documento asociado. Es útil para mantener consistencia en los nombres de los archivos adjuntos.

procedure CreateSubject() ReturnValue: Text[250]
begin
    // Función para crear asunto estándar
    ReturnValue := CreateTextFromRecord();
end;

CreateSubject: Similar a la función anterior, esta función genera un asunto estándar para el correo utilizando el texto asociado al registro actual. Esto asegura que el asunto del correo sea relevante y descriptivo en relación con el contenido del mensaje.

Funciones para Añadir Contenido al Correo

Estas funciones son responsables de construir el contenido del correo, incluyendo el asunto, el cuerpo, y los adjuntos.

procedure AddSubjectReportSelections() ReturnValue: Text;
var
    ReportSelections: Record "Report Selections";
    AuxSubjectTxt: Text;
    InitPosition: Integer;
    Length: Integer;
    Text001Lbl: Label ' / ', Locked = true;
begin
    if HideAddSubject then
        exit;

    if SubjectTxt <> '' then
        exit;

    Clear(AuxSubjectTxt);
    Clear(ReportLayoutSelection);

    ReportSelections.Reset();
    ReportSelections.SetRange(Usage, ReportSelectionUsage);
    ReportSelections.SetRange("Use for Email Subject", true);

    if LanguageCode <> '' then begin
        ReportSelections.SetRange("Language Code", LanguageCode);

        if ReportSelections.IsEmpty() then
            ReportSelections.SetRange("Language Code");
    end;

    if ReportSelections.FindFirst() then begin
        Clear(TempBlob);
        Clear(OutStream);
        Clear(InStream);

        ChangeGlobalLanguaje(LanguageCode);

        if ReportSelections."Subject Layout Code" <> '' then
            ReportLayoutSelection.SetTempLayoutSelected(ReportSelections."Subject Layout Code");

        TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8);
        REPORT.SaveAs(ReportSelections."Report ID", '', ReportFormat::Html, OutStream, RecordRef);
        TempBlob.CreateInStream(InStream, TextEncoding::UTF8);
        InStream.Read(AuxSubjectTxt);

        if ReportSelections."Subject Layout Code" <> '' then
            ReportLayoutSelection.SetTempLayoutSelected('');

        // Buscamos la posición inicial y el tamaño del texto
        InitPosition := StrPos(AuxSubjectTxt, '<span>') + 6;
        Length := StrPos(AuxSubjectTxt, '</span>');
        Length := Length - InitPosition;

        AuxSubjectTxt := CopyStr(AuxSubjectTxt, InitPosition, Length);
    end;

    if SubjectTxt <> '' then
        SubjectTxt += Text001Lbl;

    SubjectTxt += AuxSubjectTxt;

    if SubjectTxt = '' then
        SubjectTxt := CreateSubject();

    ReturnValue := SubjectTxt;

    if LanguageCode <> '' then
        GlobalLanguage(InitLanguageID);

    EmailMessage.SetSubject(SubjectTxt);
end;

AddSubjectReportSelections: Esta función construye el asunto del correo utilizando los informes seleccionados. Extrae el contenido del informe, lo convierte a HTML y lo analiza para obtener el texto del asunto, que luego se configura en el correo. Además, asegura que el asunto esté alineado con el idioma seleccionado.

procedure AddBodyReportSelections() ReturnValue: Text;
var
    ReportSelections: Record "Report Selections";
    AuxBodyText: Text;
    Text001Lbl: Label '<br> <br> <hr> <br> <br> <br> <br>', Locked = true;
begin
    if HideAddBody then
        exit;

    if BodyText <> '' then
        exit;

    Clear(AuxBodyText);
    Clear(ReportLayoutSelection);

    ReportSelections.Reset();
    ReportSelections.SetRange(Usage, ReportSelectionUsage);
    ReportSelections.SetRange("Use for Email Body", true);

    if LanguageCode <> '' then begin
        ReportSelections.SetRange("Language Code", LanguageCode);



        if ReportSelections.IsEmpty() then
            ReportSelections.SetRange("Language Code");
    end;

    if ReportSelections.FindFirst() then begin
        Clear(TempBlob);
        Clear(OutStream);
        Clear(InStream);

        ChangeGlobalLanguaje(LanguageCode);

        if ReportSelections."Email Body Layout Code" <> '' then
            ReportLayoutSelection.SetTempLayoutSelected(ReportSelections."Email Body Layout Code");

        TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8);
        REPORT.SaveAs(ReportSelections."Report ID", '', ReportFormat::Html, OutStream, RecordRef);
        TempBlob.CreateInStream(InStream, TextEncoding::UTF8);
        InStream.Read(AuxBodyText);

        if ReportSelections."Email Body Layout Code" <> '' then
            ReportLayoutSelection.SetTempLayoutSelected('');
    end;

    if BodyText <> '' then
        BodyText += Text001Lbl;

    BodyText += AuxBodyText;

    ReturnValue := BodyText;

    if LanguageCode <> '' then
        GlobalLanguage(InitLanguageID);

    EmailMessage.SetBody(BodyText);
end;

AddBodyReportSelections: Esta función construye el cuerpo del correo electrónico utilizando el contenido de los informes seleccionados. El cuerpo del correo se genera en HTML y puede incluir elementos como encabezados y separadores para mejorar la presentación. Es especialmente útil cuando se envían correos que requieren una estructura de contenido más compleja.

procedure AddAttachmentReportSelections(FileName: Text[250])
var
    ReportSelections: Record "Report Selections";
    VersionFileName: Integer;
begin
    if HideAddAttachment then
        exit;

    VersionFileName := 0;

    if FileName = '' then
        FileName := CreateFileName();

    ChangeGlobalLanguaje(LanguageCode);

    ReportSelections.Reset();
    ReportSelections.SetRange(Usage, ReportSelectionUsage);
    ReportSelections.SetRange("Use for Email Attachment", true);
    if LanguageCode <> '' then begin
        ReportSelections.SetRange("Language Code", LanguageCode);

        if ReportSelections.IsEmpty() then
            ReportSelections.SetRange("Language Code");
    end;

    if ReportSelections.FindSet() then
        repeat
            SelectAttachmentFormat(ReportSelections."Report ID");

            Clear(TempBlob);
            Clear(OutStream);
            Clear(InStream);

            VersionFileName += 1;

            if VersionFileName > 1 then
                FileName += '_' + Format(VersionFileName);

            FileName += '.' + format(ContentTypeAddAttachment);

            TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8);
            REPORT.SaveAs(ReportSelections."Report ID", '', AttachmentFormat, OutStream, RecordRef);
            TempBlob.CreateInStream(InStream, TextEncoding::UTF8);
            EmailMessage.AddAttachment(FileName, ContentTypeAddAttachment, InStream);
        until ReportSelections.Next() = 0;

    if LanguageCode <> '' then
        GlobalLanguage(InitLanguageID);
end;

AddAttachmentReportSelections: Esta función añade los adjuntos generados a partir de los informes seleccionados. Gestiona el formato del adjunto (PDF, Excel, etc.), así como el nombre del archivo. Además, asegura que todos los adjuntos sean correctamente generados y añadidos al correo, incluso si existen múltiples versiones del archivo.

Funciones de Adición para Almacenes

Estas funciones están especializadas en manejar la adición de contenido relacionado con los informes de almacén.

procedure AddSubjectReportSelectionsWarehouse() ReturnValue: Text;
var
    ReportSelectionWarehouse: Record "Report Selection Warehouse";
    AuxSubjectTxt: Text;
    InitPosition: Integer;
    Length: Integer;
    Text001Lbl: Label ' / ', Locked = true;
begin
    if HideAddSubject then
        exit;

    if SubjectTxt <> '' then
        exit;

    Clear(AuxSubjectTxt);
    Clear(ReportLayoutSelection);

    ReportSelectionWarehouse.Reset();
    ReportSelectionWarehouse.SetRange(Usage, ReportSelectionUsage);
    ReportSelectionWarehouse.SetRange("Use for Email Subject", true);

    if LanguageCode <> '' then begin
        ReportSelectionWarehouse.SetRange("Language Code", LanguageCode);

        if ReportSelectionWarehouse.IsEmpty() then
            ReportSelectionWarehouse.SetRange("Language Code");
    end;

    if ReportSelectionWarehouse.FindFirst() then begin
        Clear(TempBlob);
        Clear(OutStream);
        Clear(InStream);

        ChangeGlobalLanguaje(LanguageCode);

        if ReportSelectionWarehouse."Subject Layout Code" <> '' then
            ReportLayoutSelection.SetTempLayoutSelected(ReportSelectionWarehouse."Subject Layout Code");

        TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8);
        REPORT.SaveAs(ReportSelectionWarehouse."Report ID", '', ReportFormat::Html, OutStream, RecordRef);
        TempBlob.CreateInStream(InStream, TextEncoding::UTF8);
        InStream.Read(AuxSubjectTxt);

        if ReportSelectionWarehouse."Subject Layout Code" <> '' then
            ReportLayoutSelection.SetTempLayoutSelected('');

        // Buscamos la posición inicial y el tamaño del texto
        InitPosition := StrPos(AuxSubjectTxt, '<span>') + 6;
        Length := StrPos(AuxSubjectTxt, '</span>');
        Length := Length - InitPosition;

        AuxSubjectTxt := CopyStr(AuxSubjectTxt, InitPosition, Length);
    end;

    if SubjectTxt <> '' then
        SubjectTxt += Text001Lbl;

    SubjectTxt += AuxSubjectTxt;

    if SubjectTxt = '' then
        SubjectTxt := CreateSubject();

    ReturnValue := SubjectTxt;

    if LanguageCode <> '' then
        GlobalLanguage(InitLanguageID);

    EmailMessage.SetSubject(SubjectTxt);
end;

AddSubjectReportSelectionsWarehouse: Similar a la función de selección de informes, esta función se enfoca en construir el asunto del correo a partir de informes relacionados con el almacén. Asegura que el asunto del correo refleje correctamente las operaciones del almacén, considerando el idioma y el formato del informe.

procedure AddBodyReportSelectionWarehouse() ReturnValue: Text;
var
    ReportSelectionWarehouse: Record "Report Selection Warehouse";
    AuxBodyText: Text;
    Text001Lbl: Label '<br> <br> <hr> <br> <br> <br> <br>', Locked = true;
begin
    if HideAddBody then
        exit;

    if BodyText <> '' then
        exit;

    Clear(AuxBodyText);
    Clear(ReportLayoutSelection);

    ReportSelectionWarehouse.Reset();
    ReportSelectionWarehouse.SetRange(Usage, SelectionWarehouseUsage);

    if LanguageCode <> '' then begin
        ReportSelectionWarehouse.SetRange("Language Code", LanguageCode);

        if ReportSelectionWarehouse.IsEmpty() then
            ReportSelectionWarehouse.SetRange("Language Code");
    end;

    if ReportSelectionWarehouse.FindFirst() then begin
        Clear(TempBlob);
        Clear(OutStream);
        Clear(InStream);

        ChangeGlobalLanguaje(LanguageCode);

        if ReportSelectionWarehouse."Email Body Layout Code" <> '' then
            ReportLayoutSelection.SetTempLayoutSelected(ReportSelectionWarehouse."Email Body Layout Code");

        TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8);
        REPORT.SaveAs(ReportSelectionWarehouse."Report ID", '', ReportFormat::Html, OutStream, RecordRef);
        TempBlob.CreateInStream(InStream, TextEncoding::UTF8);
        InStream.Read(AuxBodyText);

        if ReportSelectionWarehouse."Email Body Layout Code" <> '' then
            ReportLayoutSelection.SetTempLayoutSelected('');
    end;

    if BodyText <> '' then
        BodyText += Text001Lbl;

    BodyText += AuxBodyText;

    ReturnValue := BodyText;

    if LanguageCode <> '' then
        GlobalLanguage(InitLanguageID);

    EmailMessage.SetBody(BodyText);
end;

AddBodyReportSelectionWarehouse: Esta función genera el cuerpo del correo electrónico a partir de informes de almacén. Utiliza el contenido de los informes para crear un cuerpo de mensaje detallado, que puede incluir múltiples secciones y separadores. Es esencial para garantizar que el correo refleje con precisión la información logística y operativa del almacén.

procedure AddAttachmentReportSelectionWarehouse(FileName: Text[250])
var
    ReportSelectionWarehouse: Record "Report Selection Warehouse";
    VersionFileName: Integer;
begin
    if HideAddAttachment then
        exit;

    VersionFileName := 0;

    if FileName = '' then
        FileName := CreateFileName();

    ChangeGlobalLanguaje(LanguageCode);

    ReportSelectionWarehouse.Reset();
    ReportSelectionWarehouse.SetRange(Usage, SelectionWarehouseUsage);

    if LanguageCode <> '' then begin
        ReportSelectionWarehouse.SetRange("Language Code", LanguageCode);

        if ReportSelectionWarehouse.IsEmpty() then
            ReportSelectionWarehouse.SetRange("Language Code");
    end;

    if ReportSelectionWarehouse.FindSet() then
        repeat
            SelectAttachmentFormat(ReportSelectionWarehouse."Report ID");

            Clear(TempBlob);
            Clear(OutStream);
            Clear(InStream);

            VersionFileName += 1;

            if VersionFileName > 1 then
                FileName += '_' + Format(VersionFileName);

            FileName += '.' + format(ContentTypeAddAttachment);

            TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8);
            REPORT.SaveAs(ReportSelectionWarehouse."Report ID", '', AttachmentFormat, OutStream, RecordRef);
            TempBlob.CreateInStream(InStream, TextEncoding::UTF8);
            EmailMessage.AddAttachment(FileName, ContentTypeAddAttachment, InStream);


 until ReportSelectionWarehouse.Next() = 0;

    if LanguageCode <> '' then
        GlobalLanguage(InitLanguageID);
end;

AddAttachmentReportSelectionWarehouse: Similar a su contraparte en la selección de informes generales, esta función se especializa en añadir adjuntos derivados de informes de almacén al correo. Maneja múltiples versiones de archivos y asegura que los adjuntos sean correctamente formateados y nombrados.

Funciones Varias

Estas funciones adicionales proporcionan soporte para operaciones específicas como la adición de destinatarios y la gestión de parámetros.

procedure AddRecipientsCC(NewRecipientsCC: Text)
begin
    // Adjunta los correos en copia
    RecipientsCC := NewRecipientsCC;
    EmailMessage.AddRecipient(Enum::"Email Recipient Type"::Cc, NewRecipientsCC);
end;

procedure AddRecipients(NewToRecipients: Text)
begin
    // Adjunta los correos de envío
    ToRecipients := NewToRecipients;
    EmailMessage.AddRecipient(Enum::"Email Recipient Type"::"To", NewToRecipients);
end;

procedure AddAttachment(NewInStream: InStream; NewFileName: Text[250]; NewContentType: Text[250])
begin
    EmailMessage.AddAttachment(NewFileName, NewContentType, NewInStream);
end;

AddRecipientsCC y AddRecipients: Estas funciones permiten añadir destinatarios en copia (CC) y principales (To) al correo. Son esenciales para gestionar la entrega de correos a múltiples destinatarios, asegurando que todos los involucrados reciban la información necesaria.

AddAttachment: Añade un archivo adjunto al correo utilizando un flujo de entrada (InStream). Es útil cuando se necesita adjuntar archivos que no están relacionados con los informes, como documentos externos o archivos generados dinámicamente.

Funciones de Configuración

Estas funciones permiten establecer parámetros clave para el envío de correos, como destinatarios, formatos de adjunto, y preferencias de idioma.

procedure SetToRecipients(NewRecipients: Text)
begin
    ToRecipients := NewRecipients;
end;

procedure SetRecipientsCC(NewRecipientsCC: Text)
begin
    RecipientsCC := NewRecipientsCC;
end;

procedure SetSubject(NewSubject: Text)
begin
    SubjectTxt := NewSubject;
    EmailMessage.SetSubject(SubjectTxt);
end;

procedure SetBody(NewBody: Text)
begin
    BodyText := NewBody;
    EmailMessage.SetBody(BodyText);
end;

procedure SetRecord(RecVariant: Variant)
begin
    Clear(RecordRef);
    RecordRef.GetTable(RecVariant);
    IsModifyGetTable := true;
end;

procedure SetReportSelectionUsage(NewReportSelectionUsage: Enum "Report Selection Usage")
begin
    ReportSelectionUsage := NewReportSelectionUsage;
end;

procedure SetReportSelectionWarehouseUsage(NewReportSelectionWarehouseUsage: Enum "Report Selection Warehouse Usage")
begin
    SelectionWarehouseUsage := NewReportSelectionWarehouseUsage;
end;

procedure SetLanguage(NewLanguageCode: Code[10])
begin
    LanguageCode := NewLanguageCode;
end;

procedure SetHideAddAttachment(NewHideAddAttachment: Boolean)
begin
    HideAddAttachment := NewHideAddAttachment;
end;

procedure SetHideAddBody(NewHideAddBody: Boolean)
begin
    HideAddBody := NewHideAddBody;
end;

procedure SetHideAddSubject(NewHideAddSubject: Boolean)
begin
    HideAddSubject := NewHideAddSubject;
end;

procedure SetAttachmentFormat(NewAttachmentFormat: ReportFormat)
begin
    AttachmentFormat := NewAttachmentFormat;
    IsModifyAttachmentFormat := true;
end;

procedure SetSendSeparateAttachments()
begin
    SendSeparateAttachments := true;
end;

Funciones de Configuración (SetToRecipients, SetRecipientsCC, SetSubject, SetBody, etc.): Estas funciones configuran los parámetros del correo, como los destinatarios, el asunto, el cuerpo del mensaje, y las opciones de adjuntos. Son fundamentales para personalizar el envío de correos en función de las necesidades específicas de cada operación o informe.

Funciones Locales

Las funciones locales son utilizadas internamente dentro de la codeunit para realizar operaciones específicas, como cambiar el idioma global o seleccionar el formato de los adjuntos.

local procedure ChangeGlobalLanguaje(NewLanguageCode: Code[10])
var
    Language: Record Language;
    LanguageID: Integer;
begin
    if NewLanguageCode = '' then
        exit;

    InitLanguageID := GlobalLanguage();

    Language.Reset();
    Language.SetRange(Code, NewLanguageCode);
    Language.FindFirst();

    LanguageID := Language."Windows Language ID";
    if LanguageID = 1034 then
        LanguageID := 3082;

    GlobalLanguage(LanguageID);
end;

ChangeGlobalLanguaje: Esta función cambia el idioma global de la sesión para asegurarse de que los informes y correos se generen en el idioma correcto. Esto es crucial en entornos multilingües donde los documentos deben ser producidos en diferentes idiomas según el destinatario.

local procedure CreateTextFromRecord() ReturnValue: Text[250]
var
    ReportDistributionManagement: Codeunit "Report Distribution Management";
begin
    ChangeGlobalLanguaje(LanguageCode);

    Clear(ReportDistributionManagement);

    Clear(ReportDistributionManagement);
    ReturnValue := ReportDistributionManagement.GetFullDocumentTypeText(RecordRef);

    if ReturnValue = '' then
        ReturnValue := CopyStr(RecordRef.Caption(), 1, 250);

    if LanguageCode <> '' then
        GlobalLanguage(InitLanguageID);
end;

CreateTextFromRecord: Esta función genera un texto descriptivo a partir de un registro, que puede ser utilizado como parte del asunto o nombre de archivo del correo. Es útil para generar descripciones automáticas basadas en los datos de un registro.

local procedure SelectAttachmentFormat(ReportID: Integer)
begin
    if not IsModifyAttachmentFormat then
        AttachmentFormat := SearchReportFormat(ReportID);

    case AttachmentFormat of
        AttachmentFormat::Pdf:
            ContentTypeAddAttachment := 'pdf';
        AttachmentFormat::Excel:
            ContentTypeAddAttachment := 'xlsx';
        AttachmentFormat::Html:
            ContentTypeAddAttachment := 'html';
        AttachmentFormat::Word:
            ContentTypeAddAttachment := 'docx';
        AttachmentFormat::Xml:
            ContentTypeAddAttachment := 'xml';
    end;
end;

SelectAttachmentFormat: Esta función selecciona el formato del archivo adjunto basándose en el ID del informe. Asegura que los archivos adjuntos sean generados en el formato correcto según las necesidades del informe y las preferencias del usuario.

local procedure SearchReportFormat(ReportID: Integer) ReturnValue: ReportFormat
var
    ReportLayoutList: Record "Report Layout List";
    DefaultReportLayoutList: Record "Report Layout List";
    IsDefaultLayout: Boolean;
    ValueMimeType: Text;
    Pos: Integer;
begin
    ReturnValue := ReturnValue::Pdf;

    ReportLayoutList.Reset();
    ReportLayoutList.SetRange("Report ID", ReportID);
    if ReportLayoutList.FindSet() then
        repeat
            if DefaultReportLayoutList."Report ID" <> ReportLayoutList."Report ID" then
                GetDefaultReportLayoutSelection(ReportLayoutList."Report ID", DefaultReportLayoutList);

            IsDefaultLayout := (DefaultReportLayoutList."Report ID" = ReportLayoutList."Report ID") and (DefaultReportLayoutList.Name = ReportLayoutList.Name) and (DefaultReportLayoutList."Application ID" = ReportLayoutList."Application ID");

            if IsDefaultLayout then begin
                case ReportLayoutList."Layout Format" of
                    ReportLayoutList."Layout Format"::Excel:
                        ReturnValue := ReturnValue::Excel;
                    ReportLayoutList."Layout Format"::RDLC:
                        ReturnValue := ReturnValue::Pdf;
                    ReportLayoutList."Layout Format"::Word:
                        ReturnValue := ReturnValue::Word;
                    else begin
                        ValueMimeType := ReportLayoutList."MIME Type";
                        Pos := StrPos(ValueMimeType, '/');
                        ValueMimeType := CopyStr(ValueMimeType, Pos, StrLen(ValueMimeType));

                        case ValueMimeType of
                            'docx':
                                ReturnValue := ReturnValue::Word;
                            'xlsx':
                                ReturnValue := ReturnValue::Excel;
                        end;
                    end;
                end;
                exit;
            end;
        until ReportLayoutList.Next() = 0;
end;

SearchReportFormat: Esta función busca y selecciona el formato de informe predeterminado para un ID de informe específico. Es esencial para garantizar que los informes sean generados y adjuntados en el formato correcto.

local procedure GetDefaultReportLayoutSelection(ReportId: Integer; var DefaultReportLayoutList: Record "Report Layout List"): Boolean
var
    ReportMetadata: Record "Report Metadata";
    TenantReportLayoutSelection: Record "Tenant Report Layout Selection";
    EmptyGuid: Guid;
begin
    TenantReportLayoutSelection.Init();
    DefaultReportLayoutList.Init();

    if TenantReportLayoutSelection.Get(ReportId, CompanyName(), EmptyGuid) then begin
        DefaultReportLayoutList.SetRange("Name", TenantReportLayoutSelection."Layout Name");
        DefaultReportLayoutList.SetRange("Application ID", TenantReportLayoutSelection."App ID");
        DefaultReportLayoutList.SetRange("Report ID", ReportId);

        if DefaultReportLayoutList.FindFirst() then
            exit(true);
    end else
        if ReportMetadata.Get(ReportId) then begin
            DefaultReportLayoutList.SetRange("Name", ReportMetadata."DefaultLayoutName");
            Default

ReportLayoutList.SetFilter("Application ID", '<>%1', EmptyGuid);
            DefaultReportLayoutList.SetRange("Report ID", ReportId);

            if DefaultReportLayoutList.FindFirst() then
                exit(true);
        end;

    exit(false);
end;

GetDefaultReportLayoutSelection: Esta función obtiene la selección predeterminada del diseño del informe para un ID de informe específico. Es crucial para identificar y aplicar el diseño correcto del informe al generarlo o enviarlo como adjunto.

local procedure GetDocumentNo(var DocumentRecordRef: RecordRef) ReturnValue: Code[20]
var
    SalesInvoiceHeader: Record "Sales Invoice Header";
    SalesCrMemoHeader: Record "Sales Cr.Memo Header";
    SalesHeader: Record "Sales Header";
    ServiceInvoiceHeader: Record "Service Invoice Header";
    ServiceCrMemoHeader: Record "Service Cr.Memo Header";
    ServiceHeader: Record "Service Header";
    Job: Record Job;
    SEPADirectDebitMandate: Record "SEPA Direct Debit Mandate";
    DocumentVariant: Variant;
begin
    Clear(ReturnValue);

    if DocumentRecordRef.Count = 1 then
        DocumentRecordRef.FindFirst()
    else
        exit;

    DocumentVariant := DocumentRecordRef;
    case DocumentRecordRef.Number of
        DATABASE::"Sales Invoice Header":
            begin
                SalesInvoiceHeader := DocumentVariant;
                ReturnValue := SalesInvoiceHeader."No.";
            end;
        DATABASE::"Sales Cr.Memo Header":
            begin
                SalesCrMemoHeader := DocumentVariant;
                ReturnValue := SalesCrMemoHeader."No.";
            end;
        DATABASE::"Service Invoice Header":
            begin
                ServiceInvoiceHeader := DocumentVariant;
                ReturnValue := ServiceInvoiceHeader."No.";
            end;
        DATABASE::"Service Cr.Memo Header":
            begin
                ServiceCrMemoHeader := DocumentVariant;
                ReturnValue := ServiceCrMemoHeader."No.";
            end;
        DATABASE::"Service Header":
            begin
                ServiceHeader := DocumentVariant;
                ReturnValue := ServiceHeader."No.";
            end;
        DATABASE::"Sales Header":
            begin
                SalesHeader := DocumentVariant;
                ReturnValue := SalesHeader."No.";
            end;
        DATABASE::Job:
            begin
                Job := DocumentVariant;
                ReturnValue := Job."No.";
            end;
        Database::"SEPA Direct Debit Mandate":
            begin
                SEPADirectDebitMandate := DocumentVariant;
                ReturnValue := format(SEPADirectDebitMandate.ID);
            end;
    end;
end;

GetDocumentNo: Esta función recupera el número de documento asociado a un registro específico. Es esencial para nombrar archivos adjuntos o para incluir referencias precisas en el correo electrónico.

local procedure AddSeparateAttachments(RecVariant: Variant; IntOptionAttachment: Integer)
var
    AllDocRecordRef: RecordRef;
begin
    Clear(AllDocRecordRef);
    AllDocRecordRef.GetTable(RecVariant);

    if AllDocRecordRef.FindSet() then
        repeat
            Clear(RecordRef);
            RecordRef.Open(AllDocRecordRef.Number);
            RecordRef.SetPosition(AllDocRecordRef.GetPosition());
            RecordRef.SetRecFilter();
            case IntOptionAttachment of
                OptionAttachment::ReportSelections:
                    AddAttachmentReportSelections('');
                OptionAttachment::ReportSelectionWarehouse:
                    AddAttachmentReportSelectionWarehouse('');
            end;
        until AllDocRecordRef.Next() = 0;
end;

AddSeparateAttachments: Esta función añade varios adjuntos al correo basándose en el registro de referencia y la opción de adjunto especificada. Es útil para enviar múltiples documentos relacionados en un solo correo electrónico, garantizando que cada uno se adjunte correctamente.

Eventos

Finalmente, la codeunit 60007 "Mgt. Send Mail" incluye una serie de eventos que permiten modificar o complementar el comportamiento estándar de Business Central en el envío de correos electrónicos. Estos eventos son cruciales para personalizar el proceso de generación y envío de informes, asegurando que solo se procesen los elementos deseados y evitando errores durante la ejecución.

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Report Distribution Management", OnAfterGetFullDocumentTypeText, '', false, false)]
local procedure C542_OnAfterGetFullDocumentTypeText(DocumentVariant: Variant; var DocumentTypeText: Text[50]; var DocumentRecordRef: RecordRef)
var
    SEPAMandateTxt: Label 'SEPA Mandate', Comment = 'ESP="Mandato SEPA"';
begin
    // Evento para asignar texto a los documentos no configurados
    case DocumentRecordRef.Number of
        Database::"SEPA Direct Debit Mandate":
            DocumentTypeText := SEPAMandateTxt;
    end;
end;

C542_OnAfterGetFullDocumentTypeText: Este evento se activa después de obtener el texto completo del tipo de documento. Es útil para asignar descripciones específicas a documentos que no están configurados previamente en el sistema, como los mandatos SEPA.

[EventSubscriber(ObjectType::Table, Database::"Report Selections", OnBeforeCheckEmailBodyUsage, '', false, false)]
local procedure T77_OnBeforeCheckEmailBodyUsage(var IsHandled: Boolean)
begin
    // Evita errores al verificar múltiples cuerpos de correo
    IsHandled := true;
end;

T77_OnBeforeCheckEmailBodyUsage: Este evento se utiliza para evitar errores cuando se verifican múltiples cuerpos de correo electrónico. Permite una mayor flexibilidad al gestionar correos electrónicos con cuerpos personalizados en Business Central.

[EventSubscriber(ObjectType::Table, Database::"Report Selections", OnPrintDocumentsOnAfterSelectTempReportSelectionsToPrint, '', false, false)]
local procedure T77_OnPrintDocumentsOnAfterSelectTempReportSelectionsToPrint(RecordVariant: Variant; var TempReportSelections: Record "Report Selections" temporary; var TempNameValueBuffer: Record "Name/Value Buffer" temporary; var WithCheck: Boolean; ReportUsage: Integer; TableNo: Integer)
begin
    // Evita la impresión de reportes marcados como "solo correo"
    TempReportSelections.SetRange("Mail Only Option", false);
end;

T77_OnPrintDocumentsOnAfterSelectTempReportSelectionsToPrint: Este evento garantiza que los reportes marcados como “solo correo” no se impriman, asegurando que solo los documentos relevantes sean procesados.

[EventSubscriber(ObjectType::Table, Database::"Report Selections", OnSaveAsDocumentAttachmentOnBeforeCanSaveReportAsPDF, '', false, false)]
local procedure T77_OnSaveAsDocumentAttachmentOnBeforeCanSaveReportAsPDF(var TempAttachReportSelections: Record "Report Selections" temporary; RecRef: RecordRef; DocumentNo: Code[20]; AccountNo: Code[20]; NumberOfReportsAttached: Integer)
begin
    // Evita que se guarden reportes como PDF si están marcados como "solo correo"
    TempAttachReportSelections.SetRange("Mail Only Option", false);
end;

T77_OnSaveAsDocumentAttachmentOnBeforeCanSaveReportAsPDF: Similar al evento anterior, este evento impide que los reportes marcados como “solo correo” se guarden como archivos PDF.

[EventSubscriber(ObjectType::Table, Database::"Report Selections", OnSendEmailDirectlyOnBeforeSendFiles, '', false, false)]
local procedure T77_OnSendEmailDirectlyOnBeforeSendFiles(ReportUsage: Integer; RecordVariant: Variant; var DefaultEmailAddress: Text[250]; var TempAttachReportSelections: Record "Report Selections" temporary; var CustomReportSelection: Record "Custom Report Selection")
begin
    // Filtra reportes antes de enviar archivos por correo, excluyendo aquellos marcados como "solo correo"
    TempAttachReportSelections.SetRange("Mail Only Option", false);
end;

T77_OnSendEmailDirectlyOnBeforeSendFiles: Este evento permite filtrar los reportes antes de enviar los archivos por correo electrónico, excluyendo aquellos que están marcados como “solo correo”.

[EventSubscriber(ObjectType::Table, Database::"Report Selections", OnSendToDiskForCustOnBeforeFindReportUsage, '', false, false)]
local procedure T77_OnSendToDiskForCustOnBeforeFindReportUsage(var ReportSelectionsOrg: Record "Report Selections"; ReportUsage: Enum "Report Selection Usage"; RecordVariant: Variant; CustNo: Code[20]; var ReportSelectionsPart: Record "Report Selections"; var IsHandled: Boolean)
begin
    // Filtra reportes antes de guardarlos en disco, excluyendo aquellos marcados como "solo correo"
    ReportSelectionsOrg.SetRange("Mail Only Option", false);
end;

T77_OnSendToDiskForCustOnBeforeFindReportUsage: Similar al evento anterior, este evento asegura que solo los reportes relevantes sean guardados en disco, excluyendo aquellos marcados como “solo correo”.

[EventSubscriber(ObjectType::Table, Database::"Report Selections", OnSendToZipForCustOnBeforeFindReportUsageForCust, '', false, false)]
local procedure T77_OnSendToZipForCustOnBeforeFindReportUsageForCust(var ReportSelectionsOrg: Record "Report Selections"; ReportUsage: Enum "Report Selection Usage"; RecordVariant: Variant; CustNo: Code[20]; var ReportSelectionsPart: Record "Report Selections"; var IsHandled: Boolean)
begin
    // Filtra reportes antes de comprimirlos en un

 archivo ZIP, excluyendo aquellos marcados como "solo correo"
    ReportSelectionsOrg.SetRange("Mail Only Option", false);
end;

T77_OnSendToZipForCustOnBeforeFindReportUsageForCust: Este evento es similar al anterior, pero se aplica cuando los reportes se comprimen en un archivo ZIP antes de su envío.

[EventSubscriber(ObjectType::Table, Database::"Report Selections", OnFindReportSelections, '', false, false)]
local procedure T77_OnFindReportSelections(var FilterReportSelections: Record "Report Selections"; var IsHandled: Boolean; var ReturnReportSelections: Record "Report Selections"; AccountNo: Code[20]; TableNo: Integer)
begin
    // Filtra las selecciones de reportes, excluyendo aquellos marcados como "solo correo"
    FilterReportSelections.SetRange("Mail Only Option", false);
end;

T77_OnFindReportSelections: Este evento filtra las selecciones de reportes para asegurarse de que solo se procesen aquellos que no están marcados como “solo correo”.


Beneficios y Aplicaciones Prácticas

Este desarrollo avanzado para la gestión del envío de correos en Business Central ofrece múltiples beneficios y aplicaciones prácticas en entornos empresariales. Al integrar funcionalidades como la selección de plantillas de correo personalizadas, la gestión de adjuntos en diferentes idiomas y la posibilidad de configurar tanto el asunto como el cuerpo del correo electrónico, las empresas pueden optimizar su comunicación digital con clientes, proveedores y socios.

Un ejemplo práctico de su implementación sería en una empresa multinacional que maneja clientes en varios países. Con este sistema, los correos electrónicos pueden ser enviados en el idioma preferido de cada cliente, utilizando plantillas específicas que incluyen detalles como informes personalizados y documentos adjuntos relevantes, todo configurado directamente desde Business Central. Además, la capacidad de seleccionar diferentes formatos de archivo para los adjuntos permite adaptarse a las necesidades específicas de cada situación, garantizando que la información se entregue de manera eficiente y profesional.

Otro beneficio clave es la posibilidad de automatizar estos procesos, reduciendo errores y ahorrando tiempo al personal. La flexibilidad del sistema también permite su personalización según las necesidades de la empresa, haciendo que sea una herramienta poderosa para mejorar la comunicación y el flujo de trabajo.

Conclusión

En resumen, este desarrollo de funcionalidades avanzadas para el envío de correos en Business Central no solo mejora la eficiencia en la gestión de correos electrónicos, sino que también ofrece una personalización sin precedentes para adaptarse a las necesidades específicas de cada empresa. La integración de plantillas multilingües, la gestión de adjuntos y la personalización de los mensajes hacen de este sistema una herramienta esencial para cualquier organización que busque mejorar su comunicación digital.

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

¡Espero que esta guía te haya sido útil! Hasta la próxima 😊.

Share your love

One comment

Leave a Reply

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