Skip to main content

Archiving documents from other extensions

Overview

Freeze archives documents automatically at fixed points in Business Central: when a document is posted, when a document is deleted, and when an email is sent through the BC email module. Many customers also use other extensions that print documents, send them by email or issue them as e-invoices. These extensions often use their own mechanisms, which Freeze does not pick up automatically.

This page shows partners how to archive such documents in Freeze anyway. Often a setting in Freeze is all it takes. If not, you write a small extension of your own that hands the documents over to Freeze. Freeze itself never needs to change.

The idea is simple: every archiving operation in Freeze starts with an entry in the Freeze Queue Entries. The Freeze job then takes care of the upload to the archive, retries and verification. Your extension only has to create such an entry.

Check first: is additional archiving needed?

If Archive after Posting and Archive Outgoing are enabled in the Freeze Setup, Freeze already archives a PDF of the document when it is posted. Freeze creates this PDF from the report selection.

So first clarify with the customer what exactly is missing. Typical gaps are:

  • The document looks different from the Freeze PDF, for example because of its own layout or letterhead.
  • The email text or additional attachments of the sent email are missing.
  • The documents are not posted at all, for example reminders, statements or quotes.

The simplest option: attachments of the document

Many extensions add the issued document as an attachment to the document itself, for example to the order or to the posted invoice. Technically this is a record in the Document Attachment table. Users see it among the attachments of the document. In this case you do not need an extension of your own.

Turn on Archive Attachments in the Freeze Setup. Freeze then includes all attachments of a document whenever it archives that document. This happens:

  • at posting, if Archive after Posting is enabled for purchases or sales,
  • when an unposted purchase or sales document is deleted, if Archive on Delete is enabled,
  • when a user runs the Freeze now action on the document,
  • when a user runs the Freeze new record action in the archive search.

This applies to purchase and sales documents, both open and posted (for example invoices, credit memos, shipments and returns), and to customers, vendors and items.

Timing is what matters. Freeze only includes the attachments that are already there at that moment. Attachments added later are not archived automatically.

The other extension attaches its document to …Result
… the order or invoice before it is postedBusiness Central moves the attachments to the posted document during posting, and Freeze archives them automatically along with it. Nothing else to do.
… the posted document, but only later, for example when it is sentFreeze does not archive the document automatically. Users can then run Freeze now on the posted document. Freeze creates a new version of the existing record, with the document PDF and all current attachments. If this has to happen automatically, use option 1 or 2 below.

How to test it: Post a test document to which the other extension has attached its document. Wait for the Freeze job, then open the archive search for the posted document. The document from the other extension must be listed among the attachments of the record.

Further options at a glance

If attachments are not enough, for example because the other extension does not attach its document at all or only after posting, these are your options:

OptionHow it worksA good fit when …
1. Archive when the document is issuedYour extension subscribes to an event of the other extension, for example "document sent", and passes the file to Freeze right away.the other extension offers a suitable event that gives access to the file and the document.
2. Archive later via the job queueA job queue entry regularly reads the log of the other extension and passes new entries to Freeze.the other extension has no suitable event but logs sent documents including the file. Also works for documents that were sent in the past.
3. Archive a different file at postingYour extension uses a Freeze event to replace the PDF that Freeze archives at posting.only the layout differs and the posting date is good enough.
4. Archive manuallyUsers drag the file into the Quick Freeze FactBox.only single cases are affected. See Quick Freeze.

Option 3 archives the state at posting, not the document that was actually sent later. If the sent document must be archived, use option 1 or 2.

Building blocks

All options need only a few public procedures from Freeze:

CodeunitProcedurePurpose
DXP FRZ Attachment Mgt.AddFileFromStream(var TempFrzAttachmentBuffer; var InStr; FileName; LinkedRecordRef)Puts one file into the buffer. Call it once per file.
DXP Freeze Queue Buffer Mgt.InsertFREEZEQueueEntryWithAttachments(LinkedRecordRef; var TempFrzAttachmentBuffer; FrzRecordType; ReferenceDate)Creates the queue entry with all files. From here on, Freeze takes over.
DXP Freeze Queue Buffer Mgt.InsertQueueBufferEntry(RecSystemID; RecTableNo; var TempFrzAttachmentBuffer; FrzRecordType; ReferenceDate) followed by GetEntryNo()Same as above. In addition, GetEntryNo() then returns the Entry No. of the new entry, for example for a log of your own (example 2).
DXP Freeze HelperGetFreezeRecordTypeFromReportSelectionUsage(ReportSelectionUsage)Determines the matching record type, exactly as Freeze does itself.
DXP Freeze Setup Mgt.IsModuleActivated(), ArchiveEmailAttachments()Checks that Freeze is active and that the customer has enabled archiving of sent documents.

Example 1: Archive when the document is issued

In this example the other extension publishes an event OnAfterDocumentSent. It provides the document, the report usage and the PDF. Replace the codeunit, event name and parameters with the event of the extension your customer uses.

codeunit 50100 "PTE Archive Sent Documents"
{
    // Placeholder: codeunit and event of the other extension
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Other Ext. Send Mgt.", 'OnAfterDocumentSent', '', false, false)]
    local procedure ArchiveInFreeze(DocumentRecordId: RecordId; ReportUsage: Enum "Report Selection Usage"; var PdfTempBlob: Codeunit "Temp Blob"; PdfFileName: Text)
    var
        TempFrzAttachmentBuffer: Record "DXP FRZ Attachment Buffer" temporary;
        FrzAttachmentMgt: Codeunit "DXP FRZ Attachment Mgt.";
        FrzQueueBufferMgt: Codeunit "DXP Freeze Queue Buffer Mgt.";
        FrzHelper: Codeunit "DXP Freeze Helper";
        FrzSetupMgt: Codeunit "DXP Freeze Setup Mgt.";
        LinkedRecRef: RecordRef;
        PdfInStr: InStream;
        DocumentNotFoundErr: Label 'The document %1 could not be found and was not archived in Freeze.', Comment = '%1 = Record ID';
    begin
        // 1. Respect the customer's setup
        if not FrzSetupMgt.IsModuleActivated() then
            exit;
        if not FrzSetupMgt.ArchiveEmailAttachments() then
            exit;

        // 2. Open the posted document, for example the sales invoice
        if not LinkedRecRef.Get(DocumentRecordId) then
            Error(DocumentNotFoundErr, Format(DocumentRecordId));

        // 3. Put the file into the buffer
        PdfTempBlob.CreateInStream(PdfInStr);
        FrzAttachmentMgt.AddFileFromStream(TempFrzAttachmentBuffer, PdfInStr, PdfFileName, LinkedRecRef);

        // 4. Create the queue entry: record type as Freeze does it, reference date = document date
        FrzQueueBufferMgt.InsertFREEZEQueueEntryWithAttachments(
            LinkedRecRef,
            TempFrzAttachmentBuffer,
            Format(FrzHelper.GetFreezeRecordTypeFromReportSelectionUsage(ReportUsage)),
            GetDocumentDate(LinkedRecRef));
    end;

    local procedure GetDocumentDate(LinkedRecRef: RecordRef): Date
    var
        SalesInvoiceHeader: Record "Sales Invoice Header";
        SalesCrMemoHeader: Record "Sales Cr.Memo Header";
    begin
        case LinkedRecRef.Number() of
            Database::"Sales Invoice Header":
                begin
                    LinkedRecRef.SetTable(SalesInvoiceHeader);
                    exit(SalesInvoiceHeader."Document Date");
                end;
            Database::"Sales Cr.Memo Header":
                begin
                    LinkedRecRef.SetTable(SalesCrMemoHeader);
                    exit(SalesCrMemoHeader."Document Date");
                end;
        end;
        exit(WorkDate());
    end;
}

Important notes:

  • To archive the email text as well, add it to the buffer as a second file named body.html. Freeze does the same for the BC email module.
  • Freeze saves the queue entry immediately with a Commit. If the event of the other extension fires in the middle of its own processing, this can interfere with it. In that case, use option 2.

Example 2: Archive later via the job queue

In this example the other extension keeps a log Other Ext. Sent Log. Each entry contains the document, the report usage and the PDF. Your extension uses a small table of its own to remember which entries have already been passed to Freeze.

Step 1: a table for entries that were already passed on.

table 50100 "PTE Freeze Transfer Log"
{
    DataClassification = SystemMetadata;

    fields
    {
        field(1; "Source Entry No."; Integer) { }
        field(2; "Freeze Queue Entry No."; Integer) { }
        field(3; "Transferred At"; DateTime) { }
    }

    keys
    {
        key(PK; "Source Entry No.") { Clustered = true; }
    }
}

Step 2: a codeunit that runs as a job queue entry.

codeunit 50101 "PTE Transfer Sent Log"
{
    trigger OnRun()
    var
        SentLog: Record "Other Ext. Sent Log"; // Placeholder: log table of the other extension
        TransferLog: Record "PTE Freeze Transfer Log";
        FrzSetupMgt: Codeunit "DXP Freeze Setup Mgt.";
    begin
        if not FrzSetupMgt.IsModuleActivated() then
            exit;
        if not FrzSetupMgt.ArchiveEmailAttachments() then
            exit;

        SentLog.SetRange(Status, SentLog.Status::Sent);
        if SentLog.FindSet() then
            repeat
                if not TransferLog.Get(SentLog."Entry No.") then
                    TransferEntry(SentLog);
            until SentLog.Next() = 0;
    end;

    local procedure TransferEntry(var SentLog: Record "Other Ext. Sent Log")
    var
        TempFrzAttachmentBuffer: Record "DXP FRZ Attachment Buffer" temporary;
        TransferLog: Record "PTE Freeze Transfer Log";
        FrzAttachmentMgt: Codeunit "DXP FRZ Attachment Mgt.";
        FrzQueueBufferMgt: Codeunit "DXP Freeze Queue Buffer Mgt.";
        FrzHelper: Codeunit "DXP Freeze Helper";
        LinkedRecRef: RecordRef;
        SystemIdFieldRef: FieldRef;
        PdfInStr: InStream;
        DocumentNotFoundErr: Label 'Log entry %1: the document %2 could not be found.', Comment = '%1 = Entry No., %2 = Record ID';
    begin
        if not LinkedRecRef.Get(SentLog."Document Record ID") then
            Error(DocumentNotFoundErr, SentLog."Entry No.", Format(SentLog."Document Record ID"));

        SentLog.CalcFields("PDF File");
        SentLog."PDF File".CreateInStream(PdfInStr);
        FrzAttachmentMgt.AddFileFromStream(TempFrzAttachmentBuffer, PdfInStr, SentLog."File Name", LinkedRecRef);

        // InsertQueueBufferEntry instead of InsertFREEZEQueueEntryWithAttachments,
        // so that GetEntryNo() returns the Entry No. of the new entry afterwards
        SystemIdFieldRef := LinkedRecRef.Field(LinkedRecRef.SystemIdNo());
        FrzQueueBufferMgt.InsertQueueBufferEntry(
            SystemIdFieldRef.Value(),
            LinkedRecRef.Number(),
            TempFrzAttachmentBuffer,
            Format(FrzHelper.GetFreezeRecordTypeFromReportSelectionUsage(SentLog."Report Usage")),
            SentLog."Document Date");

        TransferLog.Init();
        TransferLog."Source Entry No." := SentLog."Entry No.";
        TransferLog."Freeze Queue Entry No." := FrzQueueBufferMgt.GetEntryNo();
        TransferLog."Transferred At" := CurrentDateTime();
        TransferLog.Insert();
        Commit();
    end;
}

Important notes:

  • If an entry cannot be passed on, the run stops with an error message. The error is shown on the job queue entry, and the log entry is retried on the next run. That way no document gets lost unnoticed.
  • This only works if the other extension stores the file in the database. If the file only lives in external storage, you have to load it from there.
  • Tables of other extensions are not a guaranteed interface. Check your code after updates of the other extension.

Example 3: Archive a different file at posting

At posting, Freeze raises the event OnBeforeSaveReportAsPDFInTempBlob for each line of the report selection before it creates the PDF. Your extension can supply its own PDF here.

codeunit 50102 "PTE Freeze Posting PDF"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP FRZ Attachment Mgt.", 'OnBeforeSaveReportAsPDFInTempBlob', '', false, false)]
    local procedure UseOtherPdf(var ReportSelectionUsage: Enum "Report Selection Usage"; var ReportID: Integer; var TempBlobList: Codeunit "Temp Blob List"; var RecVarToPrint: Variant; var CustNoOrBillToNo: Code[20]; var IsHandled: Boolean)
    var
        TempBlob: Codeunit "Temp Blob";
    begin
        if IsHandled then
            exit;
        if ReportSelectionUsage <> ReportSelectionUsage::"S.Invoice" then
            exit;

        // Placeholder: create the PDF with the other extension
        if not CreatePdfWithOtherExtension(RecVarToPrint, TempBlob) then
            exit; // Freeze then creates the PDF from the report selection as usual

        TempBlobList.Add(TempBlob);
        IsHandled := true;
    end;

    local procedure CreatePdfWithOtherExtension(RecVarToPrint: Variant; var TempBlob: Codeunit "Temp Blob"): Boolean
    begin
        // Call the function of the other extension here
    end;
}

Important notes:

  • If the report selection has several lines for the same usage, the event is raised several times. The check if IsHandled then exit; prevents your PDF from being archived more than once.
  • To keep the Freeze PDF and only add a file, use OnAfterPrintOutgoingSalesDocToPDF or OnAfterPrintOutgoingPurchaseDocToPDF instead and add your file to the TempBlobList.

Important rules

More integration events

These Freeze events are useful when archiving additional documents:

CodeunitEventWhenPurpose
DXP FRZ Attachment Mgt.OnBeforeSaveReportAsPDFInTempBlobAt posting, before each PDF is createdSupply your own PDF instead of the standard report (example 3).
DXP FRZ Attachment Mgt.OnAfterPrintOutgoingSalesDocToPDF, OnAfterPrintOutgoingPurchaseDocToPDFAt posting, after the PDFs were createdAdd or replace files.
DXP Freeze HelperOnBeforeGetFreezeRecordTypeFromReportSelectionUsageWhen the record type is determinedUse your own record type for specific report usages.
DXP Freeze Queue Buffer Mgt.OnBeforeDoUpdateWhen the queue entry is createdDecide whether another entry for the same record becomes a new version or a new archive record.
DXP Freeze Queue ProcessingOnAfterPerformFREEZE, OnAfterProcessWaitingQueueBufferEntryIn the Freeze job, after the uploadFollow-up work, for example reporting the archiving status back to the other extension.

Only set IsHandled in the event OnBeforePerformFREEZE if your extension fully takes over the upload to the archive. In that case, fill the fields "Archive Record ID" and "API Path" on the queue entry. From Freeze 1.30 on, Freeze otherwise sets the entry to status Error.

Testing your implementation

  1. Check that Freeze is active and that Archive Email & Attachments is enabled in the Freeze Setup.
  2. Issue a test document through the other extension.
  3. Open the Freeze Queue Entries. A new entry with the right record type and reference date should appear.
  4. Wait for the Freeze job (every minute). The status moves via Waiting and Processed to Archived.
  5. Open the document in Business Central and check in the archive search that the file appears there.