Attachment Download
The DXP Freeze Result Management codeunit provides two primary methods for downloading archived attachments as ZIP files:
DownloadAttachmentsAsZipWithPagination- Downloads attachments from search query results with pagination supportDownloadAttachmentsAsZipFromRecord- Downloads attachments from specific Business Central records
Both methods organize attachments into structured ZIP archives with comprehensive metadata for audit and tracking purposes.
Method 1: DownloadAttachmentsAsZipWithPagination
Purpose
Downloads all attachments from a search query result set, processing results page by page to handle large datasets efficiently. Each record’s attachments are organized into individual ZIP files within a main ZIP archive.
Overloads Available
1. Basic Usage
procedure DownloadAttachmentsAsZipWithPagination(SearchQuery: Text; var ZipArchive: Codeunit "Data Compression"; var AttachmentCount: Integer): Boolean
2. With Filters
procedure DownloadAttachmentsAsZipWithPagination(SearchQuery: Text; var ZipArchive: Codeunit "Data Compression"; var AttachmentCount: Integer; FilenameFilter: Text; FileExtensionFilter: Text): Boolean
3. Full Control
procedure DownloadAttachmentsAsZipWithPagination(SearchQuery: Text; var ZipArchive: Codeunit "Data Compression"; var AttachmentCount: Integer; FilenameFilter: Text; FileExtensionFilter: Text; StoreApiLink: Text; RecordsPerPage: Integer; SuppressDialog: Boolean): Boolean
4. Pre-populated Records (Advanced)
procedure DownloadAttachmentsAsZipWithPagination(var TempFrzResultQueryHeader: Record "DXP FRZ Query Result Header" temporary; var TempFrzResultRecordHeader: Record "DXP FRZ Record Result Header" temporary; var TempFrzResultRecordField: Record "DXP FRZ Result Record-Field" temporary; var TempFrzAttachmentResult: Record "DXP FRZ Attachment Result" temporary; SearchQuery: Text; var ZipArchive: Codeunit "Data Compression"; var AttachmentCount: Integer; FilenameFilter: Text; FileExtensionFilter: Text; SuppressDialog: Boolean): Boolean
Parameters
| Parameter | Type | Description | 
|---|---|---|
SearchQuery | 
Text | Freeze search query string. If empty in pre-populated overload, uses existing query or re-executes search | 
ZipArchive | 
Codeunit “Data Compression” | ZIP archive object that will contain the downloaded files | 
AttachmentCount | 
Integer (var) | Returns the total number of attachments downloaded | 
FilenameFilter | 
Text | Filter for attachment filenames (e.g., ‘.pdf’, 'invoice’) | 
FileExtensionFilter | 
Text | Filter for file extensions (e.g., ‘pdf’, ‘docx’) | 
StoreApiLink | 
Text | Optional specific store API link | 
RecordsPerPage | 
Integer | Number of records per page (default: 100) | 
SuppressDialog | 
Boolean | Whether to suppress progress dialog | 
Return Value
Boolean:trueif attachments were found and downloaded;falseotherwise
Example Usage
Basic Download
procedure DownloadSearchResults()
var
    ResultMgt: Codeunit "DXP FRZ Result Mgt.";
    ZipArchive: Codeunit "Data Compression";
    FileMgt: Codeunit "File Management";
    TempBlob: Codeunit "Temp Blob";
    AttachmentCount: Integer;
    InStr: InStream;
    OutStr: OutStream;
    SearchQuery: Text;
begin
    SearchQuery := 'invoice AND 2024';
    
    if ResultMgt.DownloadAttachmentsAsZipWithPagination(SearchQuery, ZipArchive, AttachmentCount) then begin
        // Save ZIP to file
        TempBlob.CreateOutStream(OutStr);
        ZipArchive.SaveZipArchive(OutStr);
        TempBlob.CreateInStream(InStr);
        
        FileMgt.DownloadFromStreamHandler(InStr, '', '', '', 'SearchResults.zip');
        Message('Downloaded %1 attachments successfully.', AttachmentCount);
    end else
        Message('No attachments found for the search query.');
end;
With Filters
procedure DownloadPDFInvoices()
var
    ResultMgt: Codeunit "DXP FRZ Result Mgt.";
    ZipArchive: Codeunit "Data Compression";
    AttachmentCount: Integer;
    SearchQuery: Text;
begin
    SearchQuery := 'type:invoice';
    
    if ResultMgt.DownloadAttachmentsAsZipWithPagination(
        SearchQuery, 
        ZipArchive, 
        AttachmentCount, 
        '*.pdf', // Only PDF files
        'pdf'    // File extension filter
    ) then begin
        // Process the ZIP archive
        ProcessDownloadedFiles(ZipArchive, AttachmentCount);
    end;
end;
Using Pre-populated Records
procedure DownloadFromExistingResults()
var
    ResultMgt: Codeunit "DXP FRZ Result Mgt.";
    TempFrzResultQueryHeader: Record "DXP FRZ Query Result Header" temporary;
    TempFrzResultRecordHeader: Record "DXP FRZ Record Result Header" temporary;
    TempFrzResultRecordField: Record "DXP FRZ Result Record-Field" temporary;
    TempFrzAttachmentResult: Record "DXP FRZ Attachment Result" temporary;
    ZipArchive: Codeunit "Data Compression";
    AttachmentCount: Integer;
begin
    // Assume these records are already populated from a previous search
    PopulateSearchResults(TempFrzResultQueryHeader, TempFrzResultRecordHeader, TempFrzResultRecordField, TempFrzAttachmentResult);
    
    // Download using existing results without re-executing search
    if ResultMgt.DownloadAttachmentsAsZipWithPagination(
        TempFrzResultQueryHeader,
        TempFrzResultRecordHeader,
        TempFrzResultRecordField,
        TempFrzAttachmentResult,
        'invoice search', // SearchQuery - if empty, will re-execute search
        ZipArchive,
        AttachmentCount,
        '',     // No filename filter
        '',     // No extension filter
        true    // Suppress dialog
    ) then begin
        ProcessDownloadedFiles(ZipArchive, AttachmentCount);
    end;
end;
ZIP Structure (Pagination)
SearchResults.zip
├── export-metadata.json
├── Invoice_001_V1_20241201_1430.zip
│   ├── {GUID}_invoice.pdf
│   └── {GUID}_supporting_doc.docx
├── PurchaseOrder_002_V2_20241202_0900.zip
│   └── {GUID}_po_document.pdf
└── Contract_003_V1_20241203_1200.zip
    ├── {GUID}_contract.pdf
    └── {GUID}_amendment.pdf
Method 2: DownloadAttachmentsAsZipFromRecord
Purpose
Downloads attachments from specific Business Central records. Each selected record’s attachments are organized into individual ZIP files within a main ZIP archive.
Overloads Available
1. Basic Usage
procedure DownloadAttachmentsAsZipFromRecord(var SelectedRecord: RecordRef; var ZipArchive: Codeunit "Data Compression"): Boolean
2. With Filters
procedure DownloadAttachmentsAsZipFromRecord(var SelectedRecord: RecordRef; var ZipArchive: Codeunit "Data Compression"; FilenameFilter: Text; FileExtensionFilter: Text): Boolean
Parameters
| Parameter | Type | Description | 
|---|---|---|
SelectedRecord | 
RecordRef (var) | RecordRef containing the selected Business Central records | 
ZipArchive | 
Codeunit “Data Compression” | ZIP archive object that will contain the downloaded files | 
FilenameFilter | 
Text | Filter for attachment filenames | 
FileExtensionFilter | 
Text | Filter for file extensions | 
Return Value
Boolean:trueif attachments were found and downloaded;falseotherwise
Example Usage
Download from Sales Invoices
procedure DownloadInvoiceAttachments()
var
    SalesInvoiceHeader: Record "Sales Invoice Header";
    ResultMgt: Codeunit "DXP FRZ Result Mgt.";
    ZipArchive: Codeunit "Data Compression";
    RecordRef: RecordRef;
    HasAttachments: Boolean;
begin
    // Select specific invoices
    SalesInvoiceHeader.SetRange("Posting Date", DMY2Date(1, 1, 2024), DMY2Date(31, 12, 2024));
    SalesInvoiceHeader.SetFilter("Sell-to Customer No.", '10000|20000');
    
    if SalesInvoiceHeader.FindSet() then begin
        RecordRef.GetTable(SalesInvoiceHeader);
        
        HasAttachments := ResultMgt.DownloadAttachmentsAsZipFromRecord(RecordRef, ZipArchive);
        
        if HasAttachments then
            SaveZipFile(ZipArchive, 'InvoiceAttachments.zip')
        else
            Message('No attachments found for the selected invoices.');
    end;
end;
Download with Filters from Page
// In a page extension
action(DownloadAttachmentsFiltered)
{
    Caption = 'Download Filtered Attachments';
    Image = ExportFile;
    
    trigger OnAction()
    var
        ResultMgt: Codeunit "DXP FRZ Result Mgt.";
        ZipArchive: Codeunit "Data Compression";
        RecordRef: RecordRef;
        FilenameFilter: Text;
        FileExtensionFilter: Text;
    begin
        // Show filter dialog
        if ShowFilterDialog(FilenameFilter, FileExtensionFilter) then begin
            CurrPage.SetSelectionFilter(Rec);
            RecordRef.GetTable(Rec);
            
            if ResultMgt.DownloadAttachmentsAsZipFromRecord(
                RecordRef, 
                ZipArchive, 
                FilenameFilter, 
                FileExtensionFilter
            ) then
                DownloadZipFile(ZipArchive, 'FilteredAttachments.zip');
        end;
    end;
}
ZIP Structure (Records)
RecordAttachments.zip
├── export-metadata.json
├── Sales_Invoice_Header_Company_SI-001.zip
│   ├── {GUID}_invoice.pdf
│   └── {GUID}_terms.pdf
├── Sales_Invoice_Header_Company_SI-002.zip
│   └── {GUID}_invoice.pdf
└── Sales_Invoice_Header_Company_SI-003.zip
    ├── {GUID}_invoice.pdf
    ├── {GUID}_delivery_note.pdf
    └── {GUID}_receipt.jpg
Metadata Structure
Both methods generate comprehensive metadata in export-metadata.json:
Pagination Export Metadata
{
  "exportInfo": {
    "exportTimestamp": "2024-12-19T10:13:52.248Z",
    "exportedBy": "USER001",
    "searchQuery": "type:invoice AND year:2024",
    "totalRecordsFound": 150,
    "totalPages": 15,
    "exportType": "paginated-search",
    "description": "Freeze Search Query Export"
  },
  "appliedFilters": {
    "filenameFilter": "*.pdf",
    "fileExtensionFilter": "pdf"
  },
  "statistics": {
    "pagesProcessed": 15,
    "totalRecordsProcessed": 150,
    "recordsWithAttachments": 120,
    "recordsWithoutAttachments": 30,
    "totalAttachments": 245,
    "exportCompletedAt": "2024-12-19T10:15:33.021Z"
  },
  "records": [
    {
      "recordId": "{GUID}",
      "title": "Invoice INV-2024-001",
      "version": 1,
      "archivedAt": "2024-12-01T09:30:00Z",
      "archivedBy": "SYSTEM",
      "type": "Sales Invoice",
      "masterId": "{GUID}",
      "attachmentCount": 3,
      "hasAttachments": true,
      "zipFile": "Invoice_INV-2024-001_V1_20241201_0930.zip"
    }
  ]
}
Record Export Metadata
{
  "exportTimestamp": "2024-12-19T14:30:00Z",
  "exportedBy": "USER001",
  "totalRecordsProcessed": 25,
  "description": "DXP Freeze Attachments Export",
  "sourceTable": {
    "tableNumber": 112,
    "tableName": "Sales Invoice Header",
    "tableCaption": "Posted Sales Invoice"
  },
  "appliedFilters": {
    "filenameFilter": "*.pdf",
    "fileExtensionFilter": "pdf"
  },
  "statistics": {
    "totalAttachments": 45,
    "recordsWithAttachments": 20,
    "recordsWithoutAttachments": 5,
    "totalZipFiles": 20
  },
  "records": [
    {
      "recordId": "Sales Invoice Header: Company, SI-001",
      "systemId": "{GUID}",
      "primaryKey": {
        "fields": [
          {
            "fieldName": "No.",
            "fieldValue": "SI-001",
            "fieldType": "Code"
          }
        ]
      },
      "hasAttachments": true,
      "attachmentCount": 2,
      "zipFile": "Sales_Invoice_Header_Company_SI-001.zip"
    }
  ]
}
Performance Considerations
Pagination Method
- Large Result Sets: Automatically handles pagination to process large datasets efficiently
 - Memory Management: Processes one page at a time, clearing memory between pages
 - Progress Tracking: Shows real-time progress for long-running operations
 - Recommended For: Search queries that may return hundreds or thousands of records
 
Record Method
- Selected Records: Processes only the records you specifically select
 - Direct Processing: No pagination overhead for smaller datasets
 - Batch Processing: Efficient for processing specific record sets
 - Recommended For: Targeted downloads from specific Business Central records
 
Error Handling
Both methods include comprehensive error handling:
Common Scenarios
- No Results Found: Returns 
falsewhen no records or attachments are found - Permission Issues: Automatically excludes records the user cannot access
 - API Failures: Gracefully handles API communication errors
 - Empty Filters: Handles empty or invalid filter parameters
 
Best Practices
// Always check return value
if not ResultMgt.DownloadAttachmentsAsZipWithPagination(SearchQuery, ZipArchive, AttachmentCount) then begin
    Message('No attachments found or download failed.');
    exit;
end;
// Validate attachment count
if AttachmentCount = 0 then begin
    Message('Search completed but no attachments matched the criteria.');
    exit;
end;
// Handle large downloads
if AttachmentCount > 1000 then
    if not Confirm('This will download %1 attachments. Continue?', false, AttachmentCount) then
        exit;
Integration Events
Both methods support integration events for customization:
Available Events
OnBeforeDownloadAttachmentsAsZip: Modify behavior before download startsOnBeforeProcessAttachmentForZip: Skip or modify individual attachmentsOnAfterGetAttachmentBase64: Modify attachment content after retrievalOnAfterAddAttachmentToZip: Perform actions after adding to ZIPOnNoAttachmentsFound: Handle no attachments scenario
Example Integration
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP FRZ Result Mgt.", 'OnBeforeProcessAttachmentForZip', '', false, false)]
local procedure OnBeforeProcessAttachmentForZip(var TempFrzAttachmentResult: Record "DXP FRZ Attachment Result" temporary; var IsHandled: Boolean)
begin
    // Skip attachments larger than 10MB
    if TempFrzAttachmentResult.Filesize > 10485760 then
        IsHandled := true;
end;
File Naming Conventions
Automatic Sanitization
All filenames are automatically sanitized using the SanitizeFileName method:
- Invalid characters (
< > : " / \ | ? *) are replaced with underscores - Spaces are replaced with underscores
 - Maximum filename lengths are enforced
 
Unique Naming
- Individual Files: Include attachment GUID prefix to ensure uniqueness
 - ZIP Files: Include record information and timestamps
 - No Conflicts: Guaranteed unique names within each ZIP archive
 
                                                    
No Comments