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 posted | Business 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 sent | Freeze 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:
| Option | How it works | A good fit when … |
|---|---|---|
| 1. Archive when the document is issued | Your 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 queue | A 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 posting | Your 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 manually | Users 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:
| Codeunit | Procedure | Purpose |
|---|---|---|
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 Helper | GetFreezeRecordTypeFromReportSelectionUsage(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
OnAfterPrintOutgoingSalesDocToPDForOnAfterPrintOutgoingPurchaseDocToPDFinstead and add your file to theTempBlobList.
Important rules
- Link the right record. Freeze writes all fields of the linked record to the archive as search fields. So always link the Business Central document, for example the posted sales invoice, and not a log entry of the other extension. Only then can users find the document later by document number or customer.
- Set record type and reference date on purpose. Both control how the archive files and retains the document. Determine the record type with
GetFreezeRecordTypeFromReportSelectionUsageand use the document date as reference date. See Record Types. - Never swallow errors. If your extension cannot pass a document on, this must be visible, for example as an error message. Do not wrap the Freeze call in
if ... then;. - Same document again: new version. If the archive already contains a record for the same document, Freeze does not create a second record but a new version of the existing one. This happens, for example, when Freeze already archived the invoice at posting.
- Permissions. The user in whose session the queue entry is created needs the permission set
DXP Freeze User. See Permission sets.
More integration events
These Freeze events are useful when archiving additional documents:
| Codeunit | Event | When | Purpose |
|---|---|---|---|
DXP FRZ Attachment Mgt. | OnBeforeSaveReportAsPDFInTempBlob | At posting, before each PDF is created | Supply your own PDF instead of the standard report (example 3). |
DXP FRZ Attachment Mgt. | OnAfterPrintOutgoingSalesDocToPDF, OnAfterPrintOutgoingPurchaseDocToPDF | At posting, after the PDFs were created | Add or replace files. |
DXP Freeze Helper | OnBeforeGetFreezeRecordTypeFromReportSelectionUsage | When the record type is determined | Use your own record type for specific report usages. |
DXP Freeze Queue Buffer Mgt. | OnBeforeDoUpdate | When the queue entry is created | Decide whether another entry for the same record becomes a new version or a new archive record. |
DXP Freeze Queue Processing | OnAfterPerformFREEZE, OnAfterProcessWaitingQueueBufferEntry | In the Freeze job, after the upload | Follow-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
- Check that Freeze is active and that Archive Email & Attachments is enabled in the Freeze Setup.
- Issue a test document through the other extension.
- Open the Freeze Queue Entries. A new entry with the right record type and reference date should appear.
- Wait for the Freeze job (every minute). The status moves via
WaitingandProcessedtoArchived. - Open the document in Business Central and check in the archive search that the file appears there.
No comments to display
No comments to display