# 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

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

## Event Parameters

<div id="bkmrk-parameter-type-descr"><div><table><thead><tr><th style="width: 173px;">Parameter</th><th style="width: 322px;">Type</th><th style="width: 314px;">Description</th></tr></thead><tbody><tr><td style="width: 173px;">`DocumentAttachment`</td><td style="width: 322px;">`Record "DXP Document Attachment"` (var)</td><td style="width: 314px;">The document attachment record that is about to be modified. Passed by reference, allowing modifications.</td></tr></tbody></table>

</div></div>## When This Event is Triggered

This event is fired during the `SaveAttachments` procedure when:

<div id="bkmrk-an-attachment-is-bei">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

</div>## Use Cases

This integration event is useful for:

<div id="bkmrk-custom-field-populat">- **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

</div>## Implementation Example

### Change The Filename Before Modify

```PASCAL
[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

<div id="bkmrk-the%C2%A0documentattachme">- The `DocumentAttachment` record is passed by reference, so any changes will be persisted
- Consider field length limitations when modifying text fields

</div>### Performance

<div id="bkmrk-keep-processing-ligh">- Keep processing lightweight as this event is called for each origin file attachment
- Consider caching frequently accessed data

</div>### Error Handling

<div id="bkmrk-implement-proper-err">- Implement proper error handling to prevent the attachment save process from failing
- Use try-functions for risky operations

</div><div id="bkmrk-"></div>## Related Events

<div id="bkmrk-onaftersaveattachmen">- `OnAfterSaveAttachment`: Triggered after each attachment is completely saved
- Consider using this alternative event if you need to perform actions after the record is saved

</div><div id="bkmrk--0"></div>## Troubleshooting

If your event subscriber isn’t being triggered:

<div id="bkmrk-test-with-origin-fil">1. Test with origin files specifically (non-origin files won’t trigger this event)

</div>

# 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).

```C#
    // 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

```PASCAL
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

```PASCAL
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

```PASCAL
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:

<div id="bkmrk-"></div>```Powershell
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

```Python
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.';
        }
    }
}
```

```Python
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)

```Python
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

```Python
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;
}

```

```Python
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)

```Python
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;
            }
        }
    }
}

```

```Python
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.

```PASCAL
[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:

```PASCAL
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:

```PASCAL
//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:

```PASCAL
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:

```PASCAL
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:

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:

- 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:

```al
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)

```al
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:

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):

- `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).

```al
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:

```al
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.

```al
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`:

```al
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

1. [Overview](#bkmrk-overview)
2. [Understanding the Document Flow](#bkmrk-understanding-the-do)
3. [JSON Data Structure](#bkmrk-json-data-structure)
4. [Quick Start: Simple Custom Processing](#bkmrk-quick-start%3A-simple-)

---

## 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. <span class="hljs-keyword">Create</span> Processed <span class="hljs-keyword">JSON</span>                                        │
│    ISourceDocument.CreateProcessedJsonFromSource()              │
│    → Builds <span class="hljs-keyword">JSON</span> structure <span class="hljs-keyword">for</span> target <span class="hljs-keyword">document</span>                  │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│ <span class="hljs-number">5.</span> <span class="hljs-keyword">Update</span> Core <span class="hljs-keyword">Document</span> <span class="hljs-keyword">with</span> <span class="hljs-string">"Custom Processing"</span> <span class="hljs-keyword">Status</span>         │
│    UpdateDocument() → <span class="hljs-keyword">Sets</span> <span class="hljs-keyword">Status</span> <span class="hljs-keyword">to</span> <span class="hljs-string">"Custom Processing"</span>        │
│    → Stores <span class="hljs-keyword">JSON</span> Processed <span class="hljs-keyword">in</span> DXP <span class="hljs-keyword">Document</span>                      │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│ <span class="hljs-number">6.</span> Your Custom Processing (THIS <span class="hljs-keyword">IS</span> <span class="hljs-keyword">WHERE</span> YOU HOOK <span class="hljs-keyword">IN</span>!)          │
│    Subscribe <span class="hljs-keyword">to</span> <span class="hljs-keyword">events</span> <span class="hljs-keyword">or</span> implement <span class="hljs-keyword">interface</span>                   │
│    → <span class="hljs-keyword">Read</span> <span class="hljs-keyword">JSON</span> Processed <span class="hljs-keyword">from</span> DXP <span class="hljs-keyword">Document</span>                      │
│    → <span class="hljs-keyword">Create</span> your target BC <span class="hljs-keyword">document</span>                             │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│ <span class="hljs-number">7.</span> <span class="hljs-keyword">Final</span> <span class="hljs-keyword">Status</span> <span class="hljs-keyword">Update</span>                                          │
│    <span class="hljs-keyword">Document</span> <span class="hljs-keyword">Status</span> → Transferred <span class="hljs-keyword">or</span> Finished                    │
│    Linked-<span class="hljs-keyword">to</span> <span class="hljs-built_in">Record</span> <span class="hljs-keyword">Id</span> → Your created BC <span class="hljs-keyword">document</span>               │
└─────────────────────────────────────────────────────────────────┘

```

### Key Status Values

<table id="bkmrk-status-description-i"><thead><tr><th>Status</th><th>Description</th></tr></thead><tbody><tr><td>**Imported**</td><td>Raw JSON received, not yet processed</td></tr><tr><td>**Transferred**</td><td>Source document created, ready for processing</td></tr><tr><td>**Custom Processing**</td><td>**Your target status** - Document ready for custom handling</td></tr><tr><td>**Finished**</td><td>Target document successfully created</td></tr><tr><td>**Deleted**</td><td>Processing complete, document archived</td></tr></tbody></table>

---

## 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

```
{
  <span class="hljs-comment">// ═══════════════════════════════════════════════════════════</span>
  <span class="hljs-comment">// HEADER SECTION - Document-level information</span>
  <span class="hljs-comment">// ═══════════════════════════════════════════════════════════</span>

  <span class="hljs-string">"Type"</span>: <span class="hljs-string">"Invoice"</span>,                    <span class="hljs-comment">// Document type: Invoice, Credit Memo, Order, etc.</span>
  <span class="hljs-string">"VendorNo"</span>: <span class="hljs-string">"VENDOR001"</span>,              <span class="hljs-comment">// Vendor/Customer number</span>
  <span class="hljs-string">"DocumentDate"</span>: <span class="hljs-string">"2025-10-15"</span>,         <span class="hljs-comment">// Document date</span>
  <span class="hljs-string">"PostingDate"</span>: <span class="hljs-string">"2025-10-16"</span>,          <span class="hljs-comment">// Posting date</span>
  <span class="hljs-string">"DocumentReference"</span>: <span class="hljs-string">"INV-2025-001"</span>,  <span class="hljs-comment">// External document number</span>
  <span class="hljs-string">"OrderNo"</span>: <span class="hljs-string">"PO-12345"</span>,                <span class="hljs-comment">// Reference to order (if applicable)</span>
  <span class="hljs-string">"PostingDescription"</span>: <span class="hljs-string">"Invoice Q4"</span>,   <span class="hljs-comment">// Posting description</span>
  <span class="hljs-string">"NetAmount"</span>: <span class="hljs-number">1000.00</span>,                 <span class="hljs-comment">// Net amount (excluding tax)</span>
  <span class="hljs-string">"TotalAmount"</span>: <span class="hljs-number">1190.00</span>,               <span class="hljs-comment">// Gross amount (including tax)</span>
  <span class="hljs-string">"TaxAmount"</span>: <span class="hljs-number">190.00</span>,                  <span class="hljs-comment">// Total tax amount</span>
  <span class="hljs-string">"Currency"</span>: <span class="hljs-string">"EUR"</span>,                    <span class="hljs-comment">// Currency code</span>

  <span class="hljs-comment">// ───────────────────────────────────────────────────────────</span>
  <span class="hljs-comment">// DIMENSIONS - Header-level dimensions as key-value pairs</span>
  <span class="hljs-comment">// ───────────────────────────────────────────────────────────</span>

  <span class="hljs-string">"Dimensions"</span>: {
    <span class="hljs-string">"DEPARTMENT"</span>: <span class="hljs-string">"SALES"</span>,
    <span class="hljs-string">"PROJECT"</span>: <span class="hljs-string">"PROJ001"</span>,
    <span class="hljs-string">"COSTCENTER"</span>: <span class="hljs-string">"CC-100"</span>
  },

  <span class="hljs-comment">// ───────────────────────────────────────────────────────────</span>
  <span class="hljs-comment">// CUSTOM FIELDS - Additional header fields (see below)</span>
  <span class="hljs-comment">// ───────────────────────────────────────────────────────────</span>

  <span class="hljs-string">"CustomFields"</span>: [
    {
      <span class="hljs-string">"Id"</span>: <span class="hljs-number">50100</span>,
      <span class="hljs-string">"Name"</span>: <span class="hljs-string">"CustomerNo"</span>,
      <span class="hljs-string">"Value"</span>: <span class="hljs-string">"CUST001"</span>,
      <span class="hljs-string">"Caption"</span>: <span class="hljs-string">"Customer Number"</span>
    },
    {
      <span class="hljs-string">"Id"</span>: <span class="hljs-number">50101</span>,
      <span class="hljs-string">"Name"</span>: <span class="hljs-string">"DeliveryTerms"</span>,
      <span class="hljs-string">"Value"</span>: <span class="hljs-string">"EXW"</span>,
      <span class="hljs-string">"Caption"</span>: <span class="hljs-string">"Delivery Terms"</span>
    }
  ],

  <span class="hljs-comment">// ───────────────────────────────────────────────────────────</span>
  <span class="hljs-comment">// METADATA - System-generated field mappings</span>
  <span class="hljs-comment">// ───────────────────────────────────────────────────────────</span>

  <span class="hljs-string">"Metadata"</span>: [
    {
      <span class="hljs-string">"DocumentClass"</span>: <span class="hljs-string">"DXP Invoice / Credit Memo"</span>,
      <span class="hljs-string">"FieldType"</span>: <span class="hljs-string">"Header"</span>,
      <span class="hljs-string">"FieldId"</span>: <span class="hljs-number">101</span>,
      <span class="hljs-string">"Value"</span>: <span class="hljs-string">"Additional Info"</span>
    }
  ],

  <span class="hljs-comment">// ═══════════════════════════════════════════════════════════</span>
  <span class="hljs-comment">// LINES SECTION - Document line items</span>
  <span class="hljs-comment">// ═══════════════════════════════════════════════════════════</span>

  <span class="hljs-string">"Lines"</span>: [
    {
      <span class="hljs-comment">// ─────────────────────────────────────────────────────────</span>
      <span class="hljs-comment">// Line Basic Information</span>
      <span class="hljs-comment">// ─────────────────────────────────────────────────────────</span>

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

      <span class="hljs-comment">// ─────────────────────────────────────────────────────────</span>
      <span class="hljs-comment">// Line Amounts & Tax</span>
      <span class="hljs-comment">// ─────────────────────────────────────────────────────────</span>

      <span class="hljs-string">"NetAmount"</span>: <span class="hljs-number">950.00</span>,             <span class="hljs-comment">// Line net amount</span>
      <span class="hljs-string">"TotalAmount"</span>: <span class="hljs-number">1130.50</span>,          <span class="hljs-comment">// Line gross amount</span>
      <span class="hljs-string">"TaxRate"</span>: <span class="hljs-number">19.0</span>,                 <span class="hljs-comment">// Tax percentage</span>
      <span class="hljs-string">"VATBusPostingGroup"</span>: <span class="hljs-string">"DOMESTIC"</span>,
      <span class="hljs-string">"VATProdPostingGroup"</span>: <span class="hljs-string">"STANDARD"</span>,
      <span class="hljs-string">"GenBusPostingGroup"</span>: <span class="hljs-string">"DOMESTIC"</span>,
      <span class="hljs-string">"GenProdPostingGroup"</span>: <span class="hljs-string">"RETAIL"</span>,

      <span class="hljs-comment">// ─────────────────────────────────────────────────────────</span>
      <span class="hljs-comment">// Order Reference (for matching)</span>
      <span class="hljs-comment">// ─────────────────────────────────────────────────────────</span>

      <span class="hljs-string">"OrderNo"</span>: <span class="hljs-string">"PO-12345"</span>,           <span class="hljs-comment">// Referenced order number</span>
      <span class="hljs-string">"OrderLineNo"</span>: <span class="hljs-number">10000</span>,            <span class="hljs-comment">// Referenced order line number</span>
      <span class="hljs-string">"ReceiptNo"</span>: <span class="hljs-string">"RCP-001"</span>,          <span class="hljs-comment">// Referenced receipt number</span>
      <span class="hljs-string">"ReceiptLineNo"</span>: <span class="hljs-number">10000</span>,          <span class="hljs-comment">// Referenced receipt line number</span>

      <span class="hljs-comment">// ─────────────────────────────────────────────────────────</span>
      <span class="hljs-comment">// Line Dimensions</span>
      <span class="hljs-comment">// ─────────────────────────────────────────────────────────</span>

      <span class="hljs-string">"Dimensions"</span>: {
        <span class="hljs-string">"DEPARTMENT"</span>: <span class="hljs-string">"PROD"</span>,
        <span class="hljs-string">"PROJECT"</span>: <span class="hljs-string">"PROJ001"</span>
      },

      <span class="hljs-comment">// ─────────────────────────────────────────────────────────</span>
      <span class="hljs-comment">// Line Custom Fields</span>
      <span class="hljs-comment">// ─────────────────────────────────────────────────────────</span>

      <span class="hljs-string">"CustomFields"</span>: [
        {
          <span class="hljs-string">"Id"</span>: <span class="hljs-number">50200</span>,
          <span class="hljs-string">"Name"</span>: <span class="hljs-string">"SerialNo"</span>,
          <span class="hljs-string">"Value"</span>: <span class="hljs-string">"SN-12345"</span>,
          <span class="hljs-string">"Caption"</span>: <span class="hljs-string">"Serial Number"</span>
        }
      ],

      <span class="hljs-comment">// ─────────────────────────────────────────────────────────</span>
      <span class="hljs-comment">// Line Metadata</span>
      <span class="hljs-comment">// ─────────────────────────────────────────────────────────</span>

      <span class="hljs-string">"Metadata"</span>: [
        {
          <span class="hljs-string">"DocumentClass"</span>: <span class="hljs-string">"DXP Invoice / Credit Memo"</span>,
          <span class="hljs-string">"FieldType"</span>: <span class="hljs-string">"Line"</span>,
          <span class="hljs-string">"FieldId"</span>: <span class="hljs-number">201</span>,
          <span class="hljs-string">"Value"</span>: <span class="hljs-string">"Line-specific metadata"</span>
        }
      ]
    }
    <span class="hljs-comment">// ... additional lines</span>
  ]
}

```

### 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 <span class="hljs-number">50100</span> <span class="hljs-string">"My Custom Document Handler"</span>
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::<span class="hljs-string">"DXP Document Mgt."</span>, 
        <span class="hljs-string">'OnAfterWriteProcessedJsonToBlob'</span>, <span class="hljs-string">''</span>, <span class="hljs-literal">true</span>, <span class="hljs-literal">true</span>)]
    local <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">OnAfterWriteProcessedJsonToBlob</span><span class="hljs-params">(
        <span class="hljs-keyword">var</span> Document: Record "DXP Document"; 
        <span class="hljs-keyword">var</span> ProcessedJSONObj: JsonObject)</span>
    <span class="hljs-title">begin</span>
        <span class="hljs-comment">// Only handle Custom Processing status</span>
        <span class="hljs-title">if</span> <span class="hljs-title">Document</span>.<span class="hljs-title">Status</span> <> <span class="hljs-title">Document</span>.<span class="hljs-title">Status</span>:</span>:<span class="hljs-string">"Custom Processing"</span> <span class="hljs-keyword">then</span>
            <span class="hljs-keyword">exit</span>;

        // Create your custom document
        CreateMyCustomDocument(Document, ProcessedJSONObj);
    <span class="hljs-keyword">end</span>;

    local <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">CreateMyCustomDocument</span><span class="hljs-params">(
        <span class="hljs-keyword">var</span> Document: Record "DXP Document"; 
        ProcessedJSON: JsonObject)</span>
    <span class="hljs-title">var</span>
        <span class="hljs-title">CoreTokenMgt</span>:</span> Codeunit <span class="hljs-string">"DXP Core Token Mgt."</span>;
        JsonHelper: Codeunit <span class="hljs-string">"DXP Json Helper"</span>;
        MyCustomHeader: Record <span class="hljs-string">"My Custom Document Header"</span>;
        MyCustomLine: Record <span class="hljs-string">"My Custom Document Line"</span>;
        LineJArray: JsonArray;
        LineJToken: JsonToken;
        LineJObj: JsonObject;
    <span class="hljs-keyword">begin</span>
        // ═══════════════════════════════════════════════════════
        // <span class="hljs-number">1</span>. CREATE HEADER
        // ═══════════════════════════════════════════════════════

        MyCustomHeader.Init();
        MyCustomHeader.<span class="hljs-string">"No."</span> := <span class="hljs-string">''</span>;  // Will be assigned by number series

        // Read standard header fields
        MyCustomHeader.<span class="hljs-string">"Vendor No."</span> := 
            JsonHelper.ValAsTxt(ProcessedJSON, CoreTokenMgt.GetVendorNoTok(), <span class="hljs-literal">false</span>);
        MyCustomHeader.<span class="hljs-string">"Document Date"</span> := 
            JsonHelper.ValAsDate(ProcessedJSON, CoreTokenMgt.GetDocDateTok(), <span class="hljs-literal">false</span>);
        MyCustomHeader.<span class="hljs-string">"Document Reference"</span> := 
            JsonHelper.ValAsTxt(ProcessedJSON, CoreTokenMgt.GetDocReferenceTok(), <span class="hljs-literal">false</span>);

        MyCustomHeader.Insert(<span class="hljs-literal">true</span>);

        // ═══════════════════════════════════════════════════════
        // <span class="hljs-number">2</span>. PROCESS DIMENSIONS (<span class="hljs-keyword">if</span> your document supports them)
        // ═══════════════════════════════════════════════════════

        ProcessHeaderDimensions(ProcessedJSON, MyCustomHeader);

        // ═══════════════════════════════════════════════════════
        // <span class="hljs-number">3</span>. PROCESS CUSTOM FIELDS
        // ═══════════════════════════════════════════════════════

        ProcessCustomFields(ProcessedJSON, MyCustomHeader);

        // ═══════════════════════════════════════════════════════
        // <span class="hljs-number">4</span>. CREATE LINES
        // ═══════════════════════════════════════════════════════

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

        foreach LineJToken <span class="hljs-keyword">in</span> LineJArray <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
            LineJObj := LineJToken.AsObject();

            MyCustomLine.Init();
            MyCustomLine.<span class="hljs-string">"Document No."</span> := MyCustomHeader.<span class="hljs-string">"No."</span>;
            MyCustomLine.<span class="hljs-string">"Line No."</span> := GetNextLineNo(MyCustomHeader.<span class="hljs-string">"No."</span>);

            // Read line fields
            MyCustomLine.<span class="hljs-string">"Item No."</span> := 
                JsonHelper.ValAsTxt(LineJObj, CoreTokenMgt.GetNoTok(), <span class="hljs-literal">false</span>);
            MyCustomLine.Description := 
                JsonHelper.ValAsTxt(LineJObj, CoreTokenMgt.GetDescriptionTok(), <span class="hljs-literal">false</span>);
            MyCustomLine.Quantity := 
                JsonHelper.ValAsDec(LineJObj, CoreTokenMgt.GetQtyTok(), <span class="hljs-literal">false</span>);
            MyCustomLine.<span class="hljs-string">"Unit of Measure"</span> := 
                JsonHelper.ValAsTxt(LineJObj, CoreTokenMgt.GetUoMTok(), <span class="hljs-literal">false</span>);

            MyCustomLine.Insert(<span class="hljs-literal">true</span>);

            // Process line dimensions <span class="hljs-keyword">and</span> custom fields <span class="hljs-keyword">if</span> needed
            ProcessLineDimensions(LineJObj, MyCustomLine);
            ProcessLineCustomFields(LineJObj, MyCustomLine);
        <span class="hljs-keyword">end</span>;

        // ═══════════════════════════════════════════════════════
        // <span class="hljs-number">5</span>. UPDATE CORE DOCUMENT STATUS
        // ═══════════════════════════════════════════════════════

        UpdateCoreDocument(Document, MyCustomHeader);
    <span class="hljs-keyword">end</span>;

    local <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ProcessHeaderDimensions</span><span class="hljs-params">(ProcessedJSON: JsonObject; <span class="hljs-keyword">var</span> MyCustomHeader: Record "My Custom Document Header")</span>
    <span class="hljs-title">var</span>
        <span class="hljs-title">CoreTokenMgt</span>:</span> Codeunit <span class="hljs-string">"DXP Core Token Mgt."</span>;
        DocTransferMgt: Codeunit <span class="hljs-string">"DXP Document Transfer Mgt."</span>;
        DimensionsJObj: JsonObject;
        DimSetID: Integer;
    <span class="hljs-keyword">begin</span>
        // Read dimensions from JSON
        <span class="hljs-keyword">if</span> ProcessedJSON.Contains(CoreTokenMgt.GetDimensionsTok()) <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
            ProcessedJSON.Get(CoreTokenMgt.GetDimensionsTok(), DimensionsJObj);

            // Convert <span class="hljs-keyword">to</span> Dimension Set ID
            <span class="hljs-keyword">if</span> DocTransferMgt.GetDimSetIdFromJsonObj(DimensionsJObj, DimSetID) <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
                MyCustomHeader.<span class="hljs-string">"Dimension Set ID"</span> := DimSetID;
                MyCustomHeader.Modify(<span class="hljs-literal">true</span>);
            <span class="hljs-keyword">end</span>;
        <span class="hljs-keyword">end</span>;
    <span class="hljs-keyword">end</span>;

    local <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ProcessCustomFields</span><span class="hljs-params">(ProcessedJSON: JsonObject; <span class="hljs-keyword">var</span> MyCustomHeader: Record "My Custom Document Header")</span>
    <span class="hljs-title">var</span>
        <span class="hljs-title">CoreTokenMgt</span>:</span> Codeunit <span class="hljs-string">"DXP Core Token Mgt."</span>;
        JsonHelper: Codeunit <span class="hljs-string">"DXP Json Helper"</span>;
        CustomFieldsJArray: JsonArray;
        CustomFieldJToken: JsonToken;
        CustomFieldJObj: JsonObject;
        FieldName: Text;
        FieldValue: Text;
    <span class="hljs-keyword">begin</span>
        // Read custom fields array
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> ProcessedJSON.Contains(CoreTokenMgt.GetCustomFieldsTok()) <span class="hljs-keyword">then</span>
            <span class="hljs-keyword">exit</span>;

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

        foreach CustomFieldJToken <span class="hljs-keyword">in</span> CustomFieldsJArray <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
            CustomFieldJObj := CustomFieldJToken.AsObject();
            FieldName := JsonHelper.ValAsTxt(CustomFieldJObj, CoreTokenMgt.GetNameTok(), <span class="hljs-literal">false</span>);
            FieldValue := JsonHelper.ValAsTxt(CustomFieldJObj, CoreTokenMgt.GetValueTok(), <span class="hljs-literal">false</span>);

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

        MyCustomHeader.Modify(<span class="hljs-literal">true</span>);
    <span class="hljs-keyword">end</span>;

    local <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ProcessLineDimensions</span><span class="hljs-params">(LineJObj: JsonObject; <span class="hljs-keyword">var</span> MyCustomLine: Record "My Custom Document Line")</span>
    <span class="hljs-title">var</span>
        <span class="hljs-title">CoreTokenMgt</span>:</span> Codeunit <span class="hljs-string">"DXP Core Token Mgt."</span>;
        DocTransferMgt: Codeunit <span class="hljs-string">"DXP Document Transfer Mgt."</span>;
        DimensionsJObj: JsonObject;
        DimSetID: Integer;
    <span class="hljs-keyword">begin</span>
        <span class="hljs-keyword">if</span> LineJObj.Contains(CoreTokenMgt.GetDimensionsTok()) <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
            LineJObj.Get(CoreTokenMgt.GetDimensionsTok(), DimensionsJObj);

            <span class="hljs-keyword">if</span> DocTransferMgt.GetDimSetIdFromJsonObj(DimensionsJObj, DimSetID) <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
                MyCustomLine.<span class="hljs-string">"Dimension Set ID"</span> := DimSetID;
                MyCustomLine.Modify(<span class="hljs-literal">true</span>);
            <span class="hljs-keyword">end</span>;
        <span class="hljs-keyword">end</span>;
    <span class="hljs-keyword">end</span>;

    local <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ProcessLineCustomFields</span><span class="hljs-params">(LineJObj: JsonObject; <span class="hljs-keyword">var</span> MyCustomLine: Record "My Custom Document Line")</span>
    <span class="hljs-title">var</span>
        <span class="hljs-title">CoreTokenMgt</span>:</span> Codeunit <span class="hljs-string">"DXP Core Token Mgt."</span>;
        JsonHelper: Codeunit <span class="hljs-string">"DXP Json Helper"</span>;
        CustomFieldsJArray: JsonArray;
        CustomFieldJToken: JsonToken;
        CustomFieldJObj: JsonObject;
        FieldName: Text;
        FieldValue: Text;
    <span class="hljs-keyword">begin</span>
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> LineJObj.Contains(CoreTokenMgt.GetCustomFieldsTok()) <span class="hljs-keyword">then</span>
            <span class="hljs-keyword">exit</span>;

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

        foreach CustomFieldJToken <span class="hljs-keyword">in</span> CustomFieldsJArray <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
            CustomFieldJObj := CustomFieldJToken.AsObject();
            FieldName := JsonHelper.ValAsTxt(CustomFieldJObj, CoreTokenMgt.GetNameTok(), <span class="hljs-literal">false</span>);
            FieldValue := JsonHelper.ValAsTxt(CustomFieldJObj, CoreTokenMgt.GetValueTok(), <span class="hljs-literal">false</span>);

            // Map <span class="hljs-keyword">to</span> your line custom fields
            <span class="hljs-keyword">case</span> FieldName <span class="hljs-keyword">of</span>
                <span class="hljs-string">'SerialNo'</span>:
                    MyCustomLine.<span class="hljs-string">"Serial No."</span> := CopyStr(FieldValue, <span class="hljs-number">1</span>, <span class="hljs-number">50</span>);
                // Add more field mappings
            <span class="hljs-keyword">end</span>;
        <span class="hljs-keyword">end</span>;

        MyCustomLine.Modify(<span class="hljs-literal">true</span>);
    <span class="hljs-keyword">end</span>;

    local <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">UpdateCoreDocument</span><span class="hljs-params">(<span class="hljs-keyword">var</span> Document: Record "DXP Document"; MyCustomHeader: Record "My Custom Document Header")</span>
    <span class="hljs-title">var</span>
        <span class="hljs-title">DocumentMgt</span>:</span> Codeunit <span class="hljs-string">"DXP Document Mgt."</span>;
    <span class="hljs-keyword">begin</span>
        // Update the core document <span class="hljs-keyword">to</span> link it <span class="hljs-keyword">to</span> your created document
        Document.Status := Document.Status::Transferred;
        Document.<span class="hljs-string">"Linked-to Record Id"</span> := MyCustomHeader.RecordId;
        Document.Modify(<span class="hljs-literal">true</span>);

        // Optionally, transfer attachments from Core <span class="hljs-keyword">to</span> your document
        TransferAttachments(Document, MyCustomHeader);
    <span class="hljs-keyword">end</span>;

    local <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TransferAttachments</span><span class="hljs-params">(Document: Record "DXP Document"; MyCustomHeader: Record "My Custom Document Header")</span>
    <span class="hljs-title">var</span>
        <span class="hljs-title">DocAttachment</span>:</span> Record <span class="hljs-string">"DXP Document Attachment"</span>;
        MyDocAttachment: Record <span class="hljs-string">"Document Attachment"</span>;
        InStr: InStream;
    <span class="hljs-keyword">begin</span>
        DocAttachment.SetRange(<span class="hljs-string">"Document No."</span>, Document.<span class="hljs-string">"No."</span>);
        <span class="hljs-keyword">if</span> DocAttachment.FindSet() <span class="hljs-keyword">then</span>
            <span class="hljs-keyword">repeat</span>
                MyDocAttachment.Init();
                MyDocAttachment.ID := <span class="hljs-number">0</span>;

                DocAttachment.<span class="hljs-string">"File Content"</span>.CreateInStream(InStr);
                MyDocAttachment.SaveAttachmentFromStream(
                    InStr, 
                    MyCustomHeader.RecordId, 
                    DocAttachment.<span class="hljs-string">"File Name"</span>);
            <span class="hljs-keyword">until</span> DocAttachment.Next() = <span class="hljs-number">0</span>;
    <span class="hljs-keyword">end</span>;

    local <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">GetNextLineNo</span><span class="hljs-params">(DocumentNo: Code[20])</span>:</span> Integer
    <span class="hljs-keyword">var</span>
        MyCustomLine: Record <span class="hljs-string">"My Custom Document Line"</span>;
    <span class="hljs-keyword">begin</span>
        MyCustomLine.SetRange(<span class="hljs-string">"Document No."</span>, DocumentNo);
        <span class="hljs-keyword">if</span> MyCustomLine.FindLast() <span class="hljs-keyword">then</span>
            <span class="hljs-keyword">exit</span>(MyCustomLine.<span class="hljs-string">"Line No."</span> + <span class="hljs-number">10000</span>);
        <span class="hljs-keyword">exit</span>(<span class="hljs-number">10000</span>);
    <span class="hljs-keyword">end</span>;
}

```

### Key Helper Codeunits

<table id="bkmrk-codeunit-purpose-dxp"><thead><tr><th>Codeunit</th><th>Purpose</th></tr></thead><tbody><tr><td>**DXP Core Token Mgt.**</td><td>Provides token names for JSON fields (GetVendorNoTok(), GetDocDateTok(), etc.)</td></tr><tr><td>**DXP Json Helper**</td><td>JSON parsing utilities (ValAsTxt(), ValAsDate(), ReadJArrayFromObj(), etc.)</td></tr><tr><td>**DXP Document Transfer Mgt.**</td><td>Dimension processing, metadata transfer, custom field handling</td></tr><tr><td>**DXP Document Mgt.**</td><td>Core document management (UpdateDocument(), UpdateDocumentStatus(), etc.)</td></tr></tbody></table>

---

*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

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

```al
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]`

```al
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:

```al
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:

```al
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:

```al
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

```al
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

```al
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

```al
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).

```al
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):

```al
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:

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):

```al
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.

```al
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;
}
```