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: 
 
 
 An attachment is being processed from SQUEEZE 
 The attachment is identified as an origin file ( IsOriginFile = true ) 
 The system has set the origin file name and  DXP Is Origin File  flag 
 Just before the  DocumentAttachment.Modify(true)  call 
 
 
 Use Cases 
 This integration event is useful for: 
 
 
 Custom Field Population : Setting additional custom fields on the document attachment 
 File Name Transformation : Applying custom naming conventions or formatting 
 Metadata Enhancement : Adding custom metadata or tags to attachments 
 Validation Logic : Implementing custom validation before the record is saved 
 Integration Requirements : Preparing data for external system integrations 
 
 
 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 
 
 
 The  DocumentAttachment  record is passed by reference, so any changes will be persisted 
 Consider field length limitations when modifying text fields 
 
 
 Performance 
 
 
 Keep processing lightweight as this event is called for each origin file attachment 
 Consider caching frequently accessed data 
 
 
 Error Handling 
 
 
 Implement proper error handling to prevent the attachment save process from failing 
 Use try-functions for risky operations 
 
 
 
 Related Events 
 
 
 OnAfterSaveAttachment : Triggered after each attachment is completely saved 
 Consider using this alternative event if you need to perform actions after the record is saved 
 
 
 
 Troubleshooting 
 If your event subscriber isn’t being triggered: 
 
 
 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: 
 
 DocHeader."Buy-from Vendor No."  - Kreditorennummer (validiert) 
 DocHeader."Document Date"  - Belegdatum 
 DocHeader."Document Reference"  - Belegnummer 
 DocHeader."Core Document No."  - Verknüpfung zu Anhängen 
 Alle anderen Belegfelder sind verfügbar 
 
 Example results: 
 
 Original :  "Rechnung_2024_001.pdf" 
 Mit Kreditorenprefix :  "VEND001_Rechnung_2024_001.pdf" 
 Quelldatei :  "ORIGINAL_VEND001_Rechnung_2024_001.pdf" 
 
 Best Practices: 
 
 Duplicate check : Always check whether prefix already exists 
 Length validation : New file names may have a maximum of 1024 characters.

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: 
 
 Collect relevant document numbers based on business rules 
 A detailed comparison is performed for each document number found. 
 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 
 
 DocHeader : The document header with the data to be compared 
 OrderNoList : A list of document numbers for reconciliation 
 IsHandled : Controls whether standard processing should be skipped 
 
 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 : 
 
 Third-party developers typically work with published icons , not with the entire source code. 
 The SQUEEZE app handles downloading + import queue + creation of the core document as well as the rudimentary creation of the SQUEEZE document, taking field mapping into account , as soon as setup is complete. 
 Your extension takes over the business logic once the core document exists: create or extend source records, validate them, generate “Processed JSON,” and optionally create your own target object or document. 
 
 
 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: 
 
 DXP Document Class extend and bind your implementation:  DXP ISource Document . 
 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. 
 
 
 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). 
 
 
 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. 
 
 
 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. 
 
 
 The SQUEEZE app calls your implementation ISourceDocument.CreateSource(Document) .
 
 Your implementation begins here. 
 
 
 
 After step 6, you will have: 
 
 A Core‑Container‑Document ( DXP Document ). 
 A SQUEEZE document in the SQUEEZE app. 
 Your source records should exist once CreateSource has completed successfully. 
 
 
 2) What you implement (Interfaces) 
 Source contract, i.e., squeeze page: DXP ISource Document 
 You implement this to: 
 
 Create your editable source data sets from DXP Document.JSON Raw 
 Validate source data records 
 Generate “Processed JSON” and transfer it to core processing 
 
 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: 
 
 Validating the DXP SQZ Document Class ID triggers the automatic loading of the required configuration from SQUEEZE (e.g., DXP SQZ Export Interface ID and DXP SQZ Line Table ID ). 
 If the call fails, these API-derived fields are cleared again. 
 
 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: 
 
 Provide standard field mapping “Squeeze Field Name → AL Field No.” via InitFieldMapping(...) . 
 Alternatively, map fields via the assignment pages (custom field assignment). 
 
 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 : 
 
 Validate App Field = Validate the app field during the assignment. 
 Validate (Target) = Perform validation when transferring to the target field. 
 
 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: 
 
 Read DXP Document.JSON Raw . 
 Create SQUEEZE‑Source‑Header/Lines ( DXP SQZ Document Header  /  DXP SQZ Document Line ). 
 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): 
 
 
 SqzDocumentMgt.CreateSQUEEZEDocHeader(Document, DocHeader, RawJson) 
 
 Creates exactly one data record in DXP SQZ Document Header for the Core‑document ( DXP Document ). 
 Sets identification fields as Core Document No. and the SQUEEZE‑ API Document ID . 
 Assigns header fields from JSON based on the configured mappings: 
 
 DXP Field Mapping (field mapping) 
 DXP Custom Field Mapping (Custom field mapping) 
 
 
 Respects DXP Custom Field Mapping.Validate App Field and thus decides whether the OnValidate trigger is executed when assigning. 
 Executes the integration event OnBeforeModifySQUEEZEDocumentHeader(...) as an extension hook. 
 Standard post-processing is also performed for the built-in standard SQUEEZE classes (e.g., alternative IBAN/VAT ID determination, supplier update, standard posting date, duplicate check). 
 
 
 
 SqzDocumentMgt.CreateSQUEEZEDocLine(DocHeader, RawJson) 
 
 Creates the DXP SQZ Document Line ‑data sets based on the JSON‑Rows. 
 Assign line fields using the same mapping principle (standard + custom; optionally with validation triggers). 
 Fill the Core Document No. in the lines. 
 For standard SQUEEZE classes, standard line post-processing is also applied (e.g., transfer of recognized values). 
 Persists line-related auxiliary data (e.g., coordinates/metadata that use the UI and matching logic). 
 
 
 
 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) 
 
 No entries in DXP SQZ Import Queue → Document class setup for SQUEEZE is incomplete. 
 Core‑ DXP Document exists, but no source record → Your CreateSource(Document) has failed or returns false . 
 Source exists, but target is not created → CreateProcessedJsonFromSource is not called or DXP Target Document Process is not configured. 
 Incorrect trigger behavior when assigning → Check DXP Custom Field Mapping.Validate App Field and  Validate .
 
 Validate controls whether validation of the target field (OnValidate) is performed. 
 
 
 
 
 10) What Core does automatically 
 When you call up DXP Document Mgt.UpdateDocument(...) : 
 
 Core stores the “Processed JSON” in DXP Document . JSON Processed . 
 Core sets DXP Document . Status to the transferred status. 
 Core linked/updated DXP Document . Linked-to Record Id with the record, which your DXP IDocument Processing ‑implementation returns. 
 Core transfers Core attachments to the returned data set (and typically appends the “Processed JSON” as a .json attachment).

Custom Document Creation (Custom Processing)
  
 Table of Contents 
 
 Overview 
 Understanding the Document Flow 
 JSON Data Structure 
 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 
 
 How to intercept documents marked for custom processing 
 The JSON structure containing document header, lines, metadata, and custom fields 
 How to create your custom document 
 
 
 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 

 // ─────────────────────────────────────────────────────────── 
 // 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: 
 
 
 User-defined fields from your app 
 
 
 Mapped via Custom Field Mapping 
 
 
 Can be mapped to any field in the Squeeze Validation 
 
 
 Example: Adding a "Customer No." to the Squeeze Validation 
 
 
 Metadata: 
 
 
 System-generated fields 
 
 
 Extracted by the Squeeze System but not mapped to any Field in BC 
 
 
 
 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.

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: 
 
 Page:  "DXP SQZ Viewer Part"  (PageType =  CardPart ) 
 Source table:  "DXP SQZ Document Header" 
 
 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: 
 
 Page  "DXP SQZ Document (Generic)"  (Card, SourceTable  DXP SQZ Document Header ) bettet den Viewer‑Part als FactBox ein und verdrahtet das Resident‑Sharing zur generischen Lines‑Subform. 
 Page  "DXP SQZ Doc. Generic Subform"  (ListPart, SourceTable  DXP SQZ Document Line ) enthält eine bekannte, funktionierende Minimal‑Implementierung von  MarkRow() . 
 
 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 
 
 
 Ihre Extension muss von der DEXPRO SQUEEZE App abhängig sein (damit Page/Control Add-ins als Symbole verfügbar sind). 
 
 
 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. 
 
 
 
 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: 
 
 Setze am Part nicht  Enabled = false . Wenn eine FactBox‑Part deaktiviert ist, kann das verhindern, dass das Control Add-in „ready“ wird – dadurch bleibt der Viewer ggf. dauerhaft leer. 
 
 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: 
 
 Viewer‑Part als FactBox hinzufügen –  ohne   SubPageLink . 
 In  OnAfterGetCurrRecord()  den relevanten  Record "DXP SQZ Document Header"  auflösen. 
 Den Datensatz per  CurrPage.<PartName>.Page.SetSqzRecord(SqzHeader);  in den Part pushen. 
 
 Warum  SetSqzRecord ? 
 
 Im DEXPRO SQUEEZE Viewer‑Part läuft das Laden der URL in  OnAfterGetCurrRecord() . 
 Beim Navigieren innerhalb einer Seite (z. B. Wechsel zwischen Purchase Invoices) kann  SetRecord(...)  allein dazu führen, dass die FactBox nicht sofort neu lädt. 
 SetSqzRecord(...)  ist dafür gedacht, den Header sauber neu zu binden und einen Refresh auszulösen, sodass die korrekte Preview ohne Schließen/Öffnen der Seite geladen wird. 
 
 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: 
 
 Belegübersicht : Zeigt eine Übersichtliche Darstellung des Viewers an. 
 Positionsfokus : Zeigt eine schmalere Darstellung des Viewers an. 
 Abgedockt : Öffnet den Viewer in einem externen Fenster 
 
 Für Third‑party Entwickler: 
 
 Sie müssen keine eigene Mode‑Logik implementieren. 
 Nutzen Sie die Actions des Viewer‑Parts (erscheinen im FactBox Action‑Menü). 
 
 
 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: 
 
 Die Viewer‑UI ist eine Control Add‑in Instanz, die in einer Codeunit DXP SQUEEZE Viewer Resident  gespeichert wird. 
 Wenn Ihre Lines‑Page eine  eigene  Variable  DXP SQUEEZE Viewer Resident  verwendet (die nicht vom Viewer‑Part initialisiert wurde), hat  ApiMgt.PostMessage(...)  keine Control‑Add‑in Instanz zum Ansprechen – es wird nichts markiert. 
 
 4.1 Pattern (Host‑Page verdrahtet Viewer‑Resident in Lines) 
 Annahmen: 
 
 Ihre Header‑Page hat:
 
 part(SQZViewer; "DXP SQZ Viewer Part") 
 part(DocumentLines; <Ihre Lines ListPart>) 
 
 
 Ihre Lines‑ListPart stellt eine Procedure bereit:
 
 SetViewerResident(var NewResident: Codeunit "DXP SQUEEZE Viewer Resident") 
 
 
 
 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: 
 
 Das gehört in  OnAfterGetCurrRecord()  der Host‑Page (dort ist der aktuelle Header stabil, und die Parts existieren). 
 Wenn Ihre Host‑Page andere Part‑Namen verwendet, passen Sie  CurrPage.<PartName>.Page...  entsprechend an. 
 
 4.2 Was die Lines‑Subform tun muss 
 Ihre Lines‑Page sollte  MarkRow()  in  OnAfterGetCurrRecord()  aufrufen und  MarkRow()  ähnlich zur Standard‑Seite implementieren: 
 
 Wenn der externe Viewer aktiv ist (Abgedockt), nur die State‑Tabelle aktualisieren. 
 Ansonsten  ApiMgt.PostMessage(ApiMgt.MarkFieldByCoordinatesObj(Rec.RecordId), Resident)  aufrufen. 
 
 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: 
 
 Ist das SQUEEZE Modul aktiviert und das Setup vollständig? 
 Hat der verknüpfte  DXP SQZ Document Header  Datensatz eine gültige  "API Document ID" ? 
 Ist der Viewer‑Modus aktuell  Abgedockt  und ein externer Viewer offen?
 
 Der interne Viewer lädt nicht, solange der externe Viewer geöffnet ist. 
 
 
 
 5.2 FactBox ist sichtbar, aber Control Add-in initialisiert nicht 
 
 Deaktiviere den Viewer‑FactBox‑Part nicht über  Enabled = false . 
 Stelle sicher, dass die FactBox tatsächlich angezeigt wird (FactBoxes können vom Benutzer eingeklappt/ausgeblendet werden). 
 
 5.3 MarkRow hebt nichts hervor 
 
 Stelle sicher, dass die Lines‑Page dieselbe Viewer‑Resident‑Instanz wie der Viewer‑Part nutzt (siehe Abschnitt 4). 
 Stelle sicher, dass Koordinaten für den Zeilen‑Datensatz existieren ( DXP SQZ Coordinates Mgt.  muss Koordinaten zu  Rec.RecordId  finden). 
 
 
 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 
 
 Der Viewer‑State ist session‑getrieben (via  DXP Viewer State ). Wenn mehrere Seiten mit Viewer gleichzeitig offen sind, können Modus/External‑Viewer‑State alle beeinflussen. 
 Der Viewer‑Part ist für Datensätze mit SQUEEZE‑Bezug gedacht; er versucht nicht, automatisch „irgendeinen“ Header zu suchen. 
 
 
 8) Beispiel: Viewer auf einem Zielbeleg anzeigen (Purchase Invoice) 
 Dieser Abschnitt adressiert ein häufiges Third‑party Szenario: 
 Annahmen: 
 
 Der Zielbeleg (z. B.  Purchase Invoice ) wurde aus einem Core‑Beleg erstellt. 
 Der Core‑Beleg ist weiterhin in der Tabelle  "DXP Document"  verfügbar. 
 Der Core‑Beleg speichert eine Rückverknüpfung zum Ziel‑Datensatz im Feld  "Linked-to Record Id" . 
 Der SQUEEZE Quell‑Header existiert noch und kann aus dem Core‑Beleg aufgelöst werden (typischerweise über  Core Document No. ). 
 
 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: 
 
 Einen FactBox‑Part 
 Ein  ShowViewer ‑Flag, um den Part auszublenden, wenn kein SQZ Header aufgelöst werden kann. 
 SetSqzRecord(...) , damit die Preview beim Wechsel des Belegs sofort korrekt neu lädt. 
 Ein  SingleInstance  Context‑Codeunit, um Viewer‑Resident und SQZ‑Kontext mit der  Purch. Invoice Subform  zu teilen (FactBoxes und Subforms sind separate Pages). 
 
 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: 
 
 Wenn Sie die Preview nach Zielbeleg‑Erstellung  nicht  anzeigen möchten, bauen Sie eine Guard‑Logik auf der Host‑Page ein (Viewer‑Part dann nicht binden, wenn der SQZ Header „Deleted“ ist). 
 
 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: 
 
 User navigiert durch  Purchase Lines . 
 Der eingebettete SQUEEZE Viewer hebt den entsprechenden Bereich (Koordinaten) des originalen Belegs, bzw. der Viewer-Abbildung hervor. 
 
 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: 
 
 
 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." 
 
 
 
 
 
 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: 
 
 Den Viewer‑Resident (damit  PostMessage(...)  die richtige Control‑Add‑in Instanz trifft) 
 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;
}