Extension Development

This chapter lists important integration events and examples that help in developing an extension for SQUEEZE for BC.

Important:
Customizations may only be performed by appropriately trained consultants/developers. Furthermore, these are always outside of the standard support.

Modify Squeeze origin attachment

Overview

The OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment integration event allows third-party developers to customize document attachment records before they are modified during the SQUEEZE attachment processing workflow. An origin attachment is the relevant attachment that leads to the creation of a document (e.g. the vendor´s invoice).

Event Declaration

[IntegrationEvent(false, false)]
local procedure OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment(var DocumentAttachment: Record "DXP Document Attachment")
begin
end;

Event Parameters

Parameter Type Description
DocumentAttachment Record "DXP Document Attachment" (var) The document attachment record that is about to be modified. Passed by reference, allowing modifications.

When This Event is Triggered

This event is fired during the SaveAttachments procedure when:

  1. An attachment is being processed from SQUEEZE
  2. The attachment is identified as an origin file (IsOriginFile = true)
  3. The system has set the origin file name and DXP Is Origin File flag
  4. Just before the DocumentAttachment.Modify(true) call

Use Cases

This integration event is useful for:

Implementation Example

Change The Filename Before Modify

[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment', '', false, false)]
local procedure OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment(var DocumentAttachment: Record "DXP Document Attachment")
var
    CoreDocument: Record "DXP Document";
    SQZDocumentHeader: Record "DXP SQZ Document Header";
    Vendor: Record Vendor;
    NewFileName: Text[250];
    VendorPrefix: Text[50];
begin
    // Get the core document using the document attachment's Document No.
    if not CoreDocument.Get(DocumentAttachment."Document No.") then
        exit;

    // Check if the core document has a linked SQUEEZE document
    if IsNullGuid(CoreDocument."Linked-to Record Id") then
        exit;

    // Get the SQZ Document Header using the SystemId from Linked-to Record Id
    if not GetSQZDocumentHeaderFromRecordId(CoreDocument."Linked-to Record Id", SQZDocumentHeader) then
        exit;

    // Check if Buy-from Vendor No. is populated
    if SQZDocumentHeader."Buy-from Vendor No." = '' then
        exit;

    // Get the vendor record
    if not Vendor.Get(SQZDocumentHeader."Buy-from Vendor No.") then
        exit;

    // Create vendor prefix from Search Name (fallback to Name if Search Name is empty)
    if Vendor."Search Name" <> '' then
        VendorPrefix := Vendor."Search Name"
    else
        VendorPrefix := Vendor.Name;

    // Clean the vendor prefix (remove invalid filename characters and limit length)
    VendorPrefix := CleanFilenameText(VendorPrefix, 30);

    // Check if vendor prefix already exists in filename to avoid duplicates
    if DocumentAttachment."File Name".StartsWith(VendorPrefix + '_') then
        exit;

    // Create new filename with vendor prefix
    NewFileName := VendorPrefix + '_' + DocumentAttachment."File Name";

    // Update the attachment filename
    DocumentAttachment."File Name" := NewFileName;
end;

local procedure GetSQZDocumentHeaderFromRecordId(LinkedRecordId: Guid; var SQZDocumentHeader: Record "DXP SQZ Document Header"): Boolean
var
    RecRef: RecordRef;
    SystemIdFieldRef: FieldRef;
begin
    // Method 1: Try to get the record directly if LinkedRecordId is actually a SystemId
    if SQZDocumentHeader.GetBySystemId(LinkedRecordId) then
        exit(true);

    // Method 2: If that fails, we need to find the record another way
    // This assumes the Linked-to Record Id might be stored differently
    SQZDocumentHeader.Reset();
    SQZDocumentHeader.SetRange(SystemId, LinkedRecordId);
    exit(SQZDocumentHeader.FindFirst());
end;

local procedure CleanFilenameText(InputText: Text; MaxLength: Integer): Text
var
    CleanText: Text;
begin
    // Remove invalid filename characters
    CleanText := DelChr(InputText, '=', '<>|"/\:*?');
    
    // Replace spaces with underscores for better filename compatibility
    CleanText := CleanText.Replace(' ', '_');
    
    // Limit length
    CleanText := CopyStr(CleanText, 1, MaxLength);
    
    exit(CleanText);
end;

Important Considerations

Data Integrity

Performance

Error Handling

Troubleshooting

If your event subscriber isn’t being triggered:

  1. Test with origin files specifically (non-origin files won’t trigger this event)

 

 

Alternative use of the SQUEEZE attachments

The following briefly explains how the attachments downloaded by SQUEEZE for BC can be used for a scenario that differs from the intended use.

After creating the purchase document from the validated SQUEEZE document, you have the option of accessing the attachments ( assuming they have been downloaded in advance).

    // Set IsHandled to true and implement your code
    // Do not forget to deactivate "Transfer attachments to target document" in the Document Class Setup
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Mgt.", 'OnBeforeTransferCoreAttachmentsToStandardDocument', '', false, false)]
    local procedure DXPDocMgtOnBeforeTransferCoreAttachmentsToStandardDocument(Document: Record "DXP Document"; var IsHandled: Boolean)
    begin
    	IsHandled := true;
        HandleCoreAttachmentsAccordingToYourNeeds(Document, IsHandled);
    end;

    procedure HandleCoreAttachmentsAccordingToYourNeeds(Document: Record "DXP Document"; var IsHandled: Boolean)
    var
        CoreAttachments: Record "DXP Document Attachment";
        InStr: InStream;
    begin
        CoreAttachments.SetRange("Document No.", Document."No.");
        CoreAttachments.IsEmpty() then
       		exit;
        CoreAttachments.FindSet();
        repeat
            CoreAttachments.Calcfields(Attachment);
            CoreAttachments.Attachment.CreateInStream(InStr);
            // Here you can handle the file stream according to your needs
         	[...]
        until CoreAttachments.Next() = 0;
    end;

Adjustment of attachment file names after validation

Overview

This guide shows how developers can customize the file names of attachments in DEXPRO SQUEEZE. The OnBeforeAddLineToDocumentJObj event is executed after the plausibility check, ensuring that all document data is fully validated and available.

Available Event

OnBeforeAddLineToDocumentJObj

For invoices/credit notes: Codeunit 70954657 "DXP SQZ P. Inv/Crdt Memo Impl."
For orderconfirmations: Codeunit 70954658 "DXP SQZ P. Order Conf. Impl."

When accessed: After header processing, before line processing, after plausibility check

Implementation examples

Example 1: Simple vendor prefix for invoices/credit notes

codeunit 50100 "Custom Invoice Filename"
{
        [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnBeforeAddLineToDocumentJObj', '', false, false)]
    local procedure OnBeforeAddLineToDocumentJObjInvoice(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
    begin
        CustomizeAttachmentFilenames(DocHeader);
    end;

    local procedure CustomizeAttachmentFilenames(DocHeader: Record "DXP SQZ Document Header")
    var
        DocumentAttachment: Record "DXP Document Attachment";
        VendorPrefix: Text;
        NewFileName: Text[1024];
    begin
        // Beenden, wenn keine Kreditorinformationen vorhanden
        if DocHeader."Buy-from Vendor No." = '' then
            exit;

        // Kreditorenprefix erstellen
        VendorPrefix := DocHeader."Buy-from Vendor No." + '_';

        // Alle Anhänge für dieses Dokument bearbeiten
        DocumentAttachment.Reset();
        DocumentAttachment.SetRange("Document No.", DocHeader."Core Document No.");

        if DocumentAttachment.FindSet(true) then
            repeat
                // Überspringen, wenn Prefix bereits vorhanden
                if not DocumentAttachment."File Name".Contains(VendorPrefix) then begin
                    // Neuen Dateinamen mit Prefix erstellen
                    NewFileName := VendorPrefix + DocumentAttachment."File Name";

                    // Aktualisieren
                    DocumentAttachment."File Name" := CopyStr(NewFileName, 1, MaxStrLen(DocumentAttachment."File Name"));
                    DocumentAttachment.Modify(true);
                end;
            until DocumentAttachment.Next() = 0;
    end;
}

Example 2: Order confirmations - vendor prefix

codeunit 50101 "Custom Order Conf Filename"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Order Conf. Impl.", 'OnBeforeAddLineToDocumentJObj', '', false, false)]
    local procedure OnBeforeAddLineToDocumentJObjOrderConf(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
    begin
        CustomizeAttachmentFilenames(DocHeader);
    end;

    local procedure CustomizeAttachmentFilenames(DocHeader: Record "DXP SQZ Document Header")
    var
        DocumentAttachment: Record "DXP Document Attachment";
        VendorPrefix: Text;
        NewFileName: Text[1024];
    begin
        // Beenden, wenn keine Kreditorinformationen vorhanden
        if DocHeader."Buy-from Vendor No." = '' then
            exit;

        // Kreditorenprefix erstellen
        VendorPrefix := DocHeader."Buy-from Vendor No." + '_';

        // Alle Anhänge für dieses Dokument bearbeiten
        DocumentAttachment.Reset();
        DocumentAttachment.SetRange("Document No.", DocHeader."Core Document No.");

        if DocumentAttachment.FindSet(true) then
            repeat
                // Überspringen, wenn Prefix bereits vorhanden
                if not DocumentAttachment."File Name".Contains(VendorPrefix) then begin
                    // Neuen Dateinamen mit Prefix erstellen
                    NewFileName := VendorPrefix + DocumentAttachment."File Name";

                    // Aktualisieren
                    DocumentAttachment."File Name" := CopyStr(NewFileName, 1, MaxStrLen(DocumentAttachment."File Name"));
                    DocumentAttachment.Modify(true);
                end;
            until DocumentAttachment.Next() = 0;
    end;
}

Example 3: Both document types - Different prefixes for source files

codeunit 50102 "Advanced Filename Handling"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnBeforeAddLineToDocumentJObj', '', false, false)]
    local procedure OnBeforeAddLineToDocumentJObjInvoice(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
    begin
        CustomizeFilenamesWithSourceHandling(DocHeader);
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Order Conf. Impl.", 'OnBeforeAddLineToDocumentJObj', '', false, false)]
    local procedure OnBeforeAddLineToDocumentJObjOrderConf(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
    begin
        CustomizeFilenamesWithSourceHandling(DocHeader);
    end;

    local procedure CustomizeAttachmentFilenames(DocHeader: Record "DXP SQZ Document Header")
    var
        DocumentAttachment: Record "DXP Document Attachment";
        VendorPrefix: Text;
        NewFileName: Text[1024];
    begin
        // Beenden, wenn keine Kreditorinformationen vorhanden
        if DocHeader."Buy-from Vendor No." = '' then
            exit;

        // Kreditorenprefix erstellen
        VendorPrefix := DocHeader."Buy-from Vendor No." + '_';

        // Alle Anhänge für dieses Dokument bearbeiten
        DocumentAttachment.Reset();
        DocumentAttachment.SetRange("Document No.", DocHeader."Core Document No.");

        if DocumentAttachment.FindSet(true) then
            repeat
                // Überspringen, wenn Prefix bereits vorhanden
                if not DocumentAttachment."File Name".Contains(VendorPrefix) then begin
                    // Neuen Dateinamen mit Prefix erstellen
                    NewFileName := VendorPrefix + DocumentAttachment."File Name";

                    // Aktualisieren
                    DocumentAttachment."File Name" := CopyStr(NewFileName, 1, MaxStrLen(DocumentAttachment."File Name"));
                    DocumentAttachment.Modify(true);
                end;
            until DocumentAttachment.Next() = 0;
    end;
}

Practical information

Available data at the time of the event:

Example results:

Best Practices:

Adding a field in the validation (from version 2.10)

The following example extension shows how developers can add fields in the validation (here: document class invoice/credit note).

Optional integration events incl. sample procedures:

codeunit 50100 EventSubs
{
    //
    // [If you want to perform plausibility checks on the newly added header field, this is the place]
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnAfterDoHeaderPlausibilityChecks', '', false, false)]
    local procedure SQZPInvCrdtMemoImplOnBeforeDoHeaderPlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
    begin
        CheckCustomer(PlausibilityCheck, DocHeader."Customer No.");
    end;

    // 
    // [If you want to perform plausibility checks on the newly added line field, this is the place]
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnAfterDoLinePlausibilityChecks', '', false, false)]
    local procedure SQZPInvCrdtMemoImplOnAfterDoLinePlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; DocLine: Record "DXP SQZ Document Line"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
    begin
        CheckCustomer(PlausibilityCheck, DocLine."Customer No.");
    end;

    [TryFunction]
    local procedure CustomerExists(CustomerNo: Code[20])
    var
        Customer: Record Customer;
    begin
        Customer.Get(CustomerNo);
    end;

    local procedure CheckCustomer(var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt."; CustomerNo: Code[20]): Boolean
    begin
        if CustomerNo = '' then
            exit(false);

        if not CustomerExists(CustomerNo) then begin
            PlausibilityCheck.AddPlausibilityCheckEntry(GetLastErrorText(), Page::"Customer List");
            exit(false);
        end;

        exit(true);
    end;
}

Table Extensions:

SQUEEZE 4 BC
tableextension 50100 "SQZ Doc. Header Ext." extends "DXP SQZ Document Header"
{
    fields
    {
        field(50100; "Customer No."; Code[20])
        {
            TableRelation = Customer;
            ValidateTableRelation = false;
            Caption = 'Customer No.';
        }
    }
}
tableextension 50101 "SQZ Doc. Line Ext." extends "DXP SQZ Document Line"
{
    fields
    {
        field(50100; "Customer No."; Code[20])
        {
            TableRelation = Customer;
            ValidateTableRelation = false;
            Caption = 'Customer No.';
        }
    }
}
Target document (here: Purchase Document)
tableextension 50102 "Purchase Header Ext." extends "Purchase Header"
{
    fields
    {
        field(50100; "DXP Customer No."; Code[20])
        {
            DataClassification = CustomerContent;
            TableRelation = Customer;
            Caption = 'DEXPRO Customer No.';
        }
    }
}

 

Page Extensions:

SQUEEZE 4 BC
pageextension 50100 "SQZ Document Ext." extends "DXP SQZ Document v2"
{
    layout
    {
        addafter(BuyFromVendorInternal)
        {
            field("Customer No. Internal"; Rec."Customer No.")
            {
                ApplicationArea = All;
                ToolTip = 'Specifies the value of the Customer No. field.';
              	//The following code is optional. It is used to highlight a recognized value in the Squeeze Viewer
                trigger OnAssistEdit()
                begin
                    MarkField(Rec.FieldNo("Customer No."), Rec."Customer No.", Rec);
                end;

                trigger OnValidate()
                begin
                    CheckPlausibility();
                end;
            }
        }
        addafter(BuyFromVendorExternal)
        {
            field("Customer No."; Rec."Customer No.")
            {
                ApplicationArea = All;
                ToolTip = 'Specifies the value of the Customer No. field.';
                //The following code is optional. It is used to highlight a recognized value in the Squeeze Viewer
                trigger OnAssistEdit()
                var
                    ApiMgt: Codeunit "DXP SQZ API Mgt.";
                    ViewerResident: Codeunit "DXP SQUEEZE Viewer Resident";
                begin
                    MarkField(Rec.FieldNo("Customer No."), Rec."Customer No.", Rec);
                end;

                trigger OnValidate()
                begin
                    CheckPlausibility();
                end;
            }
        }
    }

    local procedure MarkField(AppFldNo: Integer; FieldVal: Variant; DocHeader: Record "DXP SQZ Document Header")
    var
        ApiMgt: Codeunit "DXP SQZ API Mgt.";
        ViewerResident: Codeunit "DXP SQUEEZE Viewer Resident";
    begin
        ViewerResident := GetViewerResident();
        ApiMgt.MarkField(AppFldNo, FieldVal, ViewerResident, DocHeader);
    end;
}
pageextension 50101 "SQZ Document Sf. Ext." extends "DXP SQZ Document Subform"
{
    layout
    {
        addafter("No.")
        {
            field("Customer No."; Rec."Customer No.")
            {
                ApplicationArea = All;
                ToolTip = 'Specifies the value of the Customer No. field.';
            }
        }
    }
}
Target document (here: Purchase document)
pageextension 50102 "Purchase Invoice Ext." extends "Purchase Invoice"
{
    layout
    {
        addafter("Buy-from Vendor No.")
        {
            field("DXP Customer No."; Rec."DXP Customer No.")
            {
                ToolTip = 'Specifies the value of the DEXPRO Customer No. field.';
                ApplicationArea = all;
            }
        }
    }
}
pageextension 50103 "Purchase Inv. Sf Ext." extends "Purch. Invoice Subform"
{
    layout
    {
        addafter("No.")
        {
            field("DXP Customer No."; Rec."DXP Customer No.")
            {
                ToolTip = 'Specifies the value of the DEXPRO Customer No. field.';
                ApplicationArea = all;
            }
        }
    }
}

 

Implementation of a user-defined, automatic order reconciliation

Introduction

Automatic order matching is an important part of document processing in Squeeze for Business Central. In the standard system, the system compares incoming documents with existing orders and goods receipts. This documentation shows you how you can extend this matching process to include your own document types.

The standard implementation uses a three-step process:

  1. Collect relevant document numbers based on business rules
  2. A detailed comparison is performed for each document number found.
  3. The positions read by Squeeze are then enriched with the data found.

We will retain this proven approach in the following example implementation.

Integration point

The central integration point is the OnBeforePerformAutomaticOrdermatch event in codeunit 70954632 “DXP SQZ Document Mgt.”. This event is called after header and line data has been created.

[IntegrationEvent(false, false)]
local procedure OnBeforePerformAutomaticOrdermatch(
    DocHeader: Record "DXP SQZ Document Header"; 
    var OrderNoList: List of [Code[20]]; 
    var IsHandled: Boolean)

Parameter

Implementation example

Technical implementation

1. Extension of the document type enum

First, we extend the possible document types to include our own type:

enumextension 50100 "Custom Order Match Doc. Type" extends "DXP Order Match Document Type"
{
    value(50000; "Custom")
    {
        Caption = 'Custom Document';
    }
}

2. Implementation of the matching logic

The central point of our implementation is a code unit that controls the synchronization process. The integration point is:

//codeunit 50100 "Custom Document Matching"
//{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforePerformAutomaticOrdermatch', '', false, false)]
    local procedure OnBeforePerformAutomaticOrdermatch(
        DocHeader: Record "DXP SQZ Document Header";
        var OrderNoList: List of [Code[20]];
        var IsHandled: Boolean)
    var
        CustomSourceDoc: Record "Custom Source Document";
        CustomDocNo: Code[20];
    begin
        // Take control of the matching process and prevent standard processing
        IsHandled := true;
        
        // Find potential matching documents
        CustomSourceDoc.SetRange("Vendor No.", DocHeader."Buy-from Vendor No.");
        // Add your specific document status or type filters
        CustomSourceDoc.SetRange("Document Type", CustomSourceDoc."Document Type"::Order);
        CustomSourceDoc.SetRange(Status, CustomSourceDoc.Status::Released);
        
        // Add matching document numbers to the list
        if CustomSourceDoc.FindSet() then
            repeat
            	CustomDocNo := CustomSourceDoc."No.";
                if IsDocumentEligibleForMatching(CustomSourceDoc, DocHeader) then
                	if not OrderNoList.Contains(CustomDocNo) then
                    	OrderNoList.Add(CustomDocNo);
            until CustomSourceDoc.Next() = 0;

        // Process all collected documents
        ProcessMatchingDocuments(DocHeader, OrderNoList);
    end;

    local procedure IsDocumentEligibleForMatching(
        CustomSourceDoc: Record "Custom Source Document";
        DocHeader: Record "DXP SQZ Document Header"): Boolean
    begin
        // Implement business rules for document selection
        // For example:
        if CustomSourceDoc."Document Date" > DocHeader."Document Date" then
            exit(false);

        if CustomSourceDoc."Currency Code" <> DocHeader."Currency Code" then
            exit(false);

        // Check for open lines that can be matched
        if not HasOpenLinesToMatch(CustomSourceDoc) then
            exit(false);

        exit(true);
    end;

    local procedure HasOpenLinesToMatch(CustomSourceDoc: Record "Custom Source Document"): Boolean
    var
        CustomSourceLine: Record "Custom Source Line";
    begin
        CustomSourceLine.SetRange("Document No.", CustomSourceDoc."No.");
        CustomSourceLine.SetFilter("Outstanding Quantity", '>0');
        exit(not CustomSourceLine.IsEmpty());
    end;
//}

3. Processing of the documents found

After collecting the relevant document numbers, the actual comparison takes place:

local procedure ProcessMatchingDocuments(
    DocHeader: Record "DXP SQZ Document Header";
    OrderNoList: List of [Code[20]])
var
    TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary;
    OrderNo: Code[20];
begin
    foreach OrderNo in OrderNoList do begin
        // Get matching entries for current document
        GetMatchEntries(OrderNo, TempMatchEntry, DocHeader);
        
        if not TempMatchEntry.IsEmpty() then
            // Try to find and assign matching lines
            TryMatchDocumentLines(TempMatchEntry, DocHeader);
    end;
end;

local procedure GetMatchEntries(
    DocumentNo: Code[20];
    var TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary;
    DocHeader: Record "DXP SQZ Document Header")
var
    CustomSourceLine: Record "Custom Source Line";
begin
    TempMatchEntry.Reset();
    TempMatchEntry.DeleteAll();

    // Get lines from custom document
    CustomSourceLine.SetRange("Document No.", DocumentNo);
    
    if CustomSourceLine.FindSet() then
        repeat
            // Create match entry for each relevant line
            CreateMatchEntry(CustomSourceLine, TempMatchEntry);
        until CustomSourceLine.Next() = 0;
end;

local procedure CreateMatchEntry(
    CustomSourceLine: Record "Custom Source Line";
    var TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary)
begin
    TempMatchEntry.Init();
    TempMatchEntry."Document Type" := "DXP Order Match Document Type"::Custom;
    TempMatchEntry."Document No." := CustomSourceLine."Document No.";
    TempMatchEntry."Document Line No." := CustomSourceLine."Line No.";
    TempMatchEntry."No." := CustomSourceLine."Item No.";
    TempMatchEntry.Quantity := CustomSourceLine.Quantity;
    TempMatchEntry."Direct Unit Cost" := CustomSourceLine."Unit Price";
    TempMatchEntry."Line Amount" := CustomSourceLine.Amount;
    TempMatchEntry.Insert();
end;

The matching logic in detail

The actual reconciliation of document lines is performed according to defined business rules:

local procedure TryMatchDocumentLines(
    var TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary;
    DocHeader: Record "DXP SQZ Document Header")
var
    DocLine: Record "DXP SQZ Document Line";
    MatchSetup: Record "Custom Match Setup";
    HasCustomSetup: Boolean;
begin
    // Get configuration
    MatchSetup.Get();
    
    // Process each potential match entry
    if TempMatchEntry.FindSet() then
        repeat
            DocLine.Reset();
            DocLine.SetRange("Document No.", DocHeader."No.");
            DocLine.SetRange("Buy-from Vendor No.", TempMatchEntry."Buy-from Vendor No.");
            DocLine.SetFilter("Allocated Document Line No.", '%1', 0); // Only unallocated lines
            
            // Try matching strategies in order of precision
            if TryExactMatch(DocLine, TempMatchEntry) then
                CheckTolerancesAndAllocate(TempMatchEntry, DocLine, MatchSetup)
            else
                if TryItemReferenceMatch(DocLine, TempMatchEntry) then
                    CheckTolerancesAndAllocate(TempMatchEntry, DocLine, MatchSetup)
                else
                    if TryDescriptionMatch(DocLine, TempMatchEntry) then
                        CheckTolerancesAndAllocate(TempMatchEntry, DocLine, MatchSetup)
                    else
                        if TryBasicValuesMatch(DocLine, TempMatchEntry) then
                            CheckTolerancesAndAllocate(TempMatchEntry, DocLine, MatchSetup);
        until TempMatchEntry.Next() = 0;
end;

local procedure TryExactMatch(
    var DocLine: Record "DXP SQZ Document Line";
    TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary): Boolean
begin
    DocLine.SetRange(Type, TempMatchEntry.Type);
    DocLine.SetFilter("No.", '%1|%2', TempMatchEntry."No.", TempMatchEntry."Item Reference No.");
    DocLine.SetRange("SQZ Quantity", TempMatchEntry.Quantity);
    
    exit(not DocLine.IsEmpty());
end;

local procedure TryItemReferenceMatch(
    var DocLine: Record "DXP SQZ Document Line";
    TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary): Boolean
begin
    DocLine.SetRange(Type);  // Clear previous filters
    DocLine.SetRange("Item Reference No.", TempMatchEntry."Item Reference No.");
    
    exit(not DocLine.IsEmpty());
end;

local procedure TryDescriptionMatch(
    var DocLine: Record "DXP SQZ Document Line";
    TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary): Boolean
begin
    DocLine.SetRange("Item Reference No."); // Clear previous filter
    DocLine.SetFilter("SQZ Description", '@*' + TempMatchEntry.Description + '*');
    
    exit(not DocLine.IsEmpty());
end;

local procedure TryBasicValuesMatch(
    var DocLine: Record "DXP SQZ Document Line";
    TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary): Boolean
begin
    DocLine.SetRange("SQZ Description");  // Clear previous filter
    DocLine.SetRange("SQZ Quantity", TempMatchEntry.Quantity);
    DocLine.SetRange("SQZ Unit Price", TempMatchEntry."Direct Unit Cost");
    
    exit(not DocLine.IsEmpty());
end;

local procedure CheckTolerancesAndAllocate(
    TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary;
    var DocLine: Record "DXP SQZ Document Line";
    MatchSetup: Record "Custom Match Setup")
begin
    DocLine.FindFirst();  // We know there is at least one line
    
    if IsMatchWithinTolerances(DocLine, TempMatchEntry, MatchSetup) then
        AssignMatchData(DocLine, TempMatchEntry);
end;

local procedure IsMatchWithinTolerances(
    DocLine: Record "DXP SQZ Document Line";
    TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary;
    MatchSetup: Record "Custom Match Setup"): Boolean
var
    QtyTolerance: Decimal;
    AmountTolerance: Decimal;
begin
    QtyTolerance := MatchSetup."Quantity Tolerance";
    AmountTolerance := MatchSetup."Amount Tolerance";
    
    if Abs(DocLine."SQZ Quantity" - TempMatchEntry.Quantity) > QtyTolerance then
        exit(false);
        
    if Abs(DocLine."SQZ Unit Price" - TempMatchEntry."Direct Unit Cost") > AmountTolerance then
        exit(false);
        
    exit(true);
end;

local procedure AssignMatchData(
    var DocLine: Record "DXP SQZ Document Line";
    TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary)
begin
    // Assign the matched data to the document line
    DocLine.Validate("Allocated Document Type", "DXP Order Match Document Type"::Custom);
    DocLine.Validate("Allocated Document No.", TempMatchEntry."Document No.");
    DocLine.Validate("Allocated Document Line No.", TempMatchEntry."Document Line No.");
    DocLine.Validate("Allocated Quantity", TempMatchEntry.Quantity);
    DocLine.Validate("Allocated Unit Price", TempMatchEntry."Direct Unit Cost");
    DocLine.Validate("Allocated Line Amount", TempMatchEntry."Line Amount");
    DocLine.Validate("Allocated Line Discount %", TempMatchEntry."Line Discount %");
    DocLine.Modify(true);
end;

Implementation of a new, generic document class

SQUEEZE → Core → Your processing

This guide is deliberately focused and pipeline-oriented: It shows how document data is downloaded from SQUEEZE, processed in queues, and persisted in DEXPRO Core—and where a third-party extension comes into play.

Term: In this guide, “SQUEEZE app” refers to the Business Central extension that is connected to the SQUEEZE API and controls imports to DEXPRO Core.

Assumptions:


1) The end-to-end process SQUEEZE → Core

Once the document class and setup are configured, the SQUEEZE app's import pipeline typically runs as follows:

  1. DXP Document Class extend and bind your implementation: DXP ISource Document.
  2. Configure settings for this Document class‑Setup.
    • When validating DXP SQZ Document Class ID, the SQUEEZE app automatically retrieves the required setup values from SQUEEZE (e.g., DXP SQZ Export Interface ID and DXP SQZ Line Table ID) and fills them in.
  3. The SQUEEZE app job calls DXP SQZ API Mgt.DownloadDocumentData(). Can also be called via “Download documents”.
    • New entries are created in the DXP SQZ import queue (document ID + target client).
  4. The SQUEEZE app calls DXP SQZ Import Queue Mgt.TransferImportQueueEntries().
    • The orchestration code unit DXP Sqz Create Document is executed for the entries in the import queue.
  5. DXP SQZ API Mgt.CreateCoreAndSQUEEZEDocument(DocClass, DocumentId) loads the complete JSON.
    • DXP Document Mgt.AddDocument(DocClass, DocumentId, RawJson, …) is called up.
    • Result: A DXP Document‑Record exists. Status = Imported, JSON Raw is filled.
  6. The SQUEEZE app calls your implementation ISourceDocument.CreateSource(Document).
    • Your implementation begins here.

After step 6, you will have:


2) What you implement (Interfaces)

Source contract, i.e., squeeze page: DXP ISource Document

You implement this to:

Target contract, i.e., target document: DXP IDocument Processing

You implement this if Core is to create/update its own target data set (your own table/document) from the “Processed JSON.”


3) Step 1 — Define document class and assign source implementation

Create an enum extension and bind your source implementation:

enumextension 50100 "MY Document Class Ext." extends "DXP Document Class"
{
    value(50100; "MY Generic Document")
    {
        Caption = 'My Generic Document';
        Implementation = "DXP ISource Document" = "MY Source Document Impl.";
    }
}

4) Step 2 — Configure document class setup (so that the import queue is filled)

Configure the SQUEEZE-specific fields in the document class setup for your new document class.

Important:

Result: The SQUEEZE app can execute DownloadDocumentData() and create entries in the DXP SQZ Import Queue for your class.


5) Step 3 — Ensure field mapping (downloaded mapping + your default values)

The SQUEEZE app can download the field catalog of the connected Squeeze client and persist it in Core Mapping tables (per document class).

Your role as a document class implementer:

5.1 Implement InitFieldMapping (independent example)

codeunit 50101 "MY Source Document Impl." implements "DXP ISource Document"
{
    procedure InitFieldMapping(var HeaderMapping: Dictionary of [Text, Integer]; var LineMapping: Dictionary of [Text, Integer])
    var
        Header: Record "DXP SQZ Document Header";
        Line: Record "DXP SQZ Document Line";
    begin
        Clear(HeaderMapping);
        Clear(LineMapping);

        // Header fields
        HeaderMapping.Add('vendorNo', Header.FieldNo("Buy-from Vendor No."));
        HeaderMapping.Add('documentDate', Header.FieldNo("Document Date"));
        HeaderMapping.Add('postingDate', Header.FieldNo("Posting Date"));
        HeaderMapping.Add('reference', Header.FieldNo("Document Reference"));

        // Line fields
        LineMapping.Add('description', Line.FieldNo(Description));
        LineMapping.Add('quantity', Line.FieldNo(Quantity));
        LineMapping.Add('unitPrice', Line.FieldNo("Direct Unit Cost"));
    end;

    // other interface methods omitted here...
}

5.2 Validation switch when assigning fields: Validate app field

When the SQUEEZE app or your source creation logic writes JSON values to “app/source fields,” you can control whether the OnValidate trigger is executed.

In DXP Custom Field Mapping:

Use this, for example, if extension fields have business logic in OnValidate or if you want to assign deliberately trigger-free (higher performance).


6) Step 4 — Create Source data sets in CreateSource

CreateSource(Document) is called by the SQUEEZE app immediately after DXP Document Mgt.AddDocument(...) as soon as the core container data record exists.

Minimum responsibilities:

  1. Read DXP Document.JSON Raw.
  2. Create SQUEEZE‑Source‑Header/Lines (DXP SQZ Document Header / DXP SQZ Document Line).
  3. Connect Core‑document with SQUEEZE‑Source‑Header via UpdateDocumentAfterTransfer.

Optional (recommended if you want functionality equivalent to the standard SQUEEZE document classes): 4) Download attachments if DXP Download Attachments is active. 5) Perform automatic validation if enabled for the document.

6.1 Example: Create SQUEEZE‑Header + Lines with generic helpers

What the helpers do (so you don't have to implement it twice):

procedure CreateSource(Document: Record "DXP Document"): Boolean
var
    DocClassSetup: Record "DXP Document Class Setup";
    AutoValidationMgt: Codeunit "DXP Automatic Validation Mgt.";
    JsonHelper: Codeunit "DXP Json Helper";
    DocMgt: Codeunit "DXP Document Mgt.";
    SqzDocumentMgt: Codeunit "DXP SQZ Document Mgt.";
    SqzApiMgt: Codeunit "DXP SQZ API Mgt.";
    RawJson: JsonObject;
    DocHeader: Record "DXP SQZ Document Header";
    AutomaticValidation: Boolean;
begin
    if (not Document."JSON Raw".HasValue()) or (Document.Status <> Document.Status::Imported) then
        exit(false);

    // 1) Unverarbeitetes JSON lesen
    RawJson := JsonHelper.JObjectFromBlob(Document.RecordId(), Document.FieldNo("JSON Raw"));

    // 2) SQUEEZE‑Source‑Dokument mit den generischen Hilfsfunktionen erstellen
    //    (Feldzuordnung + optionales Validierungs‑Trigger‑Verhalten wird im Helper behandelt)
    SqzDocumentMgt.CreateSQUEEZEDocHeader(Document, DocHeader, RawJson);
    SqzDocumentMgt.CreateSQUEEZEDocLine(DocHeader, RawJson);

    // 3) Core → SQUEEZE‑Source‑Header verknüpfen
    DocMgt.UpdateDocumentAfterTransfer(Document."No.", Document.Status::Transferred, DocHeader."API Document ID", DocHeader.RecordId());

    // 4) Optional: Anhänge herunterladen und speichern
    //    (gesteuert über die Dokumentenklassen‑Einrichtung)
    if DocClassSetup.Get(DocHeader."Document Class") then
        if DocClassSetup."DXP Download Attachments" then
            SqzApiMgt.DownloadAndSaveAttachments(DocHeader);

    // 5) Optional: Automatische Validierung
    //    Wenn aktiviert, führt die Validierung die Plausibilitätsprüfungen aus und erstellt das „Processed JSON“.
    AutomaticValidation := AutoValidationMgt.AutomaticValidationActivated(DocHeader."No.");
    if AutomaticValidation then
        SqzDocumentMgt.ValidateSQUEEZEDocument(DocHeader, AutomaticValidation);

    exit(true);
end;

7) Step 5 — Validation + Handover „Processed JSON“

7.1 Validation: IsSourceDataPlausible

Use the Core Plausibility Framework to ensure that errors are displayed consistently:

procedure IsSourceDataPlausible(Source: Variant; var TempPlausibilityCheckEntry: Record "DXP Plausibility Check Entry" temporary): Boolean
var
    PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.";
    Header: Record "DXP SQZ Document Header";
begin
    if not Source.IsRecord then
        exit(false);

    Header := Source;
    TempPlausibilityCheckEntry.DeleteAll();

    PlausibilityCheck.FieldIsEmpty(Header, Header.FieldNo("Buy-from Vendor No."), 0, false, false);
    PlausibilityCheck.CheckPostingDate(Header."Posting Date", Header.FieldNo("Posting Date"));
    PlausibilityCheck.GetPlausibilityCheckEntries(TempPlausibilityCheckEntry);

    exit(TempPlausibilityCheckEntry.IsEmpty());
end;

7.2 Build „Processed JSON“ and pass it to the core: CreateProcessedJsonFromSource

Core stores “Processed JSON” in DXP Document.JSON Processed and transfers it to your target processing.

procedure CreateProcessedJsonFromSource(Source: Variant)
var
    CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
    DocMgt: Codeunit "DXP Document Mgt.";
    JsonHelper: Codeunit "DXP Json Helper";
    DocTransferMgt: Codeunit "DXP Document Transfer Mgt.";
    Header: Record "DXP SQZ Document Header";
    Line: Record "DXP SQZ Document Line";
    HeaderJObj: JsonObject;
    LinesJArr: JsonArray;
    LineJObj: JsonObject;
begin
    Header := Source;

    HeaderJObj.Add(CoreTokenMgt.GetVendorNoTok(), Header."Buy-from Vendor No.");
    HeaderJObj.Add(CoreTokenMgt.GetDocDateTok(), Header."Document Date");
    HeaderJObj.Add(CoreTokenMgt.GetDocReferenceTok(), Header."Document Reference");

    // Benutzerdefinierte Felder (Erweiterungsfelder) aus den Source‑Tabellen hinzufügen
    HeaderJObj.Add(CoreTokenMgt.GetCustomFieldsTok(), JsonHelper.Rec2JsonFieldArray(Header, true, true, Header."Document Class", Enum::"DXP Field Type"::Header));

    Line.SetRange("Document No.", Header."No.");
    if Line.FindSet() then
        repeat
            Clear(LineJObj);
            LineJObj.Add(CoreTokenMgt.GetDescriptionTok(), Line.Description);
            LineJObj.Add(CoreTokenMgt.GetQtyTok(), Line.Quantity);
            LineJObj.Add(CoreTokenMgt.GetCustomFieldsTok(), JsonHelper.Rec2JsonFieldArray(Line, true, true, Header."Document Class", Enum::"DXP Field Type"::Line));
            LinesJArr.Add(LineJObj);
        until Line.Next() = 0;

    DocTransferMgt.AddDocLinesToHeaderJson(HeaderJObj, LinesJArr);

    // An Core‑Zielverarbeitung übergeben
    DocMgt.UpdateDocument(
        Header."Core Document No.",
        Enum::"DXP Document Status"::Transferred,
        Enum::"DXP Target Document Process"::"MY Create Custom Document",
        Enum::"DXP Next process step"::"Standard document",
        HeaderJObj);
end;

8) Step 6 — Create your own target document

If you want Core to create your own target data set, extend DXP Target Document Process and implement DXP IDocument Processing:

enumextension 50102 "MY Target Doc Process Ext." extends "DXP Target Document Process"
{
    value(50102; "MY Create Custom Document")
    {
        Caption = 'Create My Custom Document';
        Implementation = "DXP IDocument Processing" = "MY Custom Doc Creation";
    }
}

codeunit 50103 "MY Custom Doc Creation" implements "DXP IDocument Processing"
{
    procedure ProcessStandardDocument(JObject: JsonObject): RecordId
    var
        CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
        DocTransferMgt: Codeunit "DXP Document Transfer Mgt.";
        JsonHelper: Codeunit "DXP Json Helper";
        MyDoc: Record "MY Custom Document";
        TargetVariant: Variant;
    begin
        MyDoc.Init();
        MyDoc.Validate("Vendor No.", JsonHelper.ValAsTxt(JObject, CoreTokenMgt.GetVendorNoTok(), false));
        MyDoc.Validate("Document Date", JsonHelper.ValAsDate(JObject, CoreTokenMgt.GetDocDateTok(), false));
        MyDoc.Insert(true);

        // Überträgt Metadaten sowie benutzerdefinierte/zusätzliche Felder anhand der Core‑Zuordnungen in den Ziel‑Datensatz.
        TargetVariant := MyDoc;
        DocTransferMgt.ProcessAdditionalInformation(JObject, TargetVariant);
        MyDoc := TargetVariant;

        exit(MyDoc.RecordId());
    end;
}

Note: The transfer of metadata and user-defined/additional fields is already included in the above example and is based on the core mapping tables.


9) Troubleshooting (quick checks)


10) What Core does automatically

When you call up DXP Document Mgt.UpdateDocument(...):

Custom Document Creation (Custom Processing)

 

Table of Contents

  1. Overview
  2. Understanding the Document Flow
  3. JSON Data Structure
  4. Quick Start: Simple Custom Processing

Overview

This guide explains how to create custom Business Central documents from JSON data provided by DEXPRO Core. The system receives documents with status "Custom Processing", which third-party developers can transform into any BC document type (Purchase Orders, Sales Orders, G/L Journals, Service Orders, etc.).

What You'll Learn


Understanding the Document Flow

Core Workflow

┌─────────────────────────────────────────────────────────────────┐
│ 1. Document Import                                              │
│    JSON Raw Data → DXP Document (Status: Imported)              │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. Source Document Creation                                     │
│    ISourceDocument.CreateSource() → Your Source Tables          │
│    (e.g., SQZ Document Header/Lines or Your Custom Tables)      │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. Validation & Plausibility Checks                             │
│    ISourceDocument.IsSourceDataPlausible()                      │
│    → Validates data quality, checks for errors                  │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. Create Processed JSON                                        │
│    ISourceDocument.CreateProcessedJsonFromSource()              │
│    → Builds JSON structure for target document                  │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. Update Core Document with "Custom Processing" Status         │
│    UpdateDocument() → Sets Status to "Custom Processing"        │
│    → Stores JSON Processed in DXP Document                      │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│ 6. Your Custom Processing (THIS IS WHERE YOU HOOK IN!)          │
│    Subscribe to events or implement interface                   │
│    → Read JSON Processed from DXP Document                      │
│    → Create your target BC document                             │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│ 7. Final Status Update                                          │
│    Document Status → Transferred or Finished                    │
│    Linked-to Record Id → Your created BC document               │
└─────────────────────────────────────────────────────────────────┘

Key Status Values

Status Description
Imported Raw JSON received, not yet processed
Transferred Source document created, ready for processing
Custom Processing Your target status - Document ready for custom handling
Finished Target document successfully created
Deleted Processing complete, document archived

JSON Data Structure

Overview

The "JSON Processed" blob in the DXP Document table contains a structured JSON object with all information needed to create your target document.

Complete JSON Structure

{
  // ═══════════════════════════════════════════════════════════
  // HEADER SECTION - Document-level information
  // ═══════════════════════════════════════════════════════════

  "Type": "Invoice",                    // Document type: Invoice, Credit Memo, Order, etc.
  "VendorNo": "VENDOR001",              // Vendor/Customer number
  "DocumentDate": "2025-10-15",         // Document date
  "PostingDate": "2025-10-16",          // Posting date
  "DocumentReference": "INV-2025-001",  // External document number
  "OrderNo": "PO-12345",                // Reference to order (if applicable)
  "PostingDescription": "Invoice Q4",   // Posting description
  "NetAmount": 1000.00,                 // Net amount (excluding tax)
  "TotalAmount": 1190.00,               // Gross amount (including tax)
  "TaxAmount": 190.00,                  // Total tax amount
  "Currency": "EUR",                    // Currency code
  "ResponsibilityCenter": "BERLIN",     // Responsibility center (blank: default from user setup / vendor)
  "VATBusPostingGroup": "EU",           // VAT business posting group (blank: vendor default) - the lines' default

  // ───────────────────────────────────────────────────────────
  // DIMENSIONS - Header-level dimensions as key-value pairs
  // ───────────────────────────────────────────────────────────

  "Dimensions": {
    "DEPARTMENT": "SALES",
    "PROJECT": "PROJ001",
    "COSTCENTER": "CC-100"
  },

  // ───────────────────────────────────────────────────────────
  // CUSTOM FIELDS - Additional header fields (see below)
  // ───────────────────────────────────────────────────────────

  "CustomFields": [
    {
      "Id": 50100,
      "Name": "CustomerNo",
      "Value": "CUST001",
      "Caption": "Customer Number"
    },
    {
      "Id": 50101,
      "Name": "DeliveryTerms",
      "Value": "EXW",
      "Caption": "Delivery Terms"
    }
  ],

  // ───────────────────────────────────────────────────────────
  // METADATA - System-generated field mappings
  // ───────────────────────────────────────────────────────────

  "Metadata": [
    {
      "DocumentClass": "DXP Invoice / Credit Memo",
      "FieldType": "Header",
      "FieldId": 101,
      "Value": "Additional Info"
    }
  ],

  // ═══════════════════════════════════════════════════════════
  // LINES SECTION - Document line items
  // ═══════════════════════════════════════════════════════════

  "Lines": [
    {
      // ─────────────────────────────────────────────────────────
      // Line Basic Information
      // ─────────────────────────────────────────────────────────

      "Type": "Item",                   // Item, G/L Account, Charge (Item), Fixed Asset
      "No": "ITEM001",                  // Item/Account number
      "Description": "Product A",       // Line description
      "VendorItemNo": "VEND-SKU-123",   // Vendor's item number
      "Quantity": 10.0,                 // Quantity
      "UnitOfMeasure": "PCS",          // Unit of measure
      "DirectUnitCost": 100.00,        // Unit price
      "LineDiscount": 5.0,             // Line discount percentage

      // ─────────────────────────────────────────────────────────
      // Line Amounts & Tax
      // ─────────────────────────────────────────────────────────

      "NetAmount": 950.00,             // Line net amount
      "TotalAmount": 1130.50,          // Line gross amount
      "TaxRate": 19.0,                 // Tax percentage
      "VATBusPostingGroup": "DOMESTIC",
      "VATProdPostingGroup": "STANDARD",
      "GenBusPostingGroup": "DOMESTIC",
      "GenProdPostingGroup": "RETAIL",

      // ─────────────────────────────────────────────────────────
      // Order Reference (for matching)
      // ─────────────────────────────────────────────────────────

      "OrderNo": "PO-12345",           // Referenced order number
      "OrderLineNo": 10000,            // Referenced order line number
      "ReceiptNo": "RCP-001",          // Referenced receipt number
      "ReceiptLineNo": 10000,          // Referenced receipt line number

      // ─────────────────────────────────────────────────────────
      // Line Dimensions
      // ─────────────────────────────────────────────────────────

      "Dimensions": {
        "DEPARTMENT": "PROD",
        "PROJECT": "PROJ001"
      },

      // ─────────────────────────────────────────────────────────
      // Line Custom Fields
      // ─────────────────────────────────────────────────────────

      "CustomFields": [
        {
          "Id": 50200,
          "Name": "SerialNo",
          "Value": "SN-12345",
          "Caption": "Serial Number"
        }
      ],

      // ─────────────────────────────────────────────────────────
      // Line Metadata
      // ─────────────────────────────────────────────────────────

      "Metadata": [
        {
          "DocumentClass": "DXP Invoice / Credit Memo",
          "FieldType": "Line",
          "FieldId": 201,
          "Value": "Line-specific metadata"
        }
      ]
    }
    // ... additional lines
  ]
}

Custom Fields vs. Metadata

Custom Fields:

Metadata:


Quick Start: Simple Custom Processing

Scenario

You want to create a custom document type (e.g., a Warehouse Shipment) from documents marked with "Custom Processing" status.

Step 1: Subscribe to the Integration Event

Create a codeunit to intercept documents with "Custom Processing" status:

codeunit 50100 "My Custom Document Handler"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Mgt.", 
        'OnAfterWriteProcessedJsonToBlob', '', true, true)]
    local procedure OnAfterWriteProcessedJsonToBlob(
        var Document: Record "DXP Document"; 
        var ProcessedJSONObj: JsonObject)
    begin
        // Only handle Custom Processing status
        if Document.Status <> Document.Status::"Custom Processing" then
            exit;

        // Create your custom document
        CreateMyCustomDocument(Document, ProcessedJSONObj);
    end;

    local procedure CreateMyCustomDocument(
        var Document: Record "DXP Document"; 
        ProcessedJSON: JsonObject)
    var
        CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
        JsonHelper: Codeunit "DXP Json Helper";
        MyCustomHeader: Record "My Custom Document Header";
        MyCustomLine: Record "My Custom Document Line";
        LineJArray: JsonArray;
        LineJToken: JsonToken;
        LineJObj: JsonObject;
    begin
        // ═══════════════════════════════════════════════════════
        // 1. CREATE HEADER
        // ═══════════════════════════════════════════════════════

        MyCustomHeader.Init();
        MyCustomHeader."No." := '';  // Will be assigned by number series

        // Read standard header fields
        MyCustomHeader."Vendor No." := 
            JsonHelper.ValAsTxt(ProcessedJSON, CoreTokenMgt.GetVendorNoTok(), false);
        MyCustomHeader."Document Date" := 
            JsonHelper.ValAsDate(ProcessedJSON, CoreTokenMgt.GetDocDateTok(), false);
        MyCustomHeader."Document Reference" := 
            JsonHelper.ValAsTxt(ProcessedJSON, CoreTokenMgt.GetDocReferenceTok(), false);

        MyCustomHeader.Insert(true);

        // ═══════════════════════════════════════════════════════
        // 2. PROCESS DIMENSIONS (if your document supports them)
        // ═══════════════════════════════════════════════════════

        ProcessHeaderDimensions(ProcessedJSON, MyCustomHeader);

        // ═══════════════════════════════════════════════════════
        // 3. PROCESS CUSTOM FIELDS
        // ═══════════════════════════════════════════════════════

        ProcessCustomFields(ProcessedJSON, MyCustomHeader);

        // ═══════════════════════════════════════════════════════
        // 4. CREATE LINES
        // ═══════════════════════════════════════════════════════

        LineJArray := JsonHelper.ReadJArrayFromObj(ProcessedJSON, CoreTokenMgt.GetLinesTok());

        foreach LineJToken in LineJArray do begin
            LineJObj := LineJToken.AsObject();

            MyCustomLine.Init();
            MyCustomLine."Document No." := MyCustomHeader."No.";
            MyCustomLine."Line No." := GetNextLineNo(MyCustomHeader."No.");

            // Read line fields
            MyCustomLine."Item No." := 
                JsonHelper.ValAsTxt(LineJObj, CoreTokenMgt.GetNoTok(), false);
            MyCustomLine.Description := 
                JsonHelper.ValAsTxt(LineJObj, CoreTokenMgt.GetDescriptionTok(), false);
            MyCustomLine.Quantity := 
                JsonHelper.ValAsDec(LineJObj, CoreTokenMgt.GetQtyTok(), false);
            MyCustomLine."Unit of Measure" := 
                JsonHelper.ValAsTxt(LineJObj, CoreTokenMgt.GetUoMTok(), false);

            MyCustomLine.Insert(true);

            // Process line dimensions and custom fields if needed
            ProcessLineDimensions(LineJObj, MyCustomLine);
            ProcessLineCustomFields(LineJObj, MyCustomLine);
        end;

        // ═══════════════════════════════════════════════════════
        // 5. UPDATE CORE DOCUMENT STATUS
        // ═══════════════════════════════════════════════════════

        UpdateCoreDocument(Document, MyCustomHeader);
    end;

    local procedure ProcessHeaderDimensions(ProcessedJSON: JsonObject; var MyCustomHeader: Record "My Custom Document Header")
    var
        CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
        DocTransferMgt: Codeunit "DXP Document Transfer Mgt.";
        DimensionsJObj: JsonObject;
        DimSetID: Integer;
    begin
        // Read dimensions from JSON
        if ProcessedJSON.Contains(CoreTokenMgt.GetDimensionsTok()) then begin
            ProcessedJSON.Get(CoreTokenMgt.GetDimensionsTok(), DimensionsJObj);

            // Convert to Dimension Set ID
            if DocTransferMgt.GetDimSetIdFromJsonObj(DimensionsJObj, DimSetID) then begin
                MyCustomHeader."Dimension Set ID" := DimSetID;
                MyCustomHeader.Modify(true);
            end;
        end;
    end;

    local procedure ProcessCustomFields(ProcessedJSON: JsonObject; var MyCustomHeader: Record "My Custom Document Header")
    var
        CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
        JsonHelper: Codeunit "DXP Json Helper";
        CustomFieldsJArray: JsonArray;
        CustomFieldJToken: JsonToken;
        CustomFieldJObj: JsonObject;
        FieldName: Text;
        FieldValue: Text;
    begin
        // Read custom fields array
        if not ProcessedJSON.Contains(CoreTokenMgt.GetCustomFieldsTok()) then
            exit;

        CustomFieldsJArray := JsonHelper.ReadJArrayFromObj(ProcessedJSON, CoreTokenMgt.GetCustomFieldsTok());

        foreach CustomFieldJToken in CustomFieldsJArray do begin
            CustomFieldJObj := CustomFieldJToken.AsObject();
            FieldName := JsonHelper.ValAsTxt(CustomFieldJObj, CoreTokenMgt.GetNameTok(), false);
            FieldValue := JsonHelper.ValAsTxt(CustomFieldJObj, CoreTokenMgt.GetValueTok(), false);

            // Map to your custom fields
            case FieldName of
                'CustomerNo':
                    MyCustomHeader."Customer No." := CopyStr(FieldValue, 1, 20);
                'DeliveryTerms':
                    MyCustomHeader."Delivery Terms" := CopyStr(FieldValue, 1, 10);
                // Add more field mappings as needed
            end;
        end;

        MyCustomHeader.Modify(true);
    end;

    local procedure ProcessLineDimensions(LineJObj: JsonObject; var MyCustomLine: Record "My Custom Document Line")
    var
        CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
        DocTransferMgt: Codeunit "DXP Document Transfer Mgt.";
        DimensionsJObj: JsonObject;
        DimSetID: Integer;
    begin
        if LineJObj.Contains(CoreTokenMgt.GetDimensionsTok()) then begin
            LineJObj.Get(CoreTokenMgt.GetDimensionsTok(), DimensionsJObj);

            if DocTransferMgt.GetDimSetIdFromJsonObj(DimensionsJObj, DimSetID) then begin
                MyCustomLine."Dimension Set ID" := DimSetID;
                MyCustomLine.Modify(true);
            end;
        end;
    end;

    local procedure ProcessLineCustomFields(LineJObj: JsonObject; var MyCustomLine: Record "My Custom Document Line")
    var
        CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
        JsonHelper: Codeunit "DXP Json Helper";
        CustomFieldsJArray: JsonArray;
        CustomFieldJToken: JsonToken;
        CustomFieldJObj: JsonObject;
        FieldName: Text;
        FieldValue: Text;
    begin
        if not LineJObj.Contains(CoreTokenMgt.GetCustomFieldsTok()) then
            exit;

        CustomFieldsJArray := JsonHelper.ReadJArrayFromObj(LineJObj, CoreTokenMgt.GetCustomFieldsTok());

        foreach CustomFieldJToken in CustomFieldsJArray do begin
            CustomFieldJObj := CustomFieldJToken.AsObject();
            FieldName := JsonHelper.ValAsTxt(CustomFieldJObj, CoreTokenMgt.GetNameTok(), false);
            FieldValue := JsonHelper.ValAsTxt(CustomFieldJObj, CoreTokenMgt.GetValueTok(), false);

            // Map to your line custom fields
            case FieldName of
                'SerialNo':
                    MyCustomLine."Serial No." := CopyStr(FieldValue, 1, 50);
                // Add more field mappings
            end;
        end;

        MyCustomLine.Modify(true);
    end;

    local procedure UpdateCoreDocument(var Document: Record "DXP Document"; MyCustomHeader: Record "My Custom Document Header")
    var
        DocumentMgt: Codeunit "DXP Document Mgt.";
    begin
        // Update the core document to link it to your created document
        Document.Status := Document.Status::Transferred;
        Document."Linked-to Record Id" := MyCustomHeader.RecordId;
        Document.Modify(true);

        // Optionally, transfer attachments from Core to your document
        TransferAttachments(Document, MyCustomHeader);
    end;

    local procedure TransferAttachments(Document: Record "DXP Document"; MyCustomHeader: Record "My Custom Document Header")
    var
        DocAttachment: Record "DXP Document Attachment";
        MyDocAttachment: Record "Document Attachment";
        InStr: InStream;
    begin
        DocAttachment.SetRange("Document No.", Document."No.");
        if DocAttachment.FindSet() then
            repeat
                MyDocAttachment.Init();
                MyDocAttachment.ID := 0;

                DocAttachment."File Content".CreateInStream(InStr);
                MyDocAttachment.SaveAttachmentFromStream(
                    InStr, 
                    MyCustomHeader.RecordId, 
                    DocAttachment."File Name");
            until DocAttachment.Next() = 0;
    end;

    local procedure GetNextLineNo(DocumentNo: Code[20]): Integer
    var
        MyCustomLine: Record "My Custom Document Line";
    begin
        MyCustomLine.SetRange("Document No.", DocumentNo);
        if MyCustomLine.FindLast() then
            exit(MyCustomLine."Line No." + 10000);
        exit(10000);
    end;
}

Key Helper Codeunits

Codeunit Purpose
DXP Core Token Mgt. Provides token names for JSON fields (GetVendorNoTok(), GetDocDateTok(), etc.)
DXP Json Helper JSON parsing utilities (ValAsTxt(), ValAsDate(), ReadJArrayFromObj(), etc.)
DXP Document Transfer Mgt. Dimension processing, metadata transfer, custom field handling
DXP Document Mgt. Core document management (UpdateDocument(), UpdateDocumentStatus(), etc.)

For questions or clarifications, please contact DEXPRO Solutions GmbH.

Rule-based next process step: routing documents to Custom Processing

Audience

This guide is intended for developers and implementers who use DEXPRO SQUEEZE in Business Central and want to control, before the actual "Custom Processing" implementation, which documents should switch to the "Custom Processing" status after validation.

Context (comes before the existing Custom Processing guide)

With this configuration you ensure that a document (depending on rules) is given the "Custom Processing" status. From that point on, the flow for custom processing described in the existing documentation takes over (creating target documents from "JSON Processed").

Further reading (next step, after this documentation):

UI terms used

The following terms correspond to the field captions used in the app:

Technical background

Important:

Prerequisites

Step by step: creating and linking the "DXP Sqz Next Step Rule Group" rule group

1) Open the document class setup

  1. Open the Document Class Setup page.
  2. Select the document class for which you want to enable routing to "Custom Processing".
  3. Scroll to the Squeeze area → Rules group.

{add screenshot of Document Class Setup - Squeeze Rules here}

2) Create the rule group

  1. In Squeeze → Rules, select the "Next Process Step" field (technically: DXP Sqz Next Step Rule Group).
  2. Open the lookup (rule group list).
  3. Create a new rule group:
    • Rule Group Code: for example Next Step Rules
    • Description: for example "Routing to Custom Processing"

Note: The lookup is already filtered to the matching rule set code by the document class. You do not need to search for the rule set manually.

{add screenshot of rule group lookup here}

3) Save the rule group in the document class setup

  1. Make sure that the "Next Process Step" field in the document class setup now contains the rule group code DXP Sqz Next Step Rule Group.

{add screenshot of document class setup with rule group code here}

Step by step: creating a rule that routes to "Custom Processing"

4) Open the rules of the rule group

  1. Open the rule group DXP Sqz Next Step Rule Group you just created.
  2. Choose the "Show Rules" action (opens the rules of this rule group).

{add screenshot of Show Rules action here}

5) Create a new rule

  1. Create a new rule.
  2. Set a meaningful priority (lower value = higher priority, if your environment is configured that way).
  3. Define the conditions (rule conditions) that must match.

[Best Practice] Start with a very clear, testable condition (for example based on document type, vendor, amount thresholds, company, etc.) before you build complex rule chains.

{add screenshot of new rule here}

6) Set the SQUEEZE-specific fields (decisive for "Custom Processing")

On the rule card there is a "DEXPRO Squeeze" area (CardPart). Set the following there:

  1. Next Process Step = "Standard document"
  2. Custom Processing = Yes

Result:

{add screenshot of rule card DEXPRO Squeeze area here}

Optional:

Checks / troubleshooting

The document does not end up in "Custom Processing"

Work through these points:

  1. Is the rule group stored? The rule group code must be set in the document class setup (otherwise the system falls back to the default "Next Process Step").
  2. Does a rule match? If no rule matches, the next process step is not overwritten.
  3. Is "Custom Processing" set AND Next Step = "Standard document"?
    • If "Custom Processing" = Yes but Next Step ≠ "Standard document", the status is not set to "Custom Processing".
  4. Can you not see the "DEXPRO Squeeze" area on the rule card?
    • Then you are most likely in a rule set that does not belong to the document class setup (source table).
    • It is best to create the rule group via the lookup from the document class setup. This saves you the detour via the general rule management.

The document ends up in "Done" instead of "Custom Processing"

If a rule matches and Next Process Step is set to a value other than "Standard document", the status may be set to "Done" (which skips further processing). Therefore check the Next Step selection in the rule. This is the case, for example, when the Breeze Interface app is used.

Afterwards (transition into the existing documentation on custom document creation)

As soon as a document is in the "Custom Processing" status, it is ready for processing:

Embedding the SQUEEZE Viewer part in your own pages WIP

Diese Anleitung beschreibt, wie Sie den SQUEEZE Viewer in beliebige Seiten einbetten (eigene Pages oder Page Extensions) – über den wiederverwendbaren Viewer‑Part:

Der Viewer‑Part hostet die SQUEEZE Control Add-ins und lädt die Preview‑URL anhand des aktuellen Datensatzes DXP SQZ Document Header.

Referenz‑Implementierung, die in dieser App bereits enthalten ist:

Die Pages können Sie benutzen und mit ihren Feldern erweitern. Die Page "DXP SQZ Document (Generic)" verfügt über einen Viewer in der Factbox und die wichtigsten Actions.


1) Voraussetzungen

  1. Ihre Extension muss von der DEXPRO SQUEEZE App abhängig sein (damit Page/Control Add-ins als Symbole verfügbar sind).

  2. Der Host‑Datensatz muss eine Möglichkeit bieten, einen DXP SQZ Document Header Datensatz zu referenzieren.

    • Die SourceTable des Viewer‑Parts ist fest auf DXP SQZ Document Header.
    • Deshalb muss Ihre Seite ein Feld haben (oder bereitstellen), das sich in SubPageLink verwenden lässt, um den Header zu finden. Alternativ kann der Record in dem Viewer-Part gesetzt werden - Beispiele folgen weiter unten.
  3. SQUEEZE muss aktiviert und konfiguriert sein (Setup + API Key). Der Viewer‑Part lädt absichtlich nicht, wenn das Modul nicht aktiviert ist.


2) Empfohlenes Einbettungsmuster

2.1 Als FactBox einbetten (empfohlen)

Für die meisten Szenarien (Card Pages, List Pages) platzieren Sie den Viewer in area(FactBoxes).

Wenn die SourceTable Ihrer Seite DXP SQZ Document Header ist

layout
{
    area(FactBoxes)
    {
        part(SQZViewer; "DXP SQZ Viewer Part")
        {
            ApplicationArea = All;
            Caption = 'Viewer';

            // Link the FactBox part to the current SQZ header
            SubPageLink = "No." = field("No.");
        }
    }
}

Wenn die SourceTable Ihrer Seite NICHT DXP SQZ Document Header ist

Sie können den Viewer‑Part trotzdem einbetten, sofern Ihr Datensatz ein Feld enthält, das die SQUEEZE Header‑Nummer speichert.

Beispiel: Ihr Datensatz hat ein Feld "SQZ Document No." : Code[20]

layout
{
    area(FactBoxes)
    {
        part(SQZViewer; "DXP SQZ Viewer Part")
        {
            ApplicationArea = All;
            Caption = 'Viewer';

            // Link the viewer part’s "No." to your field
            SubPageLink = "No." = field("SQZ Document No.");
        }
    }
}

Wichtig:

2.2 Viewer dynamisch binden (ohne SubPageLink)

Wenn Ihre Host‑Seite keinen einfachen SubPageLink anbieten kann (z. B. weil es sich um eine Zielbeleg‑Seite wie Purchase Invoice handelt und dort die SQUEEZE Belegnummer nicht gespeichert ist), können Sie den Viewer‑Part trotzdem einbetten und im Code binden.

Konzept:

Warum SetSqzRecord?

Beispiel‑Gerüst:

layout
{
    area(FactBoxes)
    {
        part(SQZViewer; "DXP SQZ Viewer Part")
        {
            ApplicationArea = All;
            Caption = 'Viewer';
        }
    }
}

trigger OnAfterGetCurrRecord()
var
    SqzHeader: Record "DXP SQZ Document Header";
begin
    if TryResolveSqzHeaderForCurrentRecord(SqzHeader) then
        CurrPage.SQZViewer.Page.SetSqzRecord(SqzHeader);
end;

Dieses Pattern ist besonders für Third‑party Pages hilfreich, weil es vermeidet, „SQUEEZE Document No.“ als Tabellenfeld persistieren zu müssen.


3) Viewer‑Modus (Belegübersicht / Positionsfokus / Abgedockt)

Wenn der Viewer als FactBox eingebettet ist, sind Felder typischerweise nicht editierbar. Deshalb bietet der Viewer‑Part Actions, um den Viewer‑Modus zu ändern.

Was beim Wechsel des Modus passiert:

Für Third‑party Entwickler:


4) Optional, aber wichtig: Zeilen‑Highlighting aktivieren (MarkRow)

Wenn Sie eine Lines‑Subform haben und die Benutzererfahrung „Zeile klicken → Viewer markiert Koordinaten“ wünschen, muss Ihre Lines‑Page Messages über dieselbe Viewer‑Resident‑Instanz posten, die der Viewer‑Part initialisiert.

Warum das wichtig ist:

4.1 Pattern (Host‑Page verdrahtet Viewer‑Resident in Lines)

Annahmen:

Das ist ein expliziter Vertrag/Annahme für interaktives Line‑Highlighting.

Ergänzen Sie das auf Ihrer Header‑Page:

trigger OnAfterGetCurrRecord()
var
    SharedResident: Codeunit "DXP SQUEEZE Viewer Resident";
begin
    CurrPage.SQZViewer.Page.GetViewerResident(SharedResident);
    CurrPage.DocumentLines.Page.SetViewerResident(SharedResident);
end;

Hinweise:

4.2 Was die Lines‑Subform tun muss

Ihre Lines‑Page sollte MarkRow() in OnAfterGetCurrRecord() aufrufen und MarkRow() ähnlich zur Standard‑Seite implementieren:

Das ist die Mindestanforderung für interaktives Highlighting.

4.3 Referenz‑Implementierung: minimale Lines‑Subform mit MarkRow

Das ist ein bekannt funktionierendes Minimal‑Pattern (basierend auf der Standard SQUEEZE Lines‑Subform). Third‑party Entwickler können es kopieren und Felder/Repeater‑Spalten nach Bedarf anpassen:

page 70954672 "DXP SQZ Doc. Generic Subform"
{
    Caption = 'Lines';
    PageType = ListPart;
    AutoSplitKey = true;
    DelayedInsert = true;
    SourceTable = "DXP SQZ Document Line";
    RefreshOnActivate = true;

    layout
    {
        area(content)
        {
            repeater(General)
            {
                field("Line No."; Rec."Line No.")
                {
                    ApplicationArea = All;
                    Visible = false;
                }
                // Add your visible SQZ fields here...
            }
        }
    }

    trigger OnAfterGetCurrRecord()
    begin
        MarkRow();
    end;

    var
        ApiMgt: Codeunit "DXP SQZ API Mgt.";
        Resident: Codeunit "DXP SQUEEZE Viewer Resident";
        MarkedLineNo: Integer;

    procedure SetViewerResident(var NewResident: Codeunit "DXP SQUEEZE Viewer Resident")
    begin
        Resident := NewResident;
        MarkedLineNo := 0;
    end;

    local procedure MarkRow()
    var
        DocHeader: Record "DXP SQZ Document Header";
        LineCoordMgt: Codeunit "DXP SQZ Coordinates Mgt.";
        ViewerState: Codeunit "DXP Viewer State";
    begin
        if MarkedLineNo = Rec."Line No." then
            exit;

        if not LineCoordMgt.CoordinateExists(Rec.RecordId) then
            exit;

        // Detached viewer: update the state table only
        if IsExternalViewerActive() then begin
            if DocHeader.Get(Rec."Document No.") then
                ViewerState.UpdateViewerStateWithCoordinates(DocHeader."API Document ID", Rec.RecordId);
        end else
            // Internal viewer: post message to the control add-in instance via the shared resident
            ApiMgt.PostMessage(ApiMgt.MarkFieldByCoordinatesObj(Rec.RecordId), Resident);

        MarkedLineNo := Rec."Line No.";
    end;

    local procedure IsExternalViewerActive(): Boolean
    var
        ViewerState: Codeunit "DXP Viewer State";
    begin
        exit(ViewerState.GetViewerMode() = "DXP SQZ Viewer Mode"::Detached);
    end;
}

5) Fehlersuche (Viewer bleibt leer)

5.1 Der Viewer lädt nie eine Preview

Prüfe zuerst:

5.2 FactBox ist sichtbar, aber Control Add-in initialisiert nicht

5.3 MarkRow hebt nichts hervor


6) Minimales Beispiel: Viewer in eine eigene Page Extension einbauen

pageextension 50100 "MY SQZ Doc Ext." extends "DXP SQZ Document (Generic)"
{
    layout
    {
        addfirst(FactBoxes)
        {
            part(MySQZViewer; "DXP SQZ Viewer Part")
            {
                ApplicationArea = All;
                Caption = 'Viewer';
                SubPageLink = "No." = field("No.");
            }
        }
    }
}

7) Hinweise / Einschränkungen


8) Beispiel: Viewer auf einem Zielbeleg anzeigen (Purchase Invoice)

Dieser Abschnitt adressiert ein häufiges Third‑party Szenario:

Annahmen:

8.1 Core‑Beleg aus dem Ziel‑Datensatz auflösen

local procedure TryGetCoreDocumentForTarget(TargetRecId: RecordId; var CoreDoc: Record "DXP Document"): Boolean
begin
    CoreDoc.Reset();
    CoreDoc.SetRange("Linked-to Record Id", TargetRecId);
    exit(CoreDoc.FindFirst());
end;

8.2 SQUEEZE Header aus dem Core‑Beleg auflösen

local procedure TryGetSqzHeaderForCore(CoreDocNo: Code[20]; var SqzHeader: Record "DXP SQZ Document Header"): Boolean
begin
    SqzHeader.Reset();
    SqzHeader.SetRange("Core Document No.", CoreDocNo);
    exit(SqzHeader.FindFirst());
end;

8.3 Viewer einbetten (dynamisches Binden)

Beispiel: Purchase Invoice erweitern (konzeptionell; Basis‑Page‑Name ggf. an Ihre Umgebung anpassen).

Diese Implementierung verwendet:

pageextension 50100 "MY Purch. Invoice Viewer" extends "Purchase Invoice"
{
    layout
    {
        addfirst(FactBoxes)
        {
            part(DXPSQZViewer; "DXP SQZ Viewer Part")
            {
                ApplicationArea = All;
                Caption = 'SQUEEZE Viewer';
                Visible = ShowViewer;
            }
        }
    }

    var
        ShowViewer: Boolean;

    trigger OnAfterGetCurrRecord()
    var
        CoreDoc: Record "DXP Document";
        SqzHeader: Record "DXP SQZ Document Header";
        SharedResident: Codeunit "DXP SQUEEZE Viewer Resident";
        ViewerCtx: Codeunit "DXP SQZ Purch. Inv. Viewer Ctx";
    begin
        ShowViewer := false;
        if not TryGetCoreDocumentForTarget(Rec.RecordId, CoreDoc) then
            exit;

        if not TryGetSqzHeaderForCore(CoreDoc."No.", SqzHeader) then
            exit;

        ShowViewer := true;

        CurrPage.DXPSQZViewer.Page.SetSqzRecord(SqzHeader);

        CurrPage.DXPSQZViewer.Page.GetViewerResident(SharedResident);
        ViewerCtx.SetResident(SharedResident);
        ViewerCtx.SetSqzContextForPurchInv(Rec."No.", SqzHeader."No.", SqzHeader."API Document ID");
    end;

    local procedure TryGetCoreDocumentForTarget(TargetRecId: RecordId; var CoreDoc: Record "DXP Document"): Boolean
    begin
        CoreDoc.Reset();
        CoreDoc.SetRange("Linked-to Record Id", TargetRecId);
        exit(CoreDoc.FindFirst());
    end;

    local procedure TryGetSqzHeaderForCore(CoreDocNo: Code[20]; var SqzHeader: Record "DXP SQZ Document Header"): Boolean
    begin
        SqzHeader.Reset();
        SqzHeader.SetRange("Core Document No.", CoreDocNo);
        exit(SqzHeader.FindFirst());
    end;
}

Zugehöriges Context‑Codeunit (speichert Resident + SQZ‑Kontext, damit die Subform korrekt highlighten kann):

codeunit 50101 "MY SQZ Purch. Inv. Viewer Ctx"
{
    SingleInstance = true;

    procedure SetResident(var NewResident: Codeunit "DXP SQUEEZE Viewer Resident")
    begin
        Resident := NewResident;
        HasResident := true;
    end;

    procedure TryGetResident(var OutResident: Codeunit "DXP SQUEEZE Viewer Resident"): Boolean
    begin
        if not HasResident then
            exit(false);

        OutResident := Resident;
        exit(true);
    end;

    procedure SetSqzContextForPurchInv(PurchInvNo: Code[20]; SqzDocNo: Code[20]; ApiDocumentId: Text[50])
    begin
        PurchInvToSqzDocNo.Set(PurchInvNo, SqzDocNo);
        PurchInvToApiDocumentId.Set(PurchInvNo, ApiDocumentId);
    end;

    procedure TryGetSqzContextForPurchInv(PurchInvNo: Code[20]; var SqzDocNo: Code[20]; var ApiDocumentId: Text[50]): Boolean
    begin
        if not PurchInvToSqzDocNo.Get(PurchInvNo, SqzDocNo) then
            exit(false);

        PurchInvToApiDocumentId.Get(PurchInvNo, ApiDocumentId);
        exit(true);
    end;

    var
        Resident: Codeunit "DXP SQUEEZE Viewer Resident";
        HasResident: Boolean;
        PurchInvToSqzDocNo: Dictionary of [Code[20], Code[20]];
        PurchInvToApiDocumentId: Dictionary of [Code[20], Text[50]];
}

8.4 Wichtiger Hinweis: SQUEEZE Header‑Status nach Verarbeitung

Sie haben ein häufiges Setup beschrieben, in dem der SQUEEZE Header nach dem Erstellen des Zielbelegs noch existiert, aber Status = Deleted ist.

Der Viewer‑Part kann die Preview trotzdem laden, solange "API Document ID" vorhanden ist und SQUEEZE aktiviert ist.

Optionen:

Welche Option korrekt ist, hängt von Ihren Prozessanforderungen ab.


9) Sonderfall: Hervorheben aus der Purchase Invoice Subform (Purchase Line → SQZ line)

Manchmal mächten Sie diese Benutzererfahrung auf einer Zielbeleg‑Seite:

In dieser Situation basiert Ihre Lines‑Page nicht auf "DXP SQZ Document Line", daher können Sie MarkFieldByCoordinatesObj(Rec.RecordId) nicht direkt aufrufen.

Stattdessen müssen Sie die passende SQUEEZE Line auflösen und danach mit der RecordId dieser SQUEEZE Line highlighten.

9.1 Zwei mögliche Verknüpfungsstrategien

Es gibt zwei realistische Wege, eine Einkaufszeile wieder auf eine SQUEEZE Line zu mappen:

  1. Expliziter Allocation‑Link (nur in manchen Szenarien verfügbar)

    • Verwendet Felder auf "DXP SQZ Document Line":
      • "Allocated Document Type" (enum "DXP Order Match Document Type")
      • "Allocated Document No."
      • "Allocated Document Line No."
  2. Heuristischer Match nach Inhalt (empfohlen als Fallback)

    • Verwendet Felder, die typischerweise auf beiden Seiten existieren (Type/No/Quantity/Unit Cost/Line Amount/Description).
    • Das funktioniert auch dann, wenn keine Allocation‑Beziehung gepflegt wird.
    • Es ist nicht mathematisch perfekt (Duplikate sind möglich), daher sollten Matching‑Regeln auf Ihren Use Case angepasst werden.

9.2 Empfohlenes Wiring (Host‑Page übergibt SQZ‑Kontext + Resident)

Sie benötigen in der Purchase‑Lines‑Subpage zwei Dinge:

  1. Den Viewer‑Resident (damit PostMessage(...) die richtige Control‑Add‑in Instanz trifft)
  2. Den aktuellen SQZ Header‑Kontext (damit Sie SQZ Lines auf das richtige Dokument filtern)

Empfohlenes Pattern auf der Host‑Page (nachdem Sie SqzHeader aufgelöst haben):

trigger OnAfterGetCurrRecord()
var
    SharedResident: Codeunit "DXP SQUEEZE Viewer Resident";
    SqzHeader: Record "DXP SQZ Document Header";
begin
    if not TryResolveSqzHeaderForCurrentRecord(SqzHeader) then
        exit;

    CurrPage.DXPSQZViewer.Page.SetSqzRecord(SqzHeader);

    CurrPage.DXPSQZViewer.Page.GetViewerResident(SharedResident);
    ViewerCtx.SetResident(SharedResident);
    ViewerCtx.SetSqzContextForPurchInv(Rec."No.", SqzHeader."No.", SqzHeader."API Document ID");
end;

9.3 Minimales Beispiel: Purch. Invoice Subform triggert Highlighting im Viewer

Dieses Beispiel erweitert Purch. Invoice Subform. Beachten Sie: der Datensatz der Basis‑Subform ist ein Record "Purchase Line", daher müssen die Resolver‑Prozeduren Record "Purchase Line" akzeptieren.

Das hier gezeigte Allocation‑Mapping ist die funktionierende Implementierung für Purchase‑Invoice‑Szenarien: es verknüpft über Receipt ("Receipt No." / "Receipt Line No.") statt über Belegnummer/Zeilennummer der Rechnung.

pageextension 50101 "MY Purch. Inv. Sf Mark" extends "Purch. Invoice Subform"
{
    trigger OnAfterGetCurrRecord()
    begin
        MarkSqzLineForPurchInvLine();
    end;

    var
        ApiMgt: Codeunit "DXP SQZ API Mgt.";
        Resident: Codeunit "DXP SQUEEZE Viewer Resident";
        ViewerState: Codeunit "DXP Viewer State";
        LineCoordMgt: Codeunit "DXP SQZ Coordinates Mgt.";
        ViewerCtx: Codeunit "DXP SQZ Purch. Inv. Viewer Ctx";
        SqzDocNo: Code[20];
        ApiDocumentId: Text[50];

    local procedure MarkSqzLineForPurchInvLine()
    var
        SqzLine: Record "DXP SQZ Document Line";
    begin
        if not ViewerCtx.TryGetSqzContextForPurchInv(Rec."Document No.", SqzDocNo, ApiDocumentId) then
            exit;

        if not ViewerCtx.TryGetResident(Resident) then
            exit;

        if not TryResolveSqzLineForPurchLine(SqzDocNo, Rec, SqzLine) then
            exit;

        if not LineCoordMgt.CoordinateExists(SqzLine.RecordId) then
            exit;

        if ViewerState.GetViewerMode() = "DXP SQZ Viewer Mode"::Detached then
            ViewerState.UpdateViewerStateWithCoordinates(ApiDocumentId, SqzLine.RecordId)
        else
            ApiMgt.PostMessage(ApiMgt.MarkFieldByCoordinatesObj(SqzLine.RecordId), Resident);
    end;

    local procedure TryResolveSqzLineForPurchLine(SqzDocNo: Code[20]; PurchLine: Record "Purchase Line"; var SqzLine: Record "DXP SQZ Document Line"): Boolean
    begin
        // 1) Allocation link via receipt (Purchase Invoice Szenario)
        SqzLine.Reset();
        SqzLine.SetRange("Document No.", SqzDocNo);
        SqzLine.SetFilter("Allocated Document No.", '%1&<>%2', PurchLine."Receipt No.", '');
        SqzLine.SetFilter("Allocated Document Line No.", '%1&<>%2', PurchLine."Receipt Line No.", 0);
        if SqzLine.FindFirst() then
            exit(true);

        // 2) Fallback: content-based match (Type/No/Amounts)
        exit(TryResolveSqzLineByContent(SqzDocNo, PurchLine, SqzLine));
    end;

    local procedure TryResolveSqzLineByContent(SqzDocNo: Code[20]; PurchLine: Record "Purchase Line"; var BestSqzLine: Record "DXP SQZ Document Line"): Boolean
    var
        Candidate: Record "DXP SQZ Document Line";
        BestScore: Integer;
        Score: Integer;
        QtyTol: Decimal;
        AmtTol: Decimal;
    begin
        QtyTol := 0.00001;
        AmtTol := 0.01;

        BestScore := 0;
        Candidate.Reset();
        Candidate.SetRange("Document No.", SqzDocNo);

        // Prefer narrowing early on stable fields
        Candidate.SetRange(Type, PurchLine.Type);
        if PurchLine."No." <> '' then
            Candidate.SetRange("No.", PurchLine."No.");

        if not Candidate.FindSet() then
            exit(false);

        repeat
            Score := 0;

            // Strong signals
            if Candidate.Type = PurchLine.Type then
                Score += 20;
            if (PurchLine."No." <> '') and (Candidate."No." = PurchLine."No.") then
                Score += 40;

            // Weak/medium signals (tolerant)
            if (Candidate.Quantity <> 0) and (Abs(Candidate.Quantity - PurchLine.Quantity) <= QtyTol) then
                Score += 10;
            if (Candidate."Direct Unit Cost" <> 0) and (Abs(Candidate."Direct Unit Cost" - PurchLine."Direct Unit Cost") <= AmtTol) then
                Score += 10;
            if (Candidate."Line Amount" <> 0) and (Abs(Candidate."Line Amount" - PurchLine."Line Amount") <= AmtTol) then
                Score += 10;
            if (Candidate.Description <> '') and (PurchLine.Description <> '') and (Candidate.Description = PurchLine.Description) then
                Score += 5;

            if Score > BestScore then begin
                BestScore := Score;
                BestSqzLine := Candidate;
            end;
        until Candidate.Next() = 0;

        // Guard: avoid picking a random line when nothing matches well.
        exit(BestScore >= 30);
    end;
}

Events after creating a SQUEEZE document (CreateSource)

Audience

This documentation is intended for third-party developers who want to extend the creation process of a SQUEEZE document (tables DXP SQZ Document Header/DXP SQZ Document Line).

The focus is on the integration events that are raised after or during ISourceDocument.CreateSource(...).

Context: where is CreateSource called?

The entry point for the SQUEEZE integration is in the DXP SQZ API Mgt. codeunit:

Simplified flow (from CreateCoreAndSQUEEZEDocument):

  1. DocumentMgt.AddDocument(DocClass, ...) (create the Core document)
  2. ISourceDocument.CreateSource(Document) (create the SQUEEZE document)
  3. Process the import queue / "finish" the SQUEEZE document

Document classes & default behavior

Which CreateSource implementation is executed is defined in the enum extension DXP Document Class Ext..

Default (when no specific implementation is set): The DXP Document Class enum defines a DefaultImplementation on DXP Source Document Def. Impl.. This default implementation:

Common pipeline inside CreateSource

Both SQUEEZE implementations follow (in short) this pattern:

  1. Guard: the Core document must have Status = Imported and JSON Raw must exist.
  2. DXP SQZ Document Mgt.:
    • CreateSQUEEZEDocHeader(Document, DocHeader, RawJson)
    • CreateSQUEEZEDocLine(DocHeader, RawJson)
  3. DXP Document Mgt.: UpdateDocumentAfterTransfer(...) (set the Core document to "Transferred" and link it to the SQZ header)
  4. Order match / autocomplete / attachments / validation (depending on document class and setup)

The most important events are therefore attached to:

Event matrix (short overview)

Area Event Timing Control Typical use cases
DXP SQZ API Mgt. OnAfterInitMapping(...) after initializing the mapping/token context – extend/check mapping
DXP SQZ API Mgt. OnAfterSaveAttachment(...) after an attachment has been saved successfully – post-processing/indexing
DXP SQZ API Mgt. OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment(...) before the attachment record is modified – influence file name/origin
DXP SQZ P. Inv/Crdt Memo Impl. OnAfterCreateDocLines directly after CreateSQUEEZEDocLine(...) – evaluate lines/RawJson after creation
DXP SQZ P. Inv/Crdt Memo Impl. OnAfterCreateSource at the end of CreateSource(...) – post actions after autocomplete/order match/validation
DXP SQZ P. Inv/Crdt Memo Impl. OnBeforeDoPlausibilityChecks before the plausibility checks IsHandled replace/extend checks
DXP SQZ P. Inv/Crdt Memo Impl. OnAfterDoPlausibilityChecks after the plausibility checks – reuse the result (entries)
DXP SQZ P. Order Conf. Impl. OnBeforeDoPlausibilityChecks before the plausibility checks IsHandled replace/extend checks
DXP SQZ Document Mgt. OnBeforeModifySQUEEZEDocumentHeader in CreateSQUEEZEDocHeader(...) before the final modify – add/normalize header values from RawJson
DXP SQZ Document Mgt. OnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues in standard line post-processing before TransferRecognizedValues – correct recognized values per line
DXP SQZ Document Mgt. OnAfterTransferRecognizedValuesOnBeforeInsertDocLine in standard line post-processing after TransferRecognizedValues – line-based derivations (account assignment/dimensions)
DXP SQZ Document Mgt. OnBeforePerformAutomaticOrdermatch before the automatic order match IsHandled take over order match completely/adjust OrderNoList
JSON creation OnBeforeAddLineToDocumentJObj before the line structure is built – extend the header JSON
JSON creation OnBeforeAddLineJObjToLineJArray before LineJArray.Add(LineJObj) – extend the line JSON

Events in DXP SQZ P. Inv/Crdt Memo Impl. (document class: Invoice / Credit Memo)

This codeunit provides the strongest extension points directly in the CreateSource flow.

OnAfterCreateDocLines

Raised directly after CreateSQUEEZEDocLine(...) (that is, once the header & lines exist).

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterCreateDocLines(DocHeader: Record "DXP SQZ Document Header"; RawJson: JsonObject)

Typical use cases:

OnAfterCreateSource

Raised at the end of CreateSource(...) (after autocomplete/order match/optional attachment download/optional auto-validate).

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterCreateSource(DocHeader: Record "DXP SQZ Document Header"; RawJson: JsonObject)

Typical use cases:

Plausibility check events

These events relate to the IsSourceDataPlausible(...) method and are relevant if you extend/replace plausibility checks.

Signatures (excerpt):

[IntegrationEvent(false, false)]
local procedure OnBeforeDoPlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; var TempPlausibilityCheckEntry: Record "DXP Plausibility Check Entry" temporary; var IsHandled: Boolean)

[IntegrationEvent(false, false)]
local procedure OnBeforeDoHeaderPlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")

[IntegrationEvent(false, false)]
local procedure OnBeforeDoLinePlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; DocLine: Record "DXP SQZ Document Line"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")

[IntegrationEvent(false, false)]
local procedure OnAfterDoPlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; var TempPlausibilityCheckEntry: Record "DXP Plausibility Check Entry" temporary)

JSON creation (Processed JSON from the SQZ document)

The implementation contains events before line JSON is added to the header structure:

OnBeforeAddLineToDocumentJObj

[IntegrationEvent(false, false)]
local procedure OnBeforeAddLineToDocumentJObj(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")

OnBeforeAddLineJObjToLineJArray


[IntegrationEvent(false, false)]
local procedure OnBeforeAddLineJObjToLineJArray(var LineJObj: JsonObject; DocLine: Record "DXP SQZ Document Line")

Use cases:


Events in DXP SQZ P. Order Conf. Impl. (document class: Order Confirmation)

This codeunit has no dedicated OnAfterCreateSource events in CreateSource(...). Extensions here typically happen via:

JSON creation

[IntegrationEvent(false, false)]
local procedure OnBeforeAddLineToDocumentJObj(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")

[IntegrationEvent(false, false)]
local procedure OnBeforeAddLineJObjToLineJArray(var LineJObj: JsonObject; DocLine: Record "DXP SQZ Document Line")

Plausibility check events

Analogous to the Invoice/Credit Memo implementation:


Events in DXP SQZ Document Mgt. (common to all document classes)

These events are especially relevant because they are raised within the header/line creation and order match pipeline.

OnBeforeModifySQUEEZEDocumentHeader

Raised in CreateSQUEEZEDocHeader(...), after values have been assigned via field mapping and before the header is finally modified.

Signature:

[IntegrationEvent(false, false)]
local procedure OnBeforeModifySQUEEZEDocumentHeader(var DocHeader: Record "DXP SQZ Document Header"; RawJson: JsonObject)

Use cases:

OnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues

OnAfterTransferRecognizedValuesOnBeforeInsertDocLine

Raised in the standard line post-processing (for standard document classes) around TransferRecognizedValues(DocLine).

Signatures:

[IntegrationEvent(false, false)]
local procedure OnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues(var DocLine: Record "DXP SQZ Document Line"; RowJTok: JsonToken)

[IntegrationEvent(false, false)]
local procedure OnAfterTransferRecognizedValuesOnBeforeInsertDocLine(var DocLine: Record "DXP SQZ Document Line")

Use cases:

OnBeforePerformAutomaticOrdermatch

Raised in PerformAutomaticOrdermatch(...), after the order number list (OrderNoList) has been filled and before the automatic order match runs.

Signature:

[IntegrationEvent(false, false)]
local procedure OnBeforePerformAutomaticOrdermatch(DocHeader: Record "DXP SQZ Document Header"; var OrderNoList: List of [Code[20]]; var IsHandled: Boolean)

Use cases:


Events in DXP SQZ API Mgt. (close to creation/attachments)

These events sit "around" the API communication (for example attachments).

OnAfterInitMapping

Raised in DownloadFieldMapping(...), after the document class has initialized the default mapping via the DXP ISource Document interface (ISourceDocument.InitFieldMapping(...)) and before the field list from SQUEEZE is synchronized into the BC field mapping structures.

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterInitMapping(var HeaderMapping: Dictionary of [Text, Integer]; var LineMapping: Dictionary of [Text, Integer]; DocClass: Enum "DXP Document Class")

Parameters:

Typical use cases:

OnAfterSaveAttachment

Raised in SaveAttachments(...) per attachment, after the attachment has been created via DXP Document Attachment Mgt. and – if the attachment is marked as origin file – after the Modify(true) of the record.

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterSaveAttachment(DocumentAttachment: Record "DXP Document Attachment")

DocumentAttachment is not passed as var. If you want to change fields on the record persistently, you must reload the record in the subscriber and explicitly call Modify(...).

Typical use cases:

OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment

Raised in SaveAttachments(...), when the attachment is marked as origin file according to the payload and after Is Source File and File Name (origin name) have been set – but before DocumentAttachment.Modify(true) is called.

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment(var DocumentAttachment: Record "DXP Document Attachment")

Typical use cases:

Minimal examples: EventSubscriber for DXP SQZ API Mgt.

The following examples show typical, small extensions. Adjust the object IDs/names to your namespace.

codeunit 50101 "My SQZ API Mgt. Subs"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnAfterInitMapping', '', false, false)]
    local procedure OnAfterInitMapping(var HeaderMapping: Dictionary of [Text, Integer]; var LineMapping: Dictionary of [Text, Integer]; DocClass: Enum "DXP Document Class")
    var
        SqzDocHeader: Record "DXP SQZ Document Header";
        SqzDocLine: Record "DXP SQZ Document Line";
    begin
        // Example: add a new SQZ field as default mapping.
        // Important: The integer value is the BC field no. of the target field (depends on your mapping concept).
        if not HeaderMapping.ContainsKey('my_custom_header_token') then
            HeaderMapping.Add('my_custom_header_token', SqzDocHeader.FieldNo("Vendor No."));

        if not LineMapping.ContainsKey('my_custom_line_token') then
            LineMapping.Add('my_custom_line_token', SqzDocLine.FieldNo(Description));
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment', '', false, false)]
    local procedure OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment(var DocumentAttachment: Record "DXP Document Attachment")
    begin
        // Example: normalize the origin file name (remove whitespace).
        DocumentAttachment."File Name" := DelChr(DocumentAttachment."File Name", '=', ' ');
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnAfterSaveAttachment', '', false, false)]
    local procedure OnAfterSaveAttachment(DocumentAttachment: Record "DXP Document Attachment")
    begin
        // Example: kick off post-processing.
        // If you want to change the attachment persistently, you can also modify the parameter record directly.
        // DocumentAttachment.Validate("Your Field", ...);
        // DocumentAttachment.Modify(true);
    end;
}

Events after creating a Core document (DXP Document)

Audience

This documentation is intended for third-party developers who want to extend the lifecycle of the Core document DXP Document – in particular at the points where:

Context: Core document creation vs. further processing

1) The Core document is created (AddDocument)

A DXP Document is created in DXP Document Mgt. via AddDocument(...):

2) Afterwards: transfer/linking and processing

Typical next steps (depending on the app flow):

Most extension points are therefore not directly at the insert, but in the update/processing flow.

Event matrix (short overview)

Area Event Timing Control Typical use cases
DXP Document Mgt. OnAfterSetLinkedToRecIdOnBeforeModify in UpdateDocumentAfterTransfer(...) before Modify(true) – set additional info as soon as Linked-to Record Id is set
DXP Document Mgt. OnBeforeGetDocumentOnBeforeCheckStatusAndNextProcessStep early in UpdateDocument(...) before Document.Get(DocNo) IsHandled overwrite status/next step, take over processing
DXP Document Mgt. OnAfterGetDocumentOnBeforeCheckStatusAndNextProcessStep in UpdateDocument(...) after Get(...) before standard document creation Handled decide depending on the document, adjust JSON/status
DXP Document Mgt. OnAfterUpdateRawJson after writing JSON Raw in UpdateRawJsonJson(...) – normalize raw JSON/additional derivations
DXP Document Mgt. OnAfterWriteProcessedJsonToBlob after writing JSON Processed in UpdateStatusAndProcessedJson(...) – finalize processed JSON, add tokens
DXP Document Mgt. OnBeforeTransferCoreAttachmentsToStandardDocument before the attachment transfer IsHandled replace/selectively control the transfer
DXP Document Transfer Mgt. OnBeforeExitProcessedJsonDocHeader when creating header JSON (before exit(JObject)) – add/normalize standard tokens
DXP Document Transfer Mgt. OnBeforeExitProcessedJsonDocLine when creating line JSON (before exit(JObject)) – add/normalize line tokens
DXP Document Transfer Mgt. OnAfterCreatePurchaseHeader after creating/validating the purchase header – add header fields, post-processing
DXP Document Transfer Mgt. OnBeforeInsertPurchaseLine before inserting a purchase line – adjust purchase line (for example account assignment, type/no.)
DXP Document Transfer Mgt. OnAfterInsertPurchaseLineOnBeforeAddDimensions after insert, before dimensions – prepare dimensions/additional info
DXP Document Transfer Mgt. OnAfterOnBeforeAddLineDimensions for receipt lines: before the dimensions of the line to be updated – adjust update cases (receipt)
DXP Document Transfer Mgt. OnAfterProcessAdditionalInformation after transferring additional information – add add-on fields per target RecordRef
DXP Document Transfer Mgt. OnAfterCreatePurchaseDoc after the complete creation of the purchase document – final hook (logging, follow-up actions)

Events in DXP Document Mgt. (Core lifecycle)

OnAfterSetLinkedToRecIdOnBeforeModify

Raised in UpdateDocumentAfterTransfer(...) – after the status/Linked-to Record Id have been set, but before the DXP Document is saved.

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterSetLinkedToRecIdOnBeforeModify(var Document: Record "DXP Document")

Use cases:

OnBeforeGetDocumentOnBeforeCheckStatusAndNextProcessStep

Called in UpdateDocument(...) as the earliest hook – even before the DXP Document is loaded via Get(DocNo).

Signature:

[IntegrationEvent(false, false)]
local procedure OnBeforeGetDocumentOnBeforeCheckStatusAndNextProcessStep(
    DocNo: Code[20];
    var Status: Enum "DXP Document Status";
    var NextStep: Enum "DXP Next Process Step";
    JObject: JsonObject;
    var IsHandled: Boolean)

Use cases:

OnAfterGetDocumentOnBeforeCheckStatusAndNextProcessStep

Raised in UpdateDocument(...) after Document.Get(DocNo), before standard documents are created.

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterGetDocumentOnBeforeCheckStatusAndNextProcessStep(
    var Document: Record "DXP Document";
    NewStatus: Enum "DXP Document Status";
    NextStep: Enum "DXP Next process step";
    var ProcessedJSONObj: JsonObject;
    var Handled: Boolean)

Use cases:

OnAfterUpdateRawJson

Raised in UpdateRawJsonJson(...) after writing JSON Raw.

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterUpdateRawJson(var Document: Record "DXP Document"; var RawJSONObj: JsonObject)

Use cases:

OnAfterWriteProcessedJsonToBlob

Raised in UpdateStatusAndProcessedJson(...) after JSON Processed has been written to the BLOB.

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterWriteProcessedJsonToBlob(var Document: Record "DXP Document"; var ProcessedJSONObj: JsonObject)

Use cases:

OnBeforeTransferCoreAttachmentsToStandardDocument

Raised in TransferCoreAttachmentsToStandardDocument(...) before the transfer. The standard logic can be replaced via IsHandled.

Signature:

[IntegrationEvent(false, false)]
local procedure OnBeforeTransferCoreAttachmentsToStandardDocument(Document: Record "DXP Document"; var IsHandled: Boolean)

Use cases:


Events in DXP Document Transfer Mgt. (standard documents from JSON Processed)

This codeunit is relevant as soon as standard documents (for example purchase documents) are created from JSON Processed.

OnBeforeExitProcessedJsonDocHeader

Raised in CreateDocHeaderJson(...) immediately before exit(JObject).

Signature:

[IntegrationEvent(false, false)]
local procedure OnBeforeExitProcessedJsonDocHeader(var JObject: JsonObject)

Use cases:

OnBeforeExitProcessedJsonDocLine

Raised in CreateDocLineJson(...) immediately before exit(JObject).

Signature:

[IntegrationEvent(false, false)]
local procedure OnBeforeExitProcessedJsonDocLine(var JObject: JsonObject)

Use cases:

OnAfterCreatePurchaseHeader

Raised after the purchase header is created/validated.

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterCreatePurchaseHeader(var JObject: JsonObject; PurchaseHeader: Record "Purchase Header")

Use cases:

OnBeforeInsertPurchaseLine

Raised immediately before a purchase line is inserted.

Signature:

[IntegrationEvent(false, false)]
local procedure OnBeforeInsertPurchaseLine(var JObjectLine: JsonObject; var PurchaseLine: Record "Purchase Line")

Use cases:

OnAfterInsertPurchaseLineOnBeforeAddDimensions

Raised after the purchase line is inserted, but before dimension validation.

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterInsertPurchaseLineOnBeforeAddDimensions(var JObjectLine: JsonObject; PurchaseLine: Record "Purchase Line")

Use cases:

OnAfterOnBeforeAddLineDimensions

Raised in the receipt line update case, before dimensions are set for the purchase line to be updated.

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterOnBeforeAddLineDimensions(var JObjectLine: JsonObject; PurchaseLineToUpdate: Record "Purchase Line")

Use cases:

OnAfterProcessAdditionalInformation

Raised after the transfer of additional information (ProcessAdditionalInformation).

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterProcessAdditionalInformation(var RecRef: RecordRef; SourceJsonObject: JsonObject)

Use cases:

OnAfterCreatePurchaseDoc

Raised after the complete creation of the purchase document.

Signature:

[IntegrationEvent(false, false)]
local procedure OnAfterCreatePurchaseDoc(var JObject: JsonObject; PurchaseHeader: Record "Purchase Header")

Use cases:


Minimal example: influencing status/next step toward "Custom Processing"

Example: set Status to "Custom Processing" before the standard processing:

codeunit 50101 "My Core Document Hooks"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Mgt.", 'OnBeforeGetDocumentOnBeforeCheckStatusAndNextProcessStep', '', false, false)]
    local procedure OnBeforeGetDoc(
        DocNo: Code[20];
        var Status: Enum "DXP Document Status";
        var NextStep: Enum "DXP Next Process Step";
        JObject: JsonObject;
        var IsHandled: Boolean)
    begin
        Status := Status::"Custom Processing";
        // optionally adjust NextStep
    end;
}

[Best Practice] Use IsHandled := true only when you fully replace the standard logic – otherwise it can lead to inconsistent status/JSON states.

Adding a field in the validation (up to Squeeze BC App version 1.*)

The following uses an example extension to show how, as a developer, you can add fields in the validation.

Required integration events including example procedures:

codeunit 50100 EventSubs
{
    //
    //Any header field that you want to transfer to the resulting document, has to be added to the source JSON Object
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforeAddLineToDocumentJObj', '', false, false)]
    local procedure SQZDocMgtOnBeforeAddLineToDocumentJObj(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
    begin
        AddCustomHeaderFieldsToJson(DocumentJObj, DocHeader);
    end;

    //
    // Any line field that you want to transfer to the resulting document, has to be added to the source JSON Object
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforeAddLineJObjToLineJArray', '', false, false)]
    local procedure SQZDocMgtOnBeforeAddLineJObjToLineJArray(var LineJObj: JsonObject; DocLine: Record "DXP SQZ Document Line")
    begin
        AddCustomLineFieldsToJson(LineJObj, DocLine);
    end;

    //
    // [If you want to perform plausibility checks on the newly added header field, this is the place]
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnAfterDoHeaderPlausibilityChecks', '', false, false)]
    local procedure SQZDocMgtOnBeforeDoHeaderPlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
    begin
        CheckCustomer(PlausibilityCheck, DocHeader."Customer No.");
    end;

    //
    // [If you want to perform plausibility checks on the newly added line field, this is the place]
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnAfterDoLinePlausibilityChecks', '', false, false)]
    local procedure SQZDocMgtOnAfterDoLinePlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; DocLine: Record "DXP SQZ Document Line"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
    begin
        CheckCustomer(PlausibilityCheck, DocLine."Customer No.");
    end;

    //
    // The value of the previously extended JSON Object now has to saved to the corresponding field in the SQUEEZE Document Line
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforeModifySQUEEZEDocumentHeader', '', false, false)]
    local procedure SqzDocMgtOnBeforeModifySQUEEZEDocumentHeader(var DocHeader: Record "DXP SQZ Document Header"; RawJson: JsonObject)
    var
        JSONHelper: Codeunit "DXP Json Helper";
        SQZApiTokenMgt: Codeunit "DXP SQZ API Token Mgt.";
        TokenMgt: Codeunit TokenMgt;
    begin
        DocHeader."Customer No." := COPYSTR(JsonHelper.ValAsTxt(RawJson, SQZApiTokenMgt.GetHeaderValueByNameTok(TokenMgt.GetCustomerNoTok()), false), 1, MaxStrLen(DocHeader."Customer No."));
    end;

    //
    // The value of the previously extended JSON Object now has to saved to the corresponding field in the SQUEEZE Document Header
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues', '', false, false)]
    local procedure SQZDocMgtOnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues(RowJTok: JsonToken; var DocLine: Record "DXP SQZ Document Line")
    var
        JSONHelper: Codeunit "DXP Json Helper";
        SQZApiTokenMgt: Codeunit "DXP SQZ API Token Mgt.";
        TokenMgt: Codeunit TokenMgt;
    begin
        DocLine."Customer No." := CopyStr(JsonHelper.ValAsTxt(RowJTok.AsObject(), SQZApiTokenMgt.GetCellValueTok(TokenMgt.GetCustomerNoTok()), false), 1, MaxStrLen(DocLine."Customer No."));
    end;

    //
    // The newly added fields now have to be added to the field mapping
    // Otherwise the recognized values will be saved as meta data
    // This step is necessary to make sure that the fields can be highlighted in the SQUEEZE Viewer
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnAfterInitMapping', '', false, false)]
    local procedure SQZApiMgtOnAfterInitMapping(var HeaderMapping: Dictionary of [Text, Integer]; var LineMapping: Dictionary of [Text, Integer]; DocClass: Enum "DXP Document Class")
    var
        SQZDocHeader: Record "DXP SQZ Document Header";
        SQZDocLine: Record "DXP SQZ Document Line";
        TokenMgt: Codeunit TokenMgt;
    begin
        // The standard (without installed extensions) supports one Document Class
        case DocClass of
            DocClass::"DXP Invoice / Credit Memo":
                begin
                    HeaderMapping.Add(TokenMgt.GetCustomerNoTok(), SQZDocHeader.FieldNo("Customer No."));
                    LineMapping.Add(TokenMgt.GetCustomerNoTok(), SQZDocLine.FieldNo("Customer No."));
                end;
        end;
    end;

    //
    // The value in the JSON Object now has to be transferred to the Purchase Header
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Transfer Mgt.", 'OnAfterCreatePurchaseHeader', '', false, false)]
    local procedure DocTransferMgtOnAfterCreatePurchaseHeader(var JObject: JsonObject; PurchaseHeader: Record "Purchase Header")
    begin
        TransferCustomHeaderFieldFromJsonToPurchaseHeader(JObject, PurchaseHeader);
    end;

    //
    // The value in the JSON Object now has to be transferred to the Purchase Line
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Transfer Mgt.", 'OnBeforeInsertPurchaseLine', '', false, false)]
    local procedure DocTransferMgtOnAfterInsertPurchaseLineOnBeforeAddDimensions(var JObjectLine: JsonObject; var PurchaseLine: Record "Purchase Line")
    begin
        TransferCustomHeaderFieldFromJsonToPurchaseLine(JObjectLine, PurchaseLine);
    end;

    local procedure TransferCustomHeaderFieldFromJsonToPurchaseHeader(var JObject: JsonObject; PurchaseHeader: Record "Purchase Header")
    var
        JsonHelper: Codeunit "DXP Json Helper";
        TokenMgt: Codeunit TokenMgt;
    begin
        PurchaseHeader.Validate("DXP Customer No.", JsonHelper.ValAsTxt(JObject, TokenMgt.GetCustomerNoTok(), false));
        PurchaseHeader.Modify();
    end;

    local procedure TransferCustomHeaderFieldFromJsonToPurchaseLine(var JObjectLine: JsonObject; var PurchaseLine: Record "Purchase Line")
    var
        JsonHelper: Codeunit "DXP Json Helper";
        TokenMgt: Codeunit TokenMgt;
    begin
        PurchaseLine.Validate("DXP Customer No.", JsonHelper.ValAsTxt(JObjectLine, TokenMgt.GetCustomerNoTok(), false));
    end;

    local procedure AddCustomHeaderFieldsToJson(var DocumentJObj: JsonObject; SQZDocumentHeader: Record "DXP SQZ Document Header")
    var
        TokenMgt: Codeunit TokenMgt;
    begin
        DocumentJObj.Add(TokenMgt.GetCustomerNoTok(), SQZDocumentHeader."Customer No.");
    end;

    local procedure AddCustomLineFieldsToJson(var LineJObj: JsonObject; SQZDocumentLine: Record "DXP SQZ Document Line")
    var
        TokenMgt: Codeunit TokenMgt;
    begin
        LineJObj.Add(TokenMgt.GetCustomerNoTok(), SQZDocumentLine."Customer No.");
    end;

    [TryFunction]
    local procedure CustomerExists(CustomerNo: Code[20])
    var
        Customer: Record Customer;
    begin
        Customer.Get(CustomerNo);
    end;

    local procedure CheckCustomer(var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt."; CustomerNo: Code[20]): Boolean
    begin
        if CustomerNo = '' then
            exit(false);

        if not CustomerExists(CustomerNo) then begin
            PlausibilityCheck.AddPlausibilityCheckEntry(GetLastErrorText(), Page::"Customer List");
            exit(false);
        end;

        exit(true);
    end;
}

Table Extensions:

SQUEEZE 4 BC
tableextension 50100 "SQZ Doc. Header Ext." extends "DXP SQZ Document Header"
{
    fields
    {
        field(50100; "Customer No."; Code[20])
        {
            TableRelation = Customer;
            ValidateTableRelation = false;
            Caption = 'Customer No.';
        }
    }
}
tableextension 50101 "SQZ Doc. Line Ext." extends "DXP SQZ Document Line"
{
    fields
    {
        field(50100; "Customer No."; Code[20])
        {
            TableRelation = Customer;
            ValidateTableRelation = false;
            Caption = 'Customer No.';
        }
    }
}
Target document (here: purchase document)
tableextension 50102 "Purchase Header Ext." extends "Purchase Header"
{
    fields
    {
        field(50100; "DXP Customer No."; Code[20])
        {
            DataClassification = CustomerContent;
            TableRelation = Customer;
            Caption = 'DEXPRO Customer No.';
        }
    }
}

Page Extensions:

SQUEEZE 4 BC
pageextension 50100 "SQZ Document Ext." extends "DXP SQZ Document"
{
    layout
    {
        addafter(BuyFromVendorInternal)
        {
            field("Customer No. Internal"; Rec."Customer No.")
            {
                ApplicationArea = All;
                ToolTip = 'Specifies the value of the Customer No. field.';
                trigger OnAssistEdit()
                begin
                    MarkField(Rec.FieldNo("Customer No."), Rec."Customer No.", Rec);
                end;

                trigger OnValidate()
                begin
                    CheckPlausibility();
                end;
            }
        }
        addafter(BuyFromVendor)
        {
            field("Customer No."; Rec."Customer No.")
            {
                ApplicationArea = All;
                ToolTip = 'Specifies the value of the Customer No. field.';
                trigger OnAssistEdit()
                var
                    ApiMgt: Codeunit "DXP SQZ API Mgt.";
                    ViewerResident: Codeunit "DXP SQUEEZE Viewer Resident";
                begin
                    MarkField(Rec.FieldNo("Customer No."), Rec."Customer No.", Rec);
                end;

                trigger OnValidate()
                begin
                    CheckPlausibility();
                end;
            }
        }
    }

    local procedure MarkField(AppFldNo: Integer; FieldVal: Variant; DocHeader: Record "DXP SQZ Document Header")
    var
        ApiMgt: Codeunit "DXP SQZ API Mgt.";
        ViewerResident: Codeunit "DXP SQUEEZE Viewer Resident";
    begin
        ViewerResident := GetViewerResident();
        ApiMgt.MarkField(AppFldNo, FieldVal, ViewerResident, DocHeader);
    end;
}
pageextension 50101 "SQZ Document Sf. Ext." extends "DXP SQZ Document Subform"
{
    layout
    {
        addafter("No.")
        {
            field("Customer No."; Rec."Customer No.")
            {
                ApplicationArea = All;
                ToolTip = 'Specifies the value of the Customer No. field.';
            }
        }
    }
}
Target document (here: purchase document)
pageextension 50102 "Purchase Invoice Ext." extends "Purchase Invoice"
{
    layout
    {
        addafter("Buy-from Vendor No.")
        {
            field("DXP Customer No."; Rec."DXP Customer No.")
            {
                ToolTip = 'Specifies the value of the DEXPRO Customer No. field.';
                ApplicationArea = all;
            }
        }
    }
}
pageextension 50103 "Purchase Inv. Sf Ext." extends "Purch. Invoice Subform"
{
    layout
    {
        addafter("No.")
        {
            field("DXP Customer No."; Rec."DXP Customer No.")
            {
                ToolTip = 'Specifies the value of the DEXPRO Customer No. field.';
                ApplicationArea = all;
            }
        }
    }
}
Optional helper objects:
codeunit 50101 TokenMgt
{
    procedure GetCustomerNoTok(): Text
    begin
        exit(CustomerNoTok);
    end;

    var
        CustomerNoTok: Label 'customerNo', Locked = true;
}

Adding a field in the validation (from version 2.0)

The following uses an example extension to show how, as a developer, you can add fields in the validation (here: document class Invoice/Credit Memo).

Required integration events including example procedures:

codeunit 50100 EventSubs
{
    //
    //Any header field that you want to transfer to the resulting document, has to be added to the source JSON Object
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnBeforeAddLineToDocumentJObj', '', false, false)]
    local procedure SQZPInvCrdtMemoImplOnBeforeAddLineToDocumentJObj(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
    begin
        AddCustomHeaderFieldsToJson(DocumentJObj, DocHeader);
    end;

    //
    // Any line field that you want to transfer to the resulting document, has to be added to the source JSON Object
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnBeforeAddLineJObjToLineJArray', '', false, false)]
    local procedure SQZPInvCrdtMemoImplOnBeforeAddLineJObjToLineJArray(var LineJObj: JsonObject; DocLine: Record "DXP SQZ Document Line")
    begin
        AddCustomLineFieldsToJson(LineJObj, DocLine);
    end;

    //
    // [If you want to perform plausibility checks on the newly added header field, this is the place]
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnAfterDoHeaderPlausibilityChecks', '', false, false)]
    local procedure SQZPInvCrdtMemoImplOnBeforeDoHeaderPlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
    begin
        CheckCustomer(PlausibilityCheck, DocHeader."Customer No.");
    end;

    //
    // [If you want to perform plausibility checks on the newly added line field, this is the place]
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnAfterDoLinePlausibilityChecks', '', false, false)]
    local procedure SQZPInvCrdtMemoImplOnAfterDoLinePlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; DocLine: Record "DXP SQZ Document Line"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
    begin
        CheckCustomer(PlausibilityCheck, DocLine."Customer No.");
    end;

    //
    // The value of the previously extended JSON Object now has to saved to the corresponding field in the SQUEEZE Document Line
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforeModifySQUEEZEDocumentHeader', '', false, false)]
    local procedure SqzDocMgtOnBeforeModifySQUEEZEDocumentHeader(var DocHeader: Record "DXP SQZ Document Header"; RawJson: JsonObject)
    var
        JSONHelper: Codeunit "DXP Json Helper";
        SQZApiTokenMgt: Codeunit "DXP SQZ API Token Mgt.";
        TokenMgt: Codeunit TokenMgt;
    begin
        DocHeader."Customer No." := COPYSTR(JsonHelper.ValAsTxt(RawJson, SQZApiTokenMgt.GetHeaderValueByNameTok(TokenMgt.GetCustomerNoTok()), false), 1, MaxStrLen(DocHeader."Customer No."));
    end;

    //
    // The value of the previously extended JSON Object now has to saved to the corresponding field in the SQUEEZE Document Header
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues', '', false, false)]
    local procedure SQZDocMgtOnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues(RowJTok: JsonToken; var DocLine: Record "DXP SQZ Document Line")
    var
        JSONHelper: Codeunit "DXP Json Helper";
        SQZApiTokenMgt: Codeunit "DXP SQZ API Token Mgt.";
        TokenMgt: Codeunit TokenMgt;
    begin
        DocLine."Customer No." := CopyStr(JsonHelper.ValAsTxt(RowJTok.AsObject(), SQZApiTokenMgt.GetCellValueTok(TokenMgt.GetCustomerNoTok()), false), 1, MaxStrLen(DocLine."Customer No."));
    end;

    //
    // The newly added fields now have to be added to the field mapping
    // Otherwise the recognized values will be saved as meta data
    // This step is also necessary to make sure that the fields can be highlighted in the SQUEEZE Viewer
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnAfterInitMapping', '', false, false)]
    local procedure SQZApiMgtOnAfterInitMapping(var HeaderMapping: Dictionary of [Text, Integer]; var LineMapping: Dictionary of [Text, Integer]; DocClass: Enum "DXP Document Class")
    var
        SQZDocHeader: Record "DXP SQZ Document Header";
        SQZDocLine: Record "DXP SQZ Document Line";
        TokenMgt: Codeunit TokenMgt;
    begin
        case DocClass of
            DocClass::"DXP Invoice / Credit Memo":
                begin
                    HeaderMapping.Add(TokenMgt.GetCustomerNoTok(), SQZDocHeader.FieldNo("Customer No."));
                    LineMapping.Add(TokenMgt.GetCustomerNoTok(), SQZDocLine.FieldNo("Customer No."));
                end;
        end;
    end;

    //
    // The value in the JSON Object now has to be transferred to the Purchase Header
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Transfer Mgt.", 'OnAfterCreatePurchaseHeader', '', false, false)]
    local procedure DocTransferMgtOnAfterCreatePurchaseHeader(var JObject: JsonObject; PurchaseHeader: Record "Purchase Header")
    begin
        TransferCustomHeaderFieldFromJsonToPurchaseHeader(JObject, PurchaseHeader);
    end;

    //
    // The value in the JSON Object now has to be transferred to the Purchase Line
    //
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Transfer Mgt.", 'OnBeforeInsertPurchaseLine', '', false, false)]
    local procedure DocTransferMgtOnAfterInsertPurchaseLineOnBeforeAddDimensions(var JObjectLine: JsonObject; var PurchaseLine: Record "Purchase Line")
    begin
        TransferCustomHeaderFieldFromJsonToPurchaseLine(JObjectLine, PurchaseLine);
    end;

    local procedure TransferCustomHeaderFieldFromJsonToPurchaseHeader(var JObject: JsonObject; PurchaseHeader: Record "Purchase Header")
    var
        JsonHelper: Codeunit "DXP Json Helper";
        TokenMgt: Codeunit TokenMgt;
    begin
        PurchaseHeader.Validate("DXP Customer No.", JsonHelper.ValAsTxt(JObject, TokenMgt.GetCustomerNoTok(), false));
        PurchaseHeader.Modify();
    end;

    local procedure TransferCustomHeaderFieldFromJsonToPurchaseLine(var JObjectLine: JsonObject; var PurchaseLine: Record "Purchase Line")
    var
        JsonHelper: Codeunit "DXP Json Helper";
        TokenMgt: Codeunit TokenMgt;
    begin
        PurchaseLine.Validate("DXP Customer No.", JsonHelper.ValAsTxt(JObjectLine, TokenMgt.GetCustomerNoTok(), false));
    end;

    local procedure AddCustomHeaderFieldsToJson(var DocumentJObj: JsonObject; SQZDocumentHeader: Record "DXP SQZ Document Header")
    var
        TokenMgt: Codeunit TokenMgt;
    begin
        DocumentJObj.Add(TokenMgt.GetCustomerNoTok(), SQZDocumentHeader."Customer No.");
    end;

    local procedure AddCustomLineFieldsToJson(var LineJObj: JsonObject; SQZDocumentLine: Record "DXP SQZ Document Line")
    var
        TokenMgt: Codeunit TokenMgt;
    begin
        LineJObj.Add(TokenMgt.GetCustomerNoTok(), SQZDocumentLine."Customer No.");
    end;

    [TryFunction]
    local procedure CustomerExists(CustomerNo: Code[20])
    var
        Customer: Record Customer;
    begin
        Customer.Get(CustomerNo);
    end;

    local procedure CheckCustomer(var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt."; CustomerNo: Code[20]): Boolean
    begin
        if CustomerNo = '' then
            exit(false);

        if not CustomerExists(CustomerNo) then begin
            PlausibilityCheck.AddPlausibilityCheckEntry(GetLastErrorText(), Page::"Customer List");
            exit(false);
        end;

        exit(true);
    end;
}

Table Extensions:

SQUEEZE 4 BC
tableextension 50100 "SQZ Doc. Header Ext." extends "DXP SQZ Document Header"
{
    fields
    {
        field(50100; "Customer No."; Code[20])
        {
            TableRelation = Customer;
            ValidateTableRelation = false;
            Caption = 'Customer No.';
        }
    }
}
tableextension 50101 "SQZ Doc. Line Ext." extends "DXP SQZ Document Line"
{
    fields
    {
        field(50100; "Customer No."; Code[20])
        {
            TableRelation = Customer;
            ValidateTableRelation = false;
            Caption = 'Customer No.';
        }
    }
}
Target document (here: purchase document)
tableextension 50102 "Purchase Header Ext." extends "Purchase Header"
{
    fields
    {
        field(50100; "DXP Customer No."; Code[20])
        {
            DataClassification = CustomerContent;
            TableRelation = Customer;
            Caption = 'DEXPRO Customer No.';
        }
    }
}

Page Extensions:

SQUEEZE 4 BC
pageextension 50100 "SQZ Document Ext." extends "DXP SQZ Document v2"
{
    layout
    {
        addafter(BuyFromVendorInternal)
        {
            field("Customer No. Internal"; Rec."Customer No.")
            {
                ApplicationArea = All;
                ToolTip = 'Specifies the value of the Customer No. field.';
                trigger OnAssistEdit()
                begin
                    MarkField(Rec.FieldNo("Customer No."), Rec."Customer No.", Rec);
                end;

                trigger OnValidate()
                begin
                    CheckPlausibility();
                end;
            }
        }
        addafter(BuyFromVendorExternal)
        {
            field("Customer No."; Rec."Customer No.")
            {
                ApplicationArea = All;
                ToolTip = 'Specifies the value of the Customer No. field.';
                trigger OnAssistEdit()
                var
                    ApiMgt: Codeunit "DXP SQZ API Mgt.";
                    ViewerResident: Codeunit "DXP SQUEEZE Viewer Resident";
                begin
                    MarkField(Rec.FieldNo("Customer No."), Rec."Customer No.", Rec);
                end;

                trigger OnValidate()
                begin
                    CheckPlausibility();
                end;
            }
        }
    }

    local procedure MarkField(AppFldNo: Integer; FieldVal: Variant; DocHeader: Record "DXP SQZ Document Header")
    var
        ApiMgt: Codeunit "DXP SQZ API Mgt.";
        ViewerResident: Codeunit "DXP SQUEEZE Viewer Resident";
    begin
        ViewerResident := GetViewerResident();
        ApiMgt.MarkField(AppFldNo, FieldVal, ViewerResident, DocHeader);
    end;
}
pageextension 50101 "SQZ Document Sf. Ext." extends "DXP SQZ Document Subform"
{
    layout
    {
        addafter("No.")
        {
            field("Customer No."; Rec."Customer No.")
            {
                ApplicationArea = All;
                ToolTip = 'Specifies the value of the Customer No. field.';
            }
        }
    }
}
Target document (here: purchase document)
pageextension 50102 "Purchase Invoice Ext." extends "Purchase Invoice"
{
    layout
    {
        addafter("Buy-from Vendor No.")
        {
            field("DXP Customer No."; Rec."DXP Customer No.")
            {
                ToolTip = 'Specifies the value of the DEXPRO Customer No. field.';
                ApplicationArea = all;
            }
        }
    }
}
pageextension 50103 "Purchase Inv. Sf Ext." extends "Purch. Invoice Subform"
{
    layout
    {
        addafter("No.")
        {
            field("DXP Customer No."; Rec."DXP Customer No.")
            {
                ToolTip = 'Specifies the value of the DEXPRO Customer No. field.';
                ApplicationArea = all;
            }
        }
    }
}
Optional helper objects:
codeunit 50101 TokenMgt
{
    procedure GetCustomerNoTok(): Text
    begin
        exit(CustomerNoTok);
    end;

    var
        CustomerNoTok: Label 'customerNo', Locked = true;
}

Events when validating a SQUEEZE document (Validate action)

Target audience

This documentation is aimed at third-party developers who want to run their own logic when a user triggers the Validate action on the DXP SQZ Document v2 page (page 70954659) – for example additional custom checks before validation, setting further values on the SQUEEZE document, or follow-up actions on the target document that was created.

The events described here are available from SQUEEZE for Dynamics 365 BC version 2.50.

Context: what happens when you choose Validate?

The Validate action calls two routines of the document page in sequence:

1) CheckPlausibility()

Runs the plausibility check of the respective document class and updates the plausibility entries in the lower part of the page. This routine runs not only when you choose Validate, but after almost every field change. It is therefore not a suitable hook for "once, on validation".

To extend the check itself, use the already documented events OnBeforeDoPlausibilityChecks, OnAfterDoHeaderPlausibilityChecks, OnAfterDoLinePlausibilityChecks and OnAfterDoPlausibilityChecks.

2) FinishValidation()

Stops with an error message as long as plausibility entries are still open, handles the duplicate check where applicable, and then calls the ValidateSQUEEZEDocument method of the DXP SQZ Document Mgt. codeunit (70954632). That is where the processed JSON is created, the Core document (DXP Document) is processed further, the target document is created and the SQUEEZE document is completed.

Both events described here are raised in DXP SQZ Document Mgt.ValidateSQUEEZEDocument. They therefore apply uniformly to:

There is deliberately no event directly on the action. If you want to distinguish between a manual click and automatic validation, evaluate the AutomaticValidation parameter.

Event matrix (overview)

Area Event Timing Control Typical use cases
DXP SQZ Document Mgt. OnBeforeValidateSQUEEZEDocument After the plausibility and duplicate checks, before the processed JSON is created and the target document is generated var IsHandled: Boolean cancels the validation; var DocHeader allows changes to the document Custom additional checks with cancellation, filling fields, starting an approval workflow, querying external systems
DXP SQZ Document Mgt. OnAfterValidateSQUEEZEDocument After the Core document has been processed and the target document has been created, before the validation dialog Cancellation not possible Post-processing the target document, writing custom log tables, sending notifications, informing third-party systems

Distinction from OnAfterWriteProcessedJsonToBlob

For logic that runs after validation, a Core event already exists. It is described on the page Creating custom documents (custom processing):

[IntegrationEvent(false, false)]
local procedure OnAfterWriteProcessedJsonToBlob(var Document: Record "DXP Document"; var ProcessedJSONObj: JsonObject)

It is raised in the DXP Document Mgt. codeunit (Core) after the processed JSON has been written to the Core document – within the validation process, so it fires when Validate is chosen as well.

  OnAfterWriteProcessedJsonToBlob (Core) OnAfterValidateSQUEEZEDocument (SQUEEZE)
Raised in DXP Document Mgt. DXP SQZ Document Mgt.
Applies to all Core documents – including those from other DEXPRO source apps. You have to filter by document class yourself. SQUEEZE documents only
Context DXP Document and the processed JSON additionally the complete DXP SQZ Document Header with all recognised values, order matching and splitting information
Manual vs. automatic cannot be distinguished can be distinguished via AutomaticValidation
Timing directly after the JSON is written, still inside the Core processing at the end of the validation, after the SQUEEZE document has been completed
JSON modifiable yes (var ProcessedJSONObj) no

Recommendation:

OnBeforeValidateSQUEEZEDocument

[IntegrationEvent(false, false)]
local procedure OnBeforeValidateSQUEEZEDocument(var DocHeader: Record "DXP SQZ Document Header"; AutomaticValidation: Boolean; var IsHandled: Boolean)
Parameter Description
var DocHeader The SQUEEZE document to be validated. Changes to fields are written afterwards and flow into the target document.
AutomaticValidation false when Validate is chosen, true for automatic validation.
var IsHandled Set to true to cancel the validation. The SQUEEZE document stays open, unchanged.

The event is raised after:

and before the processed JSON is created. At this point the target document does not exist yet.

OnAfterValidateSQUEEZEDocument

[IntegrationEvent(false, false)]
local procedure OnAfterValidateSQUEEZEDocument(DocHeader: Record "DXP SQZ Document Header"; CoreDocument: Record "DXP Document"; AutomaticValidation: Boolean)
Parameter Description
DocHeader The validated SQUEEZE document. At this point its status is already Deleted – this is the regular final state of a validated document and not an error.
CoreDocument The corresponding Core document (DXP Document), including Status and Linked-to Record Id of the target document that was created.
AutomaticValidation false when Validate is chosen, true for automatic validation.

Use the Core document to determine whether the target document was actually created:

Important: custom checks do not belong in this event

OnBeforeValidateSQUEEZEDocument is the wrong place for checks that are meant to prevent a validation. An Error raised here only appears as a dialog after the user chose Validate – so the user learns about the problem at the very end of their work. Use one of the two approaches below instead. Both make the problem appear as a plausibility entry and disable the Validate action.

Approach 1 (no development): plausibility rule group

In the document class setup, define rules under Plausibility Rule Group. Every active and currently valid rule whose conditions match describes a problem and automatically creates a plausibility entry. Rules that reference the document lines are evaluated per line, so the entry points to the line concerned.

For checks such as "Shortcut Dimension 1 Code must not be empty" this is the preferred approach – no code, no extension, maintainable by the user.

Approach 2 (with development): OnAfterDoPlausibilityChecks

This event is published by all document classes with var TempPlausibilityCheckEntry and is therefore the reliable route. You insert your entry into the temporary record directly:

codeunit 50100 "My SQZ Plausibility Checks"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.",
        'OnAfterDoPlausibilityChecks', '', false, false)]
    local procedure OnAfterChecks(DocHeader: Record "DXP SQZ Document Header"; var TempPlausibilityCheckEntry: Record "DXP Plausibility Check Entry" temporary)
    var
        CostCenterMissingLbl: Label 'Please enter a cost center.';
    begin
        if DocHeader."Shortcut Dimension 1 Code" <> '' then
            exit;

        AddEntry(TempPlausibilityCheckEntry, CostCenterMissingLbl, DocHeader.RecordId(), 0);
    end;

    local procedure AddEntry(var TempPlausibilityCheckEntry: Record "DXP Plausibility Check Entry" temporary; MessageText: Text; LinkedToRecId: RecordId; LookupPageId: Integer)
    var
        NewEntryNo: Integer;
    begin
        NewEntryNo := 1;
        TempPlausibilityCheckEntry.Reset();
        TempPlausibilityCheckEntry.SetCurrentKey("Entry No.");
        if TempPlausibilityCheckEntry.FindLast() then
            NewEntryNo := TempPlausibilityCheckEntry."Entry No." + 1;

        TempPlausibilityCheckEntry.Init();
        TempPlausibilityCheckEntry."Entry No." := NewEntryNo;
        TempPlausibilityCheckEntry.Text := CopyStr(MessageText, 1, MaxStrLen(TempPlausibilityCheckEntry.Text));
        TempPlausibilityCheckEntry."Linked to Record Id" := LinkedToRecId;
        TempPlausibilityCheckEntry."Lookup Page Id" := LookupPageId;
        if TempPlausibilityCheckEntry.Insert() then;
    end;
}

The publisher object depends on the document class:

Document class Codeunit
Purchase invoice / credit memo DXP SQZ P. Inv/Crdt Memo Impl.
Order confirmation DXP SQZ P. Order Conf. Impl.
Generic document classes DXP SQZ Document Mgt.

Set Linked to Record Id and Lookup Page Id (for lines: Page::"DXP SQZ Document Subform") so that the plausibility entry becomes clickable and jumps to the record concerned.

Alternative: OnAfterDoHeaderPlausibilityChecks

PlausibilityCheck.AddPlausibilityCheckEntry(...) offers a more convenient API that saves you the manual insert into the temporary record:

[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.",
    'OnAfterDoHeaderPlausibilityChecks', '', false, false)]
local procedure OnAfterHeaderChecks(DocHeader: Record "DXP SQZ Document Header"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
var
    CostCenterMissingLbl: Label 'Please enter a cost center.';
begin
    if DocHeader."Shortcut Dimension 1 Code" = '' then
        PlausibilityCheck.AddPlausibilityCheckEntry(CostCenterMissingLbl, 0);
end;

OnAfterDoLinePlausibilityChecks works the same way for line checks; the AddPlausibilityCheckEntry(Text; LinkedToRecId; LookupPageId) overload makes the entry clickable.

Caution for the order confirmation document class: in DXP SQZ P. Order Conf. Impl., the events OnBeforeDoHeaderPlausibilityChecks, OnBeforeDoLinePlausibilityChecks, OnAfterDoHeaderPlausibilityChecks and OnAfterDoLinePlausibilityChecks were originally published without var on the PlausibilityCheck parameter. A subscriber receives a copy there, so entries it adds are lost. For this document class, use the variants with the V2 suffix from version 2.50 onwards – for example OnAfterDoHeaderPlausibilityChecksV2. Their signature is identical to the events of the other document classes. The old events remain available but are marked as obsolete.

Example 1: adding values before the handover to the target document

[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.",
    'OnBeforeValidateSQUEEZEDocument', '', false, false)]
local procedure OnBeforeValidateSetPostingDescription(var DocHeader: Record "DXP SQZ Document Header"; AutomaticValidation: Boolean; var IsHandled: Boolean)
begin
    if DocHeader."Posting Description" = '' then
        DocHeader."Posting Description" := CopyStr(DocHeader."Document Reference", 1, MaxStrLen(DocHeader."Posting Description"));
    // No Modify required – the document is written afterwards.
end;

Modifying the target document: please use the Core events

If you want to change the target document that was created, neither event on this page is the right place. Core provides events that hand you the target document directly – most of them even as a typed Purchase Header, so you can skip the detour via Linked-to Record Id and a RecordRef. These events are described on the page Events after the creation of a Core document (DXP Document):

Target document Codeunit Event
Purchase invoice / credit memo DXP P. Inv. CredMemo. Creation OnAfterProcessStandardDocument(JObject; DocRecRef)
Purchase document, general DXP Document Transfer Mgt. OnAfterCreatePurchaseDoc(var JObject; PurchaseHeader)
Purchase header DXP Document Transfer Mgt. OnAfterCreatePurchaseHeader(var JObject; PurchaseHeader)
Purchase order DXP P. Order Creation OnAfterCreatePurchaseOrder(var JObject; var PurchaseHeader)
Order confirmation DXP P. Order Confirmation OnAfterProcessStandardDocument(JObject; var PurchaseHeader)

OnAfterValidateSQUEEZEDocument is only the better choice if you additionally need values from the DXP SQZ Document Header – for example recognised values, order matching or splitting information that is not contained in the JSON of the Core document.

Example 2: reacting to manual validation only

[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.",
    'OnAfterValidateSQUEEZEDocument', '', false, false)]
local procedure OnAfterValidateNotifyUser(DocHeader: Record "DXP SQZ Document Header"; CoreDocument: Record "DXP Document"; AutomaticValidation: Boolean)
var
    ValidatedMsg: Label 'Document %1 has been forwarded.', Comment = '%1 = SQUEEZE document no.';
begin
    if AutomaticValidation then
        exit;
    if not GuiAllowed() then
        exit;

    Message(ValidatedMsg, DocHeader."No.");
end;

Notes and limitations

Events before retrieving and creating SQUEEZE documents

Audience

This documentation is for third-party developers who want to control whether Business Central retrieves documents from SQUEEZE – typically to prevent a test system from retrieving them.

It focuses on the two integration events in codeunit DXP SQZ API Mgt. that are raised before documents are retrieved.

Both events are available from SQUEEZE version 2.54.0.0.

Background: why a test system should not retrieve documents

If a test system is connected to the same SQUEEZE instance as the production system – for example because it was created as a copy of production – it retrieves documents from there as well.

When a document is created, it is set to FINISHED in SQUEEZE. A document retrieved by a test system is no longer available to the production system.

If the test system should not communicate with SQUEEZE at all, no extension is needed, see Test system retrieves documents from Squeeze. The events are meant for cases where the test system should detect and prevent the retrieval by itself, or where only certain documents should be held back.

Context: how documents are retrieved

Retrieval runs in two steps, both in codeunit DXP SQZ API Mgt.:

  1. DownloadDocumentData fetches the ids of the documents waiting for export in SQUEEZE and adds them to the import queue. It also fetches documents with status Resynchronize again.
  2. CreateCoreAndSQUEEZEDocument fetches the document for each open import queue entry, creates the Core document (DXP Document) and the SQUEEZE document, marks the entry as processed and sets the document to FINISHED in SQUEEZE.

Both steps run in the job queue and with the Download Documents now action on the Squeeze Documents, Squeeze Import Queue Entries and Squeeze Queue pages. Each step is preceded by an event with IsHandled.

Event matrix (overview)

Event When Control Typical use cases
OnBeforeDownloadDocumentData before the document ids are fetched from SQUEEZE IsHandled prevent all retrieval in a test system
OnBeforeCreateCoreAndSQUEEZEDocumentV2 before a single document is fetched and created IsHandled do not create documents that are already queued; hold back certain document classes or documents

Events in DXP SQZ API Mgt.

OnBeforeDownloadDocumentData

Raised at the very start of DownloadDocumentData, before any connection to SQUEEZE is made.

Signature:

[IntegrationEvent(false, false)]
local procedure OnBeforeDownloadDocumentData(var IsHandled: Boolean)

With IsHandled := true the whole step is skipped: no document ids are fetched, no import queue entries are written and no documents with status Resynchronize are fetched again.

 

OnBeforeCreateCoreAndSQUEEZEDocumentV2

Raised in CreateCoreAndSQUEEZEDocument for each document, before it is fetched from SQUEEZE.

Signature:

[IntegrationEvent(false, false)]
local procedure OnBeforeCreateCoreAndSQUEEZEDocumentV2(DocClass: Enum "DXP Document Class"; SQUEEZEDocId: Text[50]; var IsHandled: Boolean)

Parameters:

With IsHandled := true:

To shield a test system completely, you need this event in addition to OnBeforeDownloadDocumentData: a copy of the production system also brings along the entries that were already in its import queue. Without this event they would be created in the test system.

Skipped entries are retried on every run and count towards the up to 10 entries a run processes. If you hold back only some documents, these entries can delay the processing of the others.

The older event OnBeforeCreateCoreAndSQUEEZEDocument is obsolete and no longer raised. Use OnBeforeCreateCoreAndSQUEEZEDocumentV2 instead.

Minimal example: prevent retrieval in a sandbox

The example treats every sandbox as a test system. Adapt object ids/names to your namespace and the check in IsTestSystem to your rules.

codeunit 50102 "My SQZ Test System Guard"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnBeforeDownloadDocumentData', '', false, false)]
    local procedure SkipDownloadInTestSystem(var IsHandled: Boolean)
    begin
        if IsTestSystem() then
            IsHandled := true;
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnBeforeCreateCoreAndSQUEEZEDocumentV2', '', false, false)]
    local procedure SkipCreationInTestSystem(DocClass: Enum "DXP Document Class"; SQUEEZEDocId: Text[50]; var IsHandled: Boolean)
    begin
        if IsTestSystem() then
            IsHandled := true;
    end;

    local procedure IsTestSystem(): Boolean
    var
        EnvironmentInformation: Codeunit "Environment Information";
    begin
        // Example: every sandbox counts as a test system.
        // On-premises or for other rules, use a check of your own,
        // e.g. a flag in a setup table of your own.
        exit(EnvironmentInformation.IsSandbox());
    end;
}

With the Download Documents now action the user gets no feedback that retrieval was skipped. If you want that, show a message in the subscriber – only when GuiAllowed() is true, so the job queue is not affected.