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
- Alternative use of the SQUEEZE attachments
- Adjustment of attachment file names after validation
- Adding a field in the validation (from version 2.10)
- Implementation of a user-defined, automatic order reconciliation
- Implementation of a new, generic document class
- Custom Document Creation (Custom Processing)
- Rule-based next process step: routing documents to Custom Processing
- Embedding the SQUEEZE Viewer part in your own pages WIP
- Events after creating a SQUEEZE document (CreateSource)
- Events after creating a Core document (DXP Document)
- Adding a field in the validation (up to Squeeze BC App version 1.*)
- Adding a field in the validation (from version 2.0)
- Events when validating a SQUEEZE document (Validate action)
- Events before retrieving and creating SQUEEZE documents
Modify Squeeze origin attachment
Overview
The OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment integration event allows third-party developers to customize document attachment records before they are modified during the SQUEEZE attachment processing workflow. An origin attachment is the relevant attachment that leads to the creation of a document (e.g. the vendor´s invoice).
Event Declaration
[IntegrationEvent(false, false)]
local procedure OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment(var DocumentAttachment: Record "DXP Document Attachment")
begin
end;
Event Parameters
| Parameter | Type | Description |
|---|---|---|
DocumentAttachment |
Record "DXP Document Attachment" (var) |
The document attachment record that is about to be modified. Passed by reference, allowing modifications. |
When This Event is Triggered
This event is fired during the SaveAttachments procedure when:
- An attachment is being processed from SQUEEZE
- The attachment is identified as an origin file (
IsOriginFile = true) - The system has set the origin file name and
DXP Is Origin Fileflag - Just before the
DocumentAttachment.Modify(true)call
Use Cases
This integration event is useful for:
- Custom Field Population: Setting additional custom fields on the document attachment
- File Name Transformation: Applying custom naming conventions or formatting
- Metadata Enhancement: Adding custom metadata or tags to attachments
- Validation Logic: Implementing custom validation before the record is saved
- Integration Requirements: Preparing data for external system integrations
Implementation Example
Change The Filename Before Modify
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment', '', false, false)]
local procedure OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment(var DocumentAttachment: Record "DXP Document Attachment")
var
CoreDocument: Record "DXP Document";
SQZDocumentHeader: Record "DXP SQZ Document Header";
Vendor: Record Vendor;
NewFileName: Text[250];
VendorPrefix: Text[50];
begin
// Get the core document using the document attachment's Document No.
if not CoreDocument.Get(DocumentAttachment."Document No.") then
exit;
// Check if the core document has a linked SQUEEZE document
if IsNullGuid(CoreDocument."Linked-to Record Id") then
exit;
// Get the SQZ Document Header using the SystemId from Linked-to Record Id
if not GetSQZDocumentHeaderFromRecordId(CoreDocument."Linked-to Record Id", SQZDocumentHeader) then
exit;
// Check if Buy-from Vendor No. is populated
if SQZDocumentHeader."Buy-from Vendor No." = '' then
exit;
// Get the vendor record
if not Vendor.Get(SQZDocumentHeader."Buy-from Vendor No.") then
exit;
// Create vendor prefix from Search Name (fallback to Name if Search Name is empty)
if Vendor."Search Name" <> '' then
VendorPrefix := Vendor."Search Name"
else
VendorPrefix := Vendor.Name;
// Clean the vendor prefix (remove invalid filename characters and limit length)
VendorPrefix := CleanFilenameText(VendorPrefix, 30);
// Check if vendor prefix already exists in filename to avoid duplicates
if DocumentAttachment."File Name".StartsWith(VendorPrefix + '_') then
exit;
// Create new filename with vendor prefix
NewFileName := VendorPrefix + '_' + DocumentAttachment."File Name";
// Update the attachment filename
DocumentAttachment."File Name" := NewFileName;
end;
local procedure GetSQZDocumentHeaderFromRecordId(LinkedRecordId: Guid; var SQZDocumentHeader: Record "DXP SQZ Document Header"): Boolean
var
RecRef: RecordRef;
SystemIdFieldRef: FieldRef;
begin
// Method 1: Try to get the record directly if LinkedRecordId is actually a SystemId
if SQZDocumentHeader.GetBySystemId(LinkedRecordId) then
exit(true);
// Method 2: If that fails, we need to find the record another way
// This assumes the Linked-to Record Id might be stored differently
SQZDocumentHeader.Reset();
SQZDocumentHeader.SetRange(SystemId, LinkedRecordId);
exit(SQZDocumentHeader.FindFirst());
end;
local procedure CleanFilenameText(InputText: Text; MaxLength: Integer): Text
var
CleanText: Text;
begin
// Remove invalid filename characters
CleanText := DelChr(InputText, '=', '<>|"/\:*?');
// Replace spaces with underscores for better filename compatibility
CleanText := CleanText.Replace(' ', '_');
// Limit length
CleanText := CopyStr(CleanText, 1, MaxLength);
exit(CleanText);
end;
Important Considerations
Data Integrity
- The
DocumentAttachmentrecord is passed by reference, so any changes will be persisted - Consider field length limitations when modifying text fields
Performance
- Keep processing lightweight as this event is called for each origin file attachment
- Consider caching frequently accessed data
Error Handling
- Implement proper error handling to prevent the attachment save process from failing
- Use try-functions for risky operations
Related Events
OnAfterSaveAttachment: Triggered after each attachment is completely saved- Consider using this alternative event if you need to perform actions after the record is saved
Troubleshooting
If your event subscriber isn’t being triggered:
- Test with origin files specifically (non-origin files won’t trigger this event)
Alternative use of the SQUEEZE attachments
The following briefly explains how the attachments downloaded by SQUEEZE for BC can be used for a scenario that differs from the intended use.
After creating the purchase document from the validated SQUEEZE document, you have the option of accessing the attachments ( assuming they have been downloaded in advance).
// Set IsHandled to true and implement your code
// Do not forget to deactivate "Transfer attachments to target document" in the Document Class Setup
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Mgt.", 'OnBeforeTransferCoreAttachmentsToStandardDocument', '', false, false)]
local procedure DXPDocMgtOnBeforeTransferCoreAttachmentsToStandardDocument(Document: Record "DXP Document"; var IsHandled: Boolean)
begin
IsHandled := true;
HandleCoreAttachmentsAccordingToYourNeeds(Document, IsHandled);
end;
procedure HandleCoreAttachmentsAccordingToYourNeeds(Document: Record "DXP Document"; var IsHandled: Boolean)
var
CoreAttachments: Record "DXP Document Attachment";
InStr: InStream;
begin
CoreAttachments.SetRange("Document No.", Document."No.");
CoreAttachments.IsEmpty() then
exit;
CoreAttachments.FindSet();
repeat
CoreAttachments.Calcfields(Attachment);
CoreAttachments.Attachment.CreateInStream(InStr);
// Here you can handle the file stream according to your needs
[...]
until CoreAttachments.Next() = 0;
end;
Adjustment of attachment file names after validation
Overview
This guide shows how developers can customize the file names of attachments in DEXPRO SQUEEZE. The OnBeforeAddLineToDocumentJObj event is executed after the plausibility check, ensuring that all document data is fully validated and available.
Available Event
OnBeforeAddLineToDocumentJObj
For invoices/credit notes: Codeunit 70954657 "DXP SQZ P. Inv/Crdt Memo Impl."
For orderconfirmations: Codeunit 70954658 "DXP SQZ P. Order Conf. Impl."
When accessed: After header processing, before line processing, after plausibility check
Implementation examples
Example 1: Simple vendor prefix for invoices/credit notes
codeunit 50100 "Custom Invoice Filename"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnBeforeAddLineToDocumentJObj', '', false, false)]
local procedure OnBeforeAddLineToDocumentJObjInvoice(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
begin
CustomizeAttachmentFilenames(DocHeader);
end;
local procedure CustomizeAttachmentFilenames(DocHeader: Record "DXP SQZ Document Header")
var
DocumentAttachment: Record "DXP Document Attachment";
VendorPrefix: Text;
NewFileName: Text[1024];
begin
// Beenden, wenn keine Kreditorinformationen vorhanden
if DocHeader."Buy-from Vendor No." = '' then
exit;
// Kreditorenprefix erstellen
VendorPrefix := DocHeader."Buy-from Vendor No." + '_';
// Alle Anhänge für dieses Dokument bearbeiten
DocumentAttachment.Reset();
DocumentAttachment.SetRange("Document No.", DocHeader."Core Document No.");
if DocumentAttachment.FindSet(true) then
repeat
// Überspringen, wenn Prefix bereits vorhanden
if not DocumentAttachment."File Name".Contains(VendorPrefix) then begin
// Neuen Dateinamen mit Prefix erstellen
NewFileName := VendorPrefix + DocumentAttachment."File Name";
// Aktualisieren
DocumentAttachment."File Name" := CopyStr(NewFileName, 1, MaxStrLen(DocumentAttachment."File Name"));
DocumentAttachment.Modify(true);
end;
until DocumentAttachment.Next() = 0;
end;
}
Example 2: Order confirmations - vendor prefix
codeunit 50101 "Custom Order Conf Filename"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Order Conf. Impl.", 'OnBeforeAddLineToDocumentJObj', '', false, false)]
local procedure OnBeforeAddLineToDocumentJObjOrderConf(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
begin
CustomizeAttachmentFilenames(DocHeader);
end;
local procedure CustomizeAttachmentFilenames(DocHeader: Record "DXP SQZ Document Header")
var
DocumentAttachment: Record "DXP Document Attachment";
VendorPrefix: Text;
NewFileName: Text[1024];
begin
// Beenden, wenn keine Kreditorinformationen vorhanden
if DocHeader."Buy-from Vendor No." = '' then
exit;
// Kreditorenprefix erstellen
VendorPrefix := DocHeader."Buy-from Vendor No." + '_';
// Alle Anhänge für dieses Dokument bearbeiten
DocumentAttachment.Reset();
DocumentAttachment.SetRange("Document No.", DocHeader."Core Document No.");
if DocumentAttachment.FindSet(true) then
repeat
// Überspringen, wenn Prefix bereits vorhanden
if not DocumentAttachment."File Name".Contains(VendorPrefix) then begin
// Neuen Dateinamen mit Prefix erstellen
NewFileName := VendorPrefix + DocumentAttachment."File Name";
// Aktualisieren
DocumentAttachment."File Name" := CopyStr(NewFileName, 1, MaxStrLen(DocumentAttachment."File Name"));
DocumentAttachment.Modify(true);
end;
until DocumentAttachment.Next() = 0;
end;
}
Example 3: Both document types - Different prefixes for source files
codeunit 50102 "Advanced Filename Handling"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnBeforeAddLineToDocumentJObj', '', false, false)]
local procedure OnBeforeAddLineToDocumentJObjInvoice(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
begin
CustomizeFilenamesWithSourceHandling(DocHeader);
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Order Conf. Impl.", 'OnBeforeAddLineToDocumentJObj', '', false, false)]
local procedure OnBeforeAddLineToDocumentJObjOrderConf(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
begin
CustomizeFilenamesWithSourceHandling(DocHeader);
end;
local procedure CustomizeAttachmentFilenames(DocHeader: Record "DXP SQZ Document Header")
var
DocumentAttachment: Record "DXP Document Attachment";
VendorPrefix: Text;
NewFileName: Text[1024];
begin
// Beenden, wenn keine Kreditorinformationen vorhanden
if DocHeader."Buy-from Vendor No." = '' then
exit;
// Kreditorenprefix erstellen
VendorPrefix := DocHeader."Buy-from Vendor No." + '_';
// Alle Anhänge für dieses Dokument bearbeiten
DocumentAttachment.Reset();
DocumentAttachment.SetRange("Document No.", DocHeader."Core Document No.");
if DocumentAttachment.FindSet(true) then
repeat
// Überspringen, wenn Prefix bereits vorhanden
if not DocumentAttachment."File Name".Contains(VendorPrefix) then begin
// Neuen Dateinamen mit Prefix erstellen
NewFileName := VendorPrefix + DocumentAttachment."File Name";
// Aktualisieren
DocumentAttachment."File Name" := CopyStr(NewFileName, 1, MaxStrLen(DocumentAttachment."File Name"));
DocumentAttachment.Modify(true);
end;
until DocumentAttachment.Next() = 0;
end;
}
Practical information
Available data at the time of the event:
DocHeader."Buy-from Vendor No."- Kreditorennummer (validiert)DocHeader."Document Date"- BelegdatumDocHeader."Document Reference"- BelegnummerDocHeader."Core Document No."- Verknüpfung zu Anhängen- Alle anderen Belegfelder sind verfügbar
Example results:
- Original:
"Rechnung_2024_001.pdf" - Mit Kreditorenprefix:
"VEND001_Rechnung_2024_001.pdf" - Quelldatei:
"ORIGINAL_VEND001_Rechnung_2024_001.pdf"
Best Practices:
- Duplicate check: Always check whether prefix already exists
- Length validation: New file names may have a maximum of 1024 characters.
Adding a field in the validation (from version 2.10)
The following example extension shows how developers can add fields in the validation (here: document class invoice/credit note).
Optional integration events incl. sample procedures:
codeunit 50100 EventSubs
{
//
// [If you want to perform plausibility checks on the newly added header field, this is the place]
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnAfterDoHeaderPlausibilityChecks', '', false, false)]
local procedure SQZPInvCrdtMemoImplOnBeforeDoHeaderPlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
begin
CheckCustomer(PlausibilityCheck, DocHeader."Customer No.");
end;
//
// [If you want to perform plausibility checks on the newly added line field, this is the place]
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnAfterDoLinePlausibilityChecks', '', false, false)]
local procedure SQZPInvCrdtMemoImplOnAfterDoLinePlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; DocLine: Record "DXP SQZ Document Line"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
begin
CheckCustomer(PlausibilityCheck, DocLine."Customer No.");
end;
[TryFunction]
local procedure CustomerExists(CustomerNo: Code[20])
var
Customer: Record Customer;
begin
Customer.Get(CustomerNo);
end;
local procedure CheckCustomer(var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt."; CustomerNo: Code[20]): Boolean
begin
if CustomerNo = '' then
exit(false);
if not CustomerExists(CustomerNo) then begin
PlausibilityCheck.AddPlausibilityCheckEntry(GetLastErrorText(), Page::"Customer List");
exit(false);
end;
exit(true);
end;
}
Table Extensions:
SQUEEZE 4 BC
tableextension 50100 "SQZ Doc. Header Ext." extends "DXP SQZ Document Header"
{
fields
{
field(50100; "Customer No."; Code[20])
{
TableRelation = Customer;
ValidateTableRelation = false;
Caption = 'Customer No.';
}
}
}
tableextension 50101 "SQZ Doc. Line Ext." extends "DXP SQZ Document Line"
{
fields
{
field(50100; "Customer No."; Code[20])
{
TableRelation = Customer;
ValidateTableRelation = false;
Caption = 'Customer No.';
}
}
}
Target document (here: Purchase Document)
tableextension 50102 "Purchase Header Ext." extends "Purchase Header"
{
fields
{
field(50100; "DXP Customer No."; Code[20])
{
DataClassification = CustomerContent;
TableRelation = Customer;
Caption = 'DEXPRO Customer No.';
}
}
}
Page Extensions:
SQUEEZE 4 BC
pageextension 50100 "SQZ Document Ext." extends "DXP SQZ Document v2"
{
layout
{
addafter(BuyFromVendorInternal)
{
field("Customer No. Internal"; Rec."Customer No.")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Customer No. field.';
//The following code is optional. It is used to highlight a recognized value in the Squeeze Viewer
trigger OnAssistEdit()
begin
MarkField(Rec.FieldNo("Customer No."), Rec."Customer No.", Rec);
end;
trigger OnValidate()
begin
CheckPlausibility();
end;
}
}
addafter(BuyFromVendorExternal)
{
field("Customer No."; Rec."Customer No.")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Customer No. field.';
//The following code is optional. It is used to highlight a recognized value in the Squeeze Viewer
trigger OnAssistEdit()
var
ApiMgt: Codeunit "DXP SQZ API Mgt.";
ViewerResident: Codeunit "DXP SQUEEZE Viewer Resident";
begin
MarkField(Rec.FieldNo("Customer No."), Rec."Customer No.", Rec);
end;
trigger OnValidate()
begin
CheckPlausibility();
end;
}
}
}
local procedure MarkField(AppFldNo: Integer; FieldVal: Variant; DocHeader: Record "DXP SQZ Document Header")
var
ApiMgt: Codeunit "DXP SQZ API Mgt.";
ViewerResident: Codeunit "DXP SQUEEZE Viewer Resident";
begin
ViewerResident := GetViewerResident();
ApiMgt.MarkField(AppFldNo, FieldVal, ViewerResident, DocHeader);
end;
}
pageextension 50101 "SQZ Document Sf. Ext." extends "DXP SQZ Document Subform"
{
layout
{
addafter("No.")
{
field("Customer No."; Rec."Customer No.")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Customer No. field.';
}
}
}
}
Target document (here: Purchase document)
pageextension 50102 "Purchase Invoice Ext." extends "Purchase Invoice"
{
layout
{
addafter("Buy-from Vendor No.")
{
field("DXP Customer No."; Rec."DXP Customer No.")
{
ToolTip = 'Specifies the value of the DEXPRO Customer No. field.';
ApplicationArea = all;
}
}
}
}
pageextension 50103 "Purchase Inv. Sf Ext." extends "Purch. Invoice Subform"
{
layout
{
addafter("No.")
{
field("DXP Customer No."; Rec."DXP Customer No.")
{
ToolTip = 'Specifies the value of the DEXPRO Customer No. field.';
ApplicationArea = all;
}
}
}
}
Implementation of a user-defined, automatic order reconciliation
Introduction
Automatic order matching is an important part of document processing in Squeeze for Business Central. In the standard system, the system compares incoming documents with existing orders and goods receipts. This documentation shows you how you can extend this matching process to include your own document types.
The standard implementation uses a three-step process:
- Collect relevant document numbers based on business rules
- A detailed comparison is performed for each document number found.
- The positions read by Squeeze are then enriched with the data found.
We will retain this proven approach in the following example implementation.
Integration point
The central integration point is the OnBeforePerformAutomaticOrdermatch event in codeunit 70954632 “DXP SQZ Document Mgt.”. This event is called after header and line data has been created.
[IntegrationEvent(false, false)]
local procedure OnBeforePerformAutomaticOrdermatch(
DocHeader: Record "DXP SQZ Document Header";
var OrderNoList: List of [Code[20]];
var IsHandled: Boolean)
Parameter
DocHeader: The document header with the data to be comparedOrderNoList: A list of document numbers for reconciliationIsHandled: Controls whether standard processing should be skipped
Implementation example
Technical implementation
1. Extension of the document type enum
First, we extend the possible document types to include our own type:
enumextension 50100 "Custom Order Match Doc. Type" extends "DXP Order Match Document Type"
{
value(50000; "Custom")
{
Caption = 'Custom Document';
}
}
2. Implementation of the matching logic
The central point of our implementation is a code unit that controls the synchronization process. The integration point is:
//codeunit 50100 "Custom Document Matching"
//{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforePerformAutomaticOrdermatch', '', false, false)]
local procedure OnBeforePerformAutomaticOrdermatch(
DocHeader: Record "DXP SQZ Document Header";
var OrderNoList: List of [Code[20]];
var IsHandled: Boolean)
var
CustomSourceDoc: Record "Custom Source Document";
CustomDocNo: Code[20];
begin
// Take control of the matching process and prevent standard processing
IsHandled := true;
// Find potential matching documents
CustomSourceDoc.SetRange("Vendor No.", DocHeader."Buy-from Vendor No.");
// Add your specific document status or type filters
CustomSourceDoc.SetRange("Document Type", CustomSourceDoc."Document Type"::Order);
CustomSourceDoc.SetRange(Status, CustomSourceDoc.Status::Released);
// Add matching document numbers to the list
if CustomSourceDoc.FindSet() then
repeat
CustomDocNo := CustomSourceDoc."No.";
if IsDocumentEligibleForMatching(CustomSourceDoc, DocHeader) then
if not OrderNoList.Contains(CustomDocNo) then
OrderNoList.Add(CustomDocNo);
until CustomSourceDoc.Next() = 0;
// Process all collected documents
ProcessMatchingDocuments(DocHeader, OrderNoList);
end;
local procedure IsDocumentEligibleForMatching(
CustomSourceDoc: Record "Custom Source Document";
DocHeader: Record "DXP SQZ Document Header"): Boolean
begin
// Implement business rules for document selection
// For example:
if CustomSourceDoc."Document Date" > DocHeader."Document Date" then
exit(false);
if CustomSourceDoc."Currency Code" <> DocHeader."Currency Code" then
exit(false);
// Check for open lines that can be matched
if not HasOpenLinesToMatch(CustomSourceDoc) then
exit(false);
exit(true);
end;
local procedure HasOpenLinesToMatch(CustomSourceDoc: Record "Custom Source Document"): Boolean
var
CustomSourceLine: Record "Custom Source Line";
begin
CustomSourceLine.SetRange("Document No.", CustomSourceDoc."No.");
CustomSourceLine.SetFilter("Outstanding Quantity", '>0');
exit(not CustomSourceLine.IsEmpty());
end;
//}
3. Processing of the documents found
After collecting the relevant document numbers, the actual comparison takes place:
local procedure ProcessMatchingDocuments(
DocHeader: Record "DXP SQZ Document Header";
OrderNoList: List of [Code[20]])
var
TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary;
OrderNo: Code[20];
begin
foreach OrderNo in OrderNoList do begin
// Get matching entries for current document
GetMatchEntries(OrderNo, TempMatchEntry, DocHeader);
if not TempMatchEntry.IsEmpty() then
// Try to find and assign matching lines
TryMatchDocumentLines(TempMatchEntry, DocHeader);
end;
end;
local procedure GetMatchEntries(
DocumentNo: Code[20];
var TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary;
DocHeader: Record "DXP SQZ Document Header")
var
CustomSourceLine: Record "Custom Source Line";
begin
TempMatchEntry.Reset();
TempMatchEntry.DeleteAll();
// Get lines from custom document
CustomSourceLine.SetRange("Document No.", DocumentNo);
if CustomSourceLine.FindSet() then
repeat
// Create match entry for each relevant line
CreateMatchEntry(CustomSourceLine, TempMatchEntry);
until CustomSourceLine.Next() = 0;
end;
local procedure CreateMatchEntry(
CustomSourceLine: Record "Custom Source Line";
var TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary)
begin
TempMatchEntry.Init();
TempMatchEntry."Document Type" := "DXP Order Match Document Type"::Custom;
TempMatchEntry."Document No." := CustomSourceLine."Document No.";
TempMatchEntry."Document Line No." := CustomSourceLine."Line No.";
TempMatchEntry."No." := CustomSourceLine."Item No.";
TempMatchEntry.Quantity := CustomSourceLine.Quantity;
TempMatchEntry."Direct Unit Cost" := CustomSourceLine."Unit Price";
TempMatchEntry."Line Amount" := CustomSourceLine.Amount;
TempMatchEntry.Insert();
end;
The matching logic in detail
The actual reconciliation of document lines is performed according to defined business rules:
local procedure TryMatchDocumentLines(
var TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary;
DocHeader: Record "DXP SQZ Document Header")
var
DocLine: Record "DXP SQZ Document Line";
MatchSetup: Record "Custom Match Setup";
HasCustomSetup: Boolean;
begin
// Get configuration
MatchSetup.Get();
// Process each potential match entry
if TempMatchEntry.FindSet() then
repeat
DocLine.Reset();
DocLine.SetRange("Document No.", DocHeader."No.");
DocLine.SetRange("Buy-from Vendor No.", TempMatchEntry."Buy-from Vendor No.");
DocLine.SetFilter("Allocated Document Line No.", '%1', 0); // Only unallocated lines
// Try matching strategies in order of precision
if TryExactMatch(DocLine, TempMatchEntry) then
CheckTolerancesAndAllocate(TempMatchEntry, DocLine, MatchSetup)
else
if TryItemReferenceMatch(DocLine, TempMatchEntry) then
CheckTolerancesAndAllocate(TempMatchEntry, DocLine, MatchSetup)
else
if TryDescriptionMatch(DocLine, TempMatchEntry) then
CheckTolerancesAndAllocate(TempMatchEntry, DocLine, MatchSetup)
else
if TryBasicValuesMatch(DocLine, TempMatchEntry) then
CheckTolerancesAndAllocate(TempMatchEntry, DocLine, MatchSetup);
until TempMatchEntry.Next() = 0;
end;
local procedure TryExactMatch(
var DocLine: Record "DXP SQZ Document Line";
TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary): Boolean
begin
DocLine.SetRange(Type, TempMatchEntry.Type);
DocLine.SetFilter("No.", '%1|%2', TempMatchEntry."No.", TempMatchEntry."Item Reference No.");
DocLine.SetRange("SQZ Quantity", TempMatchEntry.Quantity);
exit(not DocLine.IsEmpty());
end;
local procedure TryItemReferenceMatch(
var DocLine: Record "DXP SQZ Document Line";
TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary): Boolean
begin
DocLine.SetRange(Type); // Clear previous filters
DocLine.SetRange("Item Reference No.", TempMatchEntry."Item Reference No.");
exit(not DocLine.IsEmpty());
end;
local procedure TryDescriptionMatch(
var DocLine: Record "DXP SQZ Document Line";
TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary): Boolean
begin
DocLine.SetRange("Item Reference No."); // Clear previous filter
DocLine.SetFilter("SQZ Description", '@*' + TempMatchEntry.Description + '*');
exit(not DocLine.IsEmpty());
end;
local procedure TryBasicValuesMatch(
var DocLine: Record "DXP SQZ Document Line";
TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary): Boolean
begin
DocLine.SetRange("SQZ Description"); // Clear previous filter
DocLine.SetRange("SQZ Quantity", TempMatchEntry.Quantity);
DocLine.SetRange("SQZ Unit Price", TempMatchEntry."Direct Unit Cost");
exit(not DocLine.IsEmpty());
end;
local procedure CheckTolerancesAndAllocate(
TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary;
var DocLine: Record "DXP SQZ Document Line";
MatchSetup: Record "Custom Match Setup")
begin
DocLine.FindFirst(); // We know there is at least one line
if IsMatchWithinTolerances(DocLine, TempMatchEntry, MatchSetup) then
AssignMatchData(DocLine, TempMatchEntry);
end;
local procedure IsMatchWithinTolerances(
DocLine: Record "DXP SQZ Document Line";
TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary;
MatchSetup: Record "Custom Match Setup"): Boolean
var
QtyTolerance: Decimal;
AmountTolerance: Decimal;
begin
QtyTolerance := MatchSetup."Quantity Tolerance";
AmountTolerance := MatchSetup."Amount Tolerance";
if Abs(DocLine."SQZ Quantity" - TempMatchEntry.Quantity) > QtyTolerance then
exit(false);
if Abs(DocLine."SQZ Unit Price" - TempMatchEntry."Direct Unit Cost") > AmountTolerance then
exit(false);
exit(true);
end;
local procedure AssignMatchData(
var DocLine: Record "DXP SQZ Document Line";
TempMatchEntry: Record "DXP SQZ Order Match Entry" temporary)
begin
// Assign the matched data to the document line
DocLine.Validate("Allocated Document Type", "DXP Order Match Document Type"::Custom);
DocLine.Validate("Allocated Document No.", TempMatchEntry."Document No.");
DocLine.Validate("Allocated Document Line No.", TempMatchEntry."Document Line No.");
DocLine.Validate("Allocated Quantity", TempMatchEntry.Quantity);
DocLine.Validate("Allocated Unit Price", TempMatchEntry."Direct Unit Cost");
DocLine.Validate("Allocated Line Amount", TempMatchEntry."Line Amount");
DocLine.Validate("Allocated Line Discount %", TempMatchEntry."Line Discount %");
DocLine.Modify(true);
end;
Implementation of a new, generic document class
SQUEEZE → Core → Your processing
This guide is deliberately focused and pipeline-oriented: It shows how document data is downloaded from SQUEEZE, processed in queues, and persisted in DEXPRO Core—and where a third-party extension comes into play.
Term: In this guide, “SQUEEZE app” refers to the Business Central extension that is connected to the SQUEEZE API and controls imports to DEXPRO Core.
Assumptions:
- Third-party developers typically work with published icons, not with the entire source code.
- The SQUEEZE app handles downloading + import queue + creation of the core document as well as the rudimentary creation of the SQUEEZE document, taking field mapping into account, as soon as setup is complete.
- Your extension takes over the business logic once the core document exists: create or extend source records, validate them, generate “Processed JSON,” and optionally create your own target object or document.
1) The end-to-end process SQUEEZE → Core
Once the document class and setup are configured, the SQUEEZE app's import pipeline typically runs as follows:
DXP Document Classextend and bind your implementation:DXP ISource Document.- Configure settings for this Document class‑Setup.
- When validating
DXP SQZ Document Class ID, the SQUEEZE app automatically retrieves the required setup values from SQUEEZE (e.g.,DXP SQZ Export Interface IDandDXP SQZ Line Table ID) and fills them in.
- When validating
- The SQUEEZE app job calls
DXP SQZ API Mgt.DownloadDocumentData(). Can also be called via “Download documents”.- New entries are created in the DXP SQZ import queue (document ID + target client).
- The SQUEEZE app calls
DXP SQZ Import Queue Mgt.TransferImportQueueEntries().- The orchestration code unit
DXP Sqz Create Documentis executed for the entries in the import queue.
- The orchestration code unit
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 Rawis filled.
- The SQUEEZE app calls your implementation
ISourceDocument.CreateSource(Document).- Your implementation begins here.
After step 6, you will have:
- A Core‑Container‑Document (
DXP Document). - A SQUEEZE document in the SQUEEZE app.
- Your source records should exist once
CreateSourcehas completed successfully.
2) What you implement (Interfaces)
Source contract, i.e., squeeze page: DXP ISource Document
You implement this to:
- Create your editable source data sets from
DXP Document.JSON Raw - Validate source data records
- Generate “Processed JSON” and transfer it to core processing
Target contract, i.e., target document: DXP IDocument Processing
You implement this if Core is to create/update its own target data set (your own table/document) from the “Processed JSON.”
3) Step 1 — Define document class and assign source implementation
Create an enum extension and bind your source implementation:
enumextension 50100 "MY Document Class Ext." extends "DXP Document Class"
{
value(50100; "MY Generic Document")
{
Caption = 'My Generic Document';
Implementation = "DXP ISource Document" = "MY Source Document Impl.";
}
}
4) Step 2 — Configure document class setup (so that the import queue is filled)
Configure the SQUEEZE-specific fields in the document class setup for your new document class.
Important:
- Validating the
DXP SQZ Document Class IDtriggers the automatic loading of the required configuration from SQUEEZE (e.g.,DXP SQZ Export Interface IDandDXP SQZ Line Table ID). - If the call fails, these API-derived fields are cleared again.
Result: The SQUEEZE app can execute DownloadDocumentData() and create entries in the DXP SQZ Import Queue for your class.
5) Step 3 — Ensure field mapping (downloaded mapping + your default values)
The SQUEEZE app can download the field catalog of the connected Squeeze client and persist it in Core Mapping tables (per document class).
Your role as a document class implementer:
- Provide standard field mapping “Squeeze Field Name → AL Field No.” via
InitFieldMapping(...). - Alternatively, map fields via the assignment pages (custom field assignment).
5.1 Implement InitFieldMapping (independent example)
codeunit 50101 "MY Source Document Impl." implements "DXP ISource Document"
{
procedure InitFieldMapping(var HeaderMapping: Dictionary of [Text, Integer]; var LineMapping: Dictionary of [Text, Integer])
var
Header: Record "DXP SQZ Document Header";
Line: Record "DXP SQZ Document Line";
begin
Clear(HeaderMapping);
Clear(LineMapping);
// Header fields
HeaderMapping.Add('vendorNo', Header.FieldNo("Buy-from Vendor No."));
HeaderMapping.Add('documentDate', Header.FieldNo("Document Date"));
HeaderMapping.Add('postingDate', Header.FieldNo("Posting Date"));
HeaderMapping.Add('reference', Header.FieldNo("Document Reference"));
// Line fields
LineMapping.Add('description', Line.FieldNo(Description));
LineMapping.Add('quantity', Line.FieldNo(Quantity));
LineMapping.Add('unitPrice', Line.FieldNo("Direct Unit Cost"));
end;
// other interface methods omitted here...
}
5.2 Validation switch when assigning fields: Validate app field
When the SQUEEZE app or your source creation logic writes JSON values to “app/source fields,” you can control whether the OnValidate trigger is executed.
In DXP Custom Field Mapping:
Validate App Field= Validate the app field during the assignment.Validate(Target) = Perform validation when transferring to the target field.
Use this, for example, if extension fields have business logic in OnValidate or if you want to assign deliberately trigger-free (higher performance).
6) Step 4 — Create Source data sets in CreateSource
CreateSource(Document) is called by the SQUEEZE app immediately after DXP Document Mgt.AddDocument(...) as soon as the core container data record exists.
Minimum responsibilities:
- Read
DXP Document.JSON Raw. - Create SQUEEZE‑Source‑Header/Lines (
DXP SQZ Document Header/DXP SQZ Document Line). - Connect Core‑document with SQUEEZE‑Source‑Header via
UpdateDocumentAfterTransfer.
Optional (recommended if you want functionality equivalent to the standard SQUEEZE document classes): 4) Download attachments if DXP Download Attachments is active. 5) Perform automatic validation if enabled for the document.
6.1 Example: Create SQUEEZE‑Header + Lines with generic helpers
What the helpers do (so you don't have to implement it twice):
-
SqzDocumentMgt.CreateSQUEEZEDocHeader(Document, DocHeader, RawJson)- Creates exactly one data record in
DXP SQZ Document Headerfor 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 Fieldand 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).
- Creates exactly one data record in
-
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).
- Creates the
procedure CreateSource(Document: Record "DXP Document"): Boolean
var
DocClassSetup: Record "DXP Document Class Setup";
AutoValidationMgt: Codeunit "DXP Automatic Validation Mgt.";
JsonHelper: Codeunit "DXP Json Helper";
DocMgt: Codeunit "DXP Document Mgt.";
SqzDocumentMgt: Codeunit "DXP SQZ Document Mgt.";
SqzApiMgt: Codeunit "DXP SQZ API Mgt.";
RawJson: JsonObject;
DocHeader: Record "DXP SQZ Document Header";
AutomaticValidation: Boolean;
begin
if (not Document."JSON Raw".HasValue()) or (Document.Status <> Document.Status::Imported) then
exit(false);
// 1) Unverarbeitetes JSON lesen
RawJson := JsonHelper.JObjectFromBlob(Document.RecordId(), Document.FieldNo("JSON Raw"));
// 2) SQUEEZE‑Source‑Dokument mit den generischen Hilfsfunktionen erstellen
// (Feldzuordnung + optionales Validierungs‑Trigger‑Verhalten wird im Helper behandelt)
SqzDocumentMgt.CreateSQUEEZEDocHeader(Document, DocHeader, RawJson);
SqzDocumentMgt.CreateSQUEEZEDocLine(DocHeader, RawJson);
// 3) Core → SQUEEZE‑Source‑Header verknüpfen
DocMgt.UpdateDocumentAfterTransfer(Document."No.", Document.Status::Transferred, DocHeader."API Document ID", DocHeader.RecordId());
// 4) Optional: Anhänge herunterladen und speichern
// (gesteuert über die Dokumentenklassen‑Einrichtung)
if DocClassSetup.Get(DocHeader."Document Class") then
if DocClassSetup."DXP Download Attachments" then
SqzApiMgt.DownloadAndSaveAttachments(DocHeader);
// 5) Optional: Automatische Validierung
// Wenn aktiviert, führt die Validierung die Plausibilitätsprüfungen aus und erstellt das „Processed JSON“.
AutomaticValidation := AutoValidationMgt.AutomaticValidationActivated(DocHeader."No.");
if AutomaticValidation then
SqzDocumentMgt.ValidateSQUEEZEDocument(DocHeader, AutomaticValidation);
exit(true);
end;
7) Step 5 — Validation + Handover „Processed JSON“
7.1 Validation: IsSourceDataPlausible
Use the Core Plausibility Framework to ensure that errors are displayed consistently:
procedure IsSourceDataPlausible(Source: Variant; var TempPlausibilityCheckEntry: Record "DXP Plausibility Check Entry" temporary): Boolean
var
PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.";
Header: Record "DXP SQZ Document Header";
begin
if not Source.IsRecord then
exit(false);
Header := Source;
TempPlausibilityCheckEntry.DeleteAll();
PlausibilityCheck.FieldIsEmpty(Header, Header.FieldNo("Buy-from Vendor No."), 0, false, false);
PlausibilityCheck.CheckPostingDate(Header."Posting Date", Header.FieldNo("Posting Date"));
PlausibilityCheck.GetPlausibilityCheckEntries(TempPlausibilityCheckEntry);
exit(TempPlausibilityCheckEntry.IsEmpty());
end;
7.2 Build „Processed JSON“ and pass it to the core: CreateProcessedJsonFromSource
Core stores “Processed JSON” in DXP Document.JSON Processed and transfers it to your target processing.
procedure CreateProcessedJsonFromSource(Source: Variant)
var
CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
DocMgt: Codeunit "DXP Document Mgt.";
JsonHelper: Codeunit "DXP Json Helper";
DocTransferMgt: Codeunit "DXP Document Transfer Mgt.";
Header: Record "DXP SQZ Document Header";
Line: Record "DXP SQZ Document Line";
HeaderJObj: JsonObject;
LinesJArr: JsonArray;
LineJObj: JsonObject;
begin
Header := Source;
HeaderJObj.Add(CoreTokenMgt.GetVendorNoTok(), Header."Buy-from Vendor No.");
HeaderJObj.Add(CoreTokenMgt.GetDocDateTok(), Header."Document Date");
HeaderJObj.Add(CoreTokenMgt.GetDocReferenceTok(), Header."Document Reference");
// Benutzerdefinierte Felder (Erweiterungsfelder) aus den Source‑Tabellen hinzufügen
HeaderJObj.Add(CoreTokenMgt.GetCustomFieldsTok(), JsonHelper.Rec2JsonFieldArray(Header, true, true, Header."Document Class", Enum::"DXP Field Type"::Header));
Line.SetRange("Document No.", Header."No.");
if Line.FindSet() then
repeat
Clear(LineJObj);
LineJObj.Add(CoreTokenMgt.GetDescriptionTok(), Line.Description);
LineJObj.Add(CoreTokenMgt.GetQtyTok(), Line.Quantity);
LineJObj.Add(CoreTokenMgt.GetCustomFieldsTok(), JsonHelper.Rec2JsonFieldArray(Line, true, true, Header."Document Class", Enum::"DXP Field Type"::Line));
LinesJArr.Add(LineJObj);
until Line.Next() = 0;
DocTransferMgt.AddDocLinesToHeaderJson(HeaderJObj, LinesJArr);
// An Core‑Zielverarbeitung übergeben
DocMgt.UpdateDocument(
Header."Core Document No.",
Enum::"DXP Document Status"::Transferred,
Enum::"DXP Target Document Process"::"MY Create Custom Document",
Enum::"DXP Next process step"::"Standard document",
HeaderJObj);
end;
8) Step 6 — Create your own target document
If you want Core to create your own target data set, extend DXP Target Document Process and implement DXP IDocument Processing:
enumextension 50102 "MY Target Doc Process Ext." extends "DXP Target Document Process"
{
value(50102; "MY Create Custom Document")
{
Caption = 'Create My Custom Document';
Implementation = "DXP IDocument Processing" = "MY Custom Doc Creation";
}
}
codeunit 50103 "MY Custom Doc Creation" implements "DXP IDocument Processing"
{
procedure ProcessStandardDocument(JObject: JsonObject): RecordId
var
CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
DocTransferMgt: Codeunit "DXP Document Transfer Mgt.";
JsonHelper: Codeunit "DXP Json Helper";
MyDoc: Record "MY Custom Document";
TargetVariant: Variant;
begin
MyDoc.Init();
MyDoc.Validate("Vendor No.", JsonHelper.ValAsTxt(JObject, CoreTokenMgt.GetVendorNoTok(), false));
MyDoc.Validate("Document Date", JsonHelper.ValAsDate(JObject, CoreTokenMgt.GetDocDateTok(), false));
MyDoc.Insert(true);
// Überträgt Metadaten sowie benutzerdefinierte/zusätzliche Felder anhand der Core‑Zuordnungen in den Ziel‑Datensatz.
TargetVariant := MyDoc;
DocTransferMgt.ProcessAdditionalInformation(JObject, TargetVariant);
MyDoc := TargetVariant;
exit(MyDoc.RecordId());
end;
}
Note: The transfer of metadata and user-defined/additional fields is already included in the above example and is based on the core mapping tables.
9) Troubleshooting (quick checks)
- No entries in
DXP SQZ Import Queue→ Document class setup for SQUEEZE is incomplete. - Core‑
DXP Documentexists, but no source record → YourCreateSource(Document)has failed or returnsfalse. - Source exists, but target is not created →
CreateProcessedJsonFromSourceis not called orDXP Target Document Processis not configured. - Incorrect trigger behavior when assigning → Check
DXP Custom Field Mapping.Validate App FieldandValidate.Validatecontrols 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.Statusto the transferred status. - Core linked/updated
DXP Document.Linked-to Record Idwith the record, which yourDXP IDocument Processing‑implementation returns. - Core transfers Core attachments to the returned data set (and typically appends the “Processed JSON” as a
.jsonattachment).
Custom Document Creation (Custom Processing)
Table of Contents
Overview
This guide explains how to create custom Business Central documents from JSON data provided by DEXPRO Core. The system receives documents with status "Custom Processing", which third-party developers can transform into any BC document type (Purchase Orders, Sales Orders, G/L Journals, Service Orders, etc.).
What You'll Learn
- How to intercept documents marked for custom processing
- The JSON structure containing document header, lines, metadata, and custom fields
- How to create your custom document
Understanding the Document Flow
Core Workflow
┌─────────────────────────────────────────────────────────────────┐
│ 1. Document Import │
│ JSON Raw Data → DXP Document (Status: Imported) │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. Source Document Creation │
│ ISourceDocument.CreateSource() → Your Source Tables │
│ (e.g., SQZ Document Header/Lines or Your Custom Tables) │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. Validation & Plausibility Checks │
│ ISourceDocument.IsSourceDataPlausible() │
│ → Validates data quality, checks for errors │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. Create Processed JSON │
│ ISourceDocument.CreateProcessedJsonFromSource() │
│ → Builds JSON structure for target document │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. Update Core Document with "Custom Processing" Status │
│ UpdateDocument() → Sets Status to "Custom Processing" │
│ → Stores JSON Processed in DXP Document │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 6. Your Custom Processing (THIS IS WHERE YOU HOOK IN!) │
│ Subscribe to events or implement interface │
│ → Read JSON Processed from DXP Document │
│ → Create your target BC document │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 7. Final Status Update │
│ Document Status → Transferred or Finished │
│ Linked-to Record Id → Your created BC document │
└─────────────────────────────────────────────────────────────────┘
Key Status Values
| Status | Description |
|---|---|
| Imported | Raw JSON received, not yet processed |
| Transferred | Source document created, ready for processing |
| Custom Processing | Your target status - Document ready for custom handling |
| Finished | Target document successfully created |
| Deleted | Processing complete, document archived |
JSON Data Structure
Overview
The "JSON Processed" blob in the DXP Document table contains a structured JSON object with all information needed to create your target document.
Complete JSON Structure
{
// ═══════════════════════════════════════════════════════════
// HEADER SECTION - Document-level information
// ═══════════════════════════════════════════════════════════
"Type": "Invoice", // Document type: Invoice, Credit Memo, Order, etc.
"VendorNo": "VENDOR001", // Vendor/Customer number
"DocumentDate": "2025-10-15", // Document date
"PostingDate": "2025-10-16", // Posting date
"DocumentReference": "INV-2025-001", // External document number
"OrderNo": "PO-12345", // Reference to order (if applicable)
"PostingDescription": "Invoice Q4", // Posting description
"NetAmount": 1000.00, // Net amount (excluding tax)
"TotalAmount": 1190.00, // Gross amount (including tax)
"TaxAmount": 190.00, // Total tax amount
"Currency": "EUR", // Currency code
"ResponsibilityCenter": "BERLIN", // Responsibility center (blank: default from user setup / vendor)
"VATBusPostingGroup": "EU", // VAT business posting group (blank: vendor default) - the lines' default
// ───────────────────────────────────────────────────────────
// DIMENSIONS - Header-level dimensions as key-value pairs
// ───────────────────────────────────────────────────────────
"Dimensions": {
"DEPARTMENT": "SALES",
"PROJECT": "PROJ001",
"COSTCENTER": "CC-100"
},
// ───────────────────────────────────────────────────────────
// CUSTOM FIELDS - Additional header fields (see below)
// ───────────────────────────────────────────────────────────
"CustomFields": [
{
"Id": 50100,
"Name": "CustomerNo",
"Value": "CUST001",
"Caption": "Customer Number"
},
{
"Id": 50101,
"Name": "DeliveryTerms",
"Value": "EXW",
"Caption": "Delivery Terms"
}
],
// ───────────────────────────────────────────────────────────
// METADATA - System-generated field mappings
// ───────────────────────────────────────────────────────────
"Metadata": [
{
"DocumentClass": "DXP Invoice / Credit Memo",
"FieldType": "Header",
"FieldId": 101,
"Value": "Additional Info"
}
],
// ═══════════════════════════════════════════════════════════
// LINES SECTION - Document line items
// ═══════════════════════════════════════════════════════════
"Lines": [
{
// ─────────────────────────────────────────────────────────
// Line Basic Information
// ─────────────────────────────────────────────────────────
"Type": "Item", // Item, G/L Account, Charge (Item), Fixed Asset
"No": "ITEM001", // Item/Account number
"Description": "Product A", // Line description
"VendorItemNo": "VEND-SKU-123", // Vendor's item number
"Quantity": 10.0, // Quantity
"UnitOfMeasure": "PCS", // Unit of measure
"DirectUnitCost": 100.00, // Unit price
"LineDiscount": 5.0, // Line discount percentage
// ─────────────────────────────────────────────────────────
// Line Amounts & Tax
// ─────────────────────────────────────────────────────────
"NetAmount": 950.00, // Line net amount
"TotalAmount": 1130.50, // Line gross amount
"TaxRate": 19.0, // Tax percentage
"VATBusPostingGroup": "DOMESTIC",
"VATProdPostingGroup": "STANDARD",
"GenBusPostingGroup": "DOMESTIC",
"GenProdPostingGroup": "RETAIL",
// ─────────────────────────────────────────────────────────
// Order Reference (for matching)
// ─────────────────────────────────────────────────────────
"OrderNo": "PO-12345", // Referenced order number
"OrderLineNo": 10000, // Referenced order line number
"ReceiptNo": "RCP-001", // Referenced receipt number
"ReceiptLineNo": 10000, // Referenced receipt line number
// ─────────────────────────────────────────────────────────
// Line Dimensions
// ─────────────────────────────────────────────────────────
"Dimensions": {
"DEPARTMENT": "PROD",
"PROJECT": "PROJ001"
},
// ─────────────────────────────────────────────────────────
// Line Custom Fields
// ─────────────────────────────────────────────────────────
"CustomFields": [
{
"Id": 50200,
"Name": "SerialNo",
"Value": "SN-12345",
"Caption": "Serial Number"
}
],
// ─────────────────────────────────────────────────────────
// Line Metadata
// ─────────────────────────────────────────────────────────
"Metadata": [
{
"DocumentClass": "DXP Invoice / Credit Memo",
"FieldType": "Line",
"FieldId": 201,
"Value": "Line-specific metadata"
}
]
}
// ... additional lines
]
}
Custom Fields vs. Metadata
Custom Fields:
-
User-defined fields from your app
-
Mapped via Custom Field Mapping
-
Can be mapped to any field in the Squeeze Validation
-
Example: Adding a "Customer No." to the Squeeze Validation
Metadata:
-
System-generated fields
-
Extracted by the Squeeze System but not mapped to any Field in BC
Quick Start: Simple Custom Processing
Scenario
You want to create a custom document type (e.g., a Warehouse Shipment) from documents marked with "Custom Processing" status.
Step 1: Subscribe to the Integration Event
Create a codeunit to intercept documents with "Custom Processing" status:
codeunit 50100 "My Custom Document Handler"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Mgt.",
'OnAfterWriteProcessedJsonToBlob', '', true, true)]
local procedure OnAfterWriteProcessedJsonToBlob(
var Document: Record "DXP Document";
var ProcessedJSONObj: JsonObject)
begin
// Only handle Custom Processing status
if Document.Status <> Document.Status::"Custom Processing" then
exit;
// Create your custom document
CreateMyCustomDocument(Document, ProcessedJSONObj);
end;
local procedure CreateMyCustomDocument(
var Document: Record "DXP Document";
ProcessedJSON: JsonObject)
var
CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
JsonHelper: Codeunit "DXP Json Helper";
MyCustomHeader: Record "My Custom Document Header";
MyCustomLine: Record "My Custom Document Line";
LineJArray: JsonArray;
LineJToken: JsonToken;
LineJObj: JsonObject;
begin
// ═══════════════════════════════════════════════════════
// 1. CREATE HEADER
// ═══════════════════════════════════════════════════════
MyCustomHeader.Init();
MyCustomHeader."No." := ''; // Will be assigned by number series
// Read standard header fields
MyCustomHeader."Vendor No." :=
JsonHelper.ValAsTxt(ProcessedJSON, CoreTokenMgt.GetVendorNoTok(), false);
MyCustomHeader."Document Date" :=
JsonHelper.ValAsDate(ProcessedJSON, CoreTokenMgt.GetDocDateTok(), false);
MyCustomHeader."Document Reference" :=
JsonHelper.ValAsTxt(ProcessedJSON, CoreTokenMgt.GetDocReferenceTok(), false);
MyCustomHeader.Insert(true);
// ═══════════════════════════════════════════════════════
// 2. PROCESS DIMENSIONS (if your document supports them)
// ═══════════════════════════════════════════════════════
ProcessHeaderDimensions(ProcessedJSON, MyCustomHeader);
// ═══════════════════════════════════════════════════════
// 3. PROCESS CUSTOM FIELDS
// ═══════════════════════════════════════════════════════
ProcessCustomFields(ProcessedJSON, MyCustomHeader);
// ═══════════════════════════════════════════════════════
// 4. CREATE LINES
// ═══════════════════════════════════════════════════════
LineJArray := JsonHelper.ReadJArrayFromObj(ProcessedJSON, CoreTokenMgt.GetLinesTok());
foreach LineJToken in LineJArray do begin
LineJObj := LineJToken.AsObject();
MyCustomLine.Init();
MyCustomLine."Document No." := MyCustomHeader."No.";
MyCustomLine."Line No." := GetNextLineNo(MyCustomHeader."No.");
// Read line fields
MyCustomLine."Item No." :=
JsonHelper.ValAsTxt(LineJObj, CoreTokenMgt.GetNoTok(), false);
MyCustomLine.Description :=
JsonHelper.ValAsTxt(LineJObj, CoreTokenMgt.GetDescriptionTok(), false);
MyCustomLine.Quantity :=
JsonHelper.ValAsDec(LineJObj, CoreTokenMgt.GetQtyTok(), false);
MyCustomLine."Unit of Measure" :=
JsonHelper.ValAsTxt(LineJObj, CoreTokenMgt.GetUoMTok(), false);
MyCustomLine.Insert(true);
// Process line dimensions and custom fields if needed
ProcessLineDimensions(LineJObj, MyCustomLine);
ProcessLineCustomFields(LineJObj, MyCustomLine);
end;
// ═══════════════════════════════════════════════════════
// 5. UPDATE CORE DOCUMENT STATUS
// ═══════════════════════════════════════════════════════
UpdateCoreDocument(Document, MyCustomHeader);
end;
local procedure ProcessHeaderDimensions(ProcessedJSON: JsonObject; var MyCustomHeader: Record "My Custom Document Header")
var
CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
DocTransferMgt: Codeunit "DXP Document Transfer Mgt.";
DimensionsJObj: JsonObject;
DimSetID: Integer;
begin
// Read dimensions from JSON
if ProcessedJSON.Contains(CoreTokenMgt.GetDimensionsTok()) then begin
ProcessedJSON.Get(CoreTokenMgt.GetDimensionsTok(), DimensionsJObj);
// Convert to Dimension Set ID
if DocTransferMgt.GetDimSetIdFromJsonObj(DimensionsJObj, DimSetID) then begin
MyCustomHeader."Dimension Set ID" := DimSetID;
MyCustomHeader.Modify(true);
end;
end;
end;
local procedure ProcessCustomFields(ProcessedJSON: JsonObject; var MyCustomHeader: Record "My Custom Document Header")
var
CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
JsonHelper: Codeunit "DXP Json Helper";
CustomFieldsJArray: JsonArray;
CustomFieldJToken: JsonToken;
CustomFieldJObj: JsonObject;
FieldName: Text;
FieldValue: Text;
begin
// Read custom fields array
if not ProcessedJSON.Contains(CoreTokenMgt.GetCustomFieldsTok()) then
exit;
CustomFieldsJArray := JsonHelper.ReadJArrayFromObj(ProcessedJSON, CoreTokenMgt.GetCustomFieldsTok());
foreach CustomFieldJToken in CustomFieldsJArray do begin
CustomFieldJObj := CustomFieldJToken.AsObject();
FieldName := JsonHelper.ValAsTxt(CustomFieldJObj, CoreTokenMgt.GetNameTok(), false);
FieldValue := JsonHelper.ValAsTxt(CustomFieldJObj, CoreTokenMgt.GetValueTok(), false);
// Map to your custom fields
case FieldName of
'CustomerNo':
MyCustomHeader."Customer No." := CopyStr(FieldValue, 1, 20);
'DeliveryTerms':
MyCustomHeader."Delivery Terms" := CopyStr(FieldValue, 1, 10);
// Add more field mappings as needed
end;
end;
MyCustomHeader.Modify(true);
end;
local procedure ProcessLineDimensions(LineJObj: JsonObject; var MyCustomLine: Record "My Custom Document Line")
var
CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
DocTransferMgt: Codeunit "DXP Document Transfer Mgt.";
DimensionsJObj: JsonObject;
DimSetID: Integer;
begin
if LineJObj.Contains(CoreTokenMgt.GetDimensionsTok()) then begin
LineJObj.Get(CoreTokenMgt.GetDimensionsTok(), DimensionsJObj);
if DocTransferMgt.GetDimSetIdFromJsonObj(DimensionsJObj, DimSetID) then begin
MyCustomLine."Dimension Set ID" := DimSetID;
MyCustomLine.Modify(true);
end;
end;
end;
local procedure ProcessLineCustomFields(LineJObj: JsonObject; var MyCustomLine: Record "My Custom Document Line")
var
CoreTokenMgt: Codeunit "DXP Core Token Mgt.";
JsonHelper: Codeunit "DXP Json Helper";
CustomFieldsJArray: JsonArray;
CustomFieldJToken: JsonToken;
CustomFieldJObj: JsonObject;
FieldName: Text;
FieldValue: Text;
begin
if not LineJObj.Contains(CoreTokenMgt.GetCustomFieldsTok()) then
exit;
CustomFieldsJArray := JsonHelper.ReadJArrayFromObj(LineJObj, CoreTokenMgt.GetCustomFieldsTok());
foreach CustomFieldJToken in CustomFieldsJArray do begin
CustomFieldJObj := CustomFieldJToken.AsObject();
FieldName := JsonHelper.ValAsTxt(CustomFieldJObj, CoreTokenMgt.GetNameTok(), false);
FieldValue := JsonHelper.ValAsTxt(CustomFieldJObj, CoreTokenMgt.GetValueTok(), false);
// Map to your line custom fields
case FieldName of
'SerialNo':
MyCustomLine."Serial No." := CopyStr(FieldValue, 1, 50);
// Add more field mappings
end;
end;
MyCustomLine.Modify(true);
end;
local procedure UpdateCoreDocument(var Document: Record "DXP Document"; MyCustomHeader: Record "My Custom Document Header")
var
DocumentMgt: Codeunit "DXP Document Mgt.";
begin
// Update the core document to link it to your created document
Document.Status := Document.Status::Transferred;
Document."Linked-to Record Id" := MyCustomHeader.RecordId;
Document.Modify(true);
// Optionally, transfer attachments from Core to your document
TransferAttachments(Document, MyCustomHeader);
end;
local procedure TransferAttachments(Document: Record "DXP Document"; MyCustomHeader: Record "My Custom Document Header")
var
DocAttachment: Record "DXP Document Attachment";
MyDocAttachment: Record "Document Attachment";
InStr: InStream;
begin
DocAttachment.SetRange("Document No.", Document."No.");
if DocAttachment.FindSet() then
repeat
MyDocAttachment.Init();
MyDocAttachment.ID := 0;
DocAttachment."File Content".CreateInStream(InStr);
MyDocAttachment.SaveAttachmentFromStream(
InStr,
MyCustomHeader.RecordId,
DocAttachment."File Name");
until DocAttachment.Next() = 0;
end;
local procedure GetNextLineNo(DocumentNo: Code[20]): Integer
var
MyCustomLine: Record "My Custom Document Line";
begin
MyCustomLine.SetRange("Document No.", DocumentNo);
if MyCustomLine.FindLast() then
exit(MyCustomLine."Line No." + 10000);
exit(10000);
end;
}
Key Helper Codeunits
| Codeunit | Purpose |
|---|---|
| DXP Core Token Mgt. | Provides token names for JSON fields (GetVendorNoTok(), GetDocDateTok(), etc.) |
| DXP Json Helper | JSON parsing utilities (ValAsTxt(), ValAsDate(), ReadJArrayFromObj(), etc.) |
| DXP Document Transfer Mgt. | Dimension processing, metadata transfer, custom field handling |
| DXP Document Mgt. | Core document management (UpdateDocument(), UpdateDocumentStatus(), etc.) |
For questions or clarifications, please contact DEXPRO Solutions GmbH.
Rule-based next process step: routing documents to Custom Processing
Audience
This guide is intended for developers and implementers who use DEXPRO SQUEEZE in Business Central and want to control, before the actual "Custom Processing" implementation, which documents should switch to the "Custom Processing" status after validation.
Context (comes before the existing Custom Processing guide)
With this configuration you ensure that a document (depending on rules) is given the "Custom Processing" status. From that point on, the flow for custom processing described in the existing documentation takes over (creating target documents from "JSON Processed").
Further reading (next step, after this documentation):
UI terms used
The following terms correspond to the field captions used in the app:
- "Custom Processing" =
DXP Document Status/Custom Processing - "Next Process Step" =
Next Process Step - "Standard document" = enum value
Standard document - "Rule Set", "Rule Set Code" =
Rule Set,Rule Set Code - "Rule Group", "Rule Group Code" =
Rule Group,Rule Group Code
Technical background
- There is one document class setup (
DXP Document Class Setup) per document class. - A SQUEEZE rule set is created automatically for each document class setup (internally: field "DXP Sqz Rule Set Code").
- A rule group code is stored in the document class setup (internally: field "DXP Sqz Next Step Rule Group").
- When the next process step is determined, the rules of this rule group are evaluated.
- If a rule matches and
- Next Process Step = "Standard document" and
- Custom Processing = Yes, then the document status is set to "Custom Processing".
Important:
- If no rule group is stored or no rule matches, the system falls back to the default "Next Process Step" value from the document class setup.
Prerequisites
- DEXPRO Core and DEXPRO SQUEEZE are installed.
- You have access to the Document Class Setup (DXP Document Class Setup) page as well as the rule engine pages (rule groups / rules).
Step by step: creating and linking the "DXP Sqz Next Step Rule Group" rule group
1) Open the document class setup
- Open the Document Class Setup page.
- Select the document class for which you want to enable routing to "Custom Processing".
- Scroll to the Squeeze area → Rules group.
{add screenshot of Document Class Setup - Squeeze Rules here}
2) Create the rule group
- In Squeeze → Rules, select the "Next Process Step" field (technically: DXP Sqz Next Step Rule Group).
- Open the lookup (rule group list).
- Create a new rule group:
- Rule Group Code: for example
Next Step Rules - Description: for example "Routing to Custom Processing"
- Rule Group Code: for example
Note: The lookup is already filtered to the matching rule set code by the document class. You do not need to search for the rule set manually.
{add screenshot of rule group lookup here}
3) Save the rule group in the document class setup
- Make sure that the "Next Process Step" field in the document class setup now contains the rule group code
DXP Sqz Next Step Rule Group.
{add screenshot of document class setup with rule group code here}
Step by step: creating a rule that routes to "Custom Processing"
4) Open the rules of the rule group
- Open the rule group
DXP Sqz Next Step Rule Groupyou just created. - Choose the "Show Rules" action (opens the rules of this rule group).
{add screenshot of Show Rules action here}
5) Create a new rule
- Create a new rule.
- Set a meaningful priority (lower value = higher priority, if your environment is configured that way).
- Define the conditions (rule conditions) that must match.
[Best Practice] Start with a very clear, testable condition (for example based on document type, vendor, amount thresholds, company, etc.) before you build complex rule chains.
{add screenshot of new rule here}
6) Set the SQUEEZE-specific fields (decisive for "Custom Processing")
On the rule card there is a "DEXPRO Squeeze" area (CardPart). Set the following there:
- Next Process Step = "Standard document"
- Custom Processing = Yes
Result:
- When this rule matches, the document status is set to "Custom Processing".
{add screenshot of rule card DEXPRO Squeeze area here}
Optional:
- Account Assignment Code (if you additionally want to control account assignment logic via rules; independent of the Custom Processing routing).
Checks / troubleshooting
The document does not end up in "Custom Processing"
Work through these points:
- Is the rule group stored? The rule group code must be set in the document class setup (otherwise the system falls back to the default "Next Process Step").
- Does a rule match? If no rule matches, the next process step is not overwritten.
- Is "Custom Processing" set AND Next Step = "Standard document"?
- If "Custom Processing" = Yes but Next Step ≠ "Standard document", the status is not set to "Custom Processing".
- Can you not see the "DEXPRO Squeeze" area on the rule card?
- Then you are most likely in a rule set that does not belong to the document class setup (source table).
- It is best to create the rule group via the lookup from the document class setup. This saves you the detour via the general rule management.
The document ends up in "Done" instead of "Custom Processing"
If a rule matches and Next Process Step is set to a value other than "Standard document", the status may be set to "Done" (which skips further processing). Therefore check the Next Step selection in the rule. This is the case, for example, when the Breeze Interface app is used.
Afterwards (transition into the existing documentation on custom document creation)
As soon as a document is in the "Custom Processing" status, it is ready for processing:
- Read "JSON Processed" from
DXP Document - Create target documents
- Set the final status
Embedding the SQUEEZE Viewer part in your own pages WIP
Diese Anleitung beschreibt, wie Sie den SQUEEZE Viewer in beliebige Seiten einbetten (eigene Pages oder Page Extensions) – über den wiederverwendbaren Viewer‑Part:
- Page: "DXP SQZ Viewer Part" (PageType =
CardPart) - Source table: "DXP SQZ Document Header"
Der Viewer‑Part hostet die SQUEEZE Control Add-ins und lädt die Preview‑URL anhand des aktuellen Datensatzes DXP SQZ Document Header.
Referenz‑Implementierung, die in dieser App bereits enthalten ist:
- Page "DXP SQZ Document (Generic)" (Card, SourceTable DXP SQZ Document Header) bettet den Viewer‑Part als FactBox ein und verdrahtet das Resident‑Sharing zur generischen Lines‑Subform.
- Page "DXP SQZ Doc. Generic Subform" (ListPart, SourceTable DXP SQZ Document Line) enthält eine bekannte, funktionierende Minimal‑Implementierung von
MarkRow().
Die Pages können Sie benutzen und mit ihren Feldern erweitern. Die Page "DXP SQZ Document (Generic)" verfügt über einen Viewer in der Factbox und die wichtigsten Actions.
1) Voraussetzungen
-
Ihre Extension muss von der DEXPRO SQUEEZE App abhängig sein (damit Page/Control Add-ins als Symbole verfügbar sind).
-
Der Host‑Datensatz muss eine Möglichkeit bieten, einen DXP SQZ Document Header Datensatz zu referenzieren.
- Die
SourceTabledes Viewer‑Parts ist fest auf DXP SQZ Document Header. - Deshalb muss Ihre Seite ein Feld haben (oder bereitstellen), das sich in
SubPageLinkverwenden lässt, um den Header zu finden. Alternativ kann der Record in dem Viewer-Part gesetzt werden - Beispiele folgen weiter unten.
- Die
-
SQUEEZE muss aktiviert und konfiguriert sein (Setup + API Key). Der Viewer‑Part lädt absichtlich nicht, wenn das Modul nicht aktiviert ist.
2) Empfohlenes Einbettungsmuster
2.1 Als FactBox einbetten (empfohlen)
Für die meisten Szenarien (Card Pages, List Pages) platzieren Sie den Viewer in area(FactBoxes).
Wenn die SourceTable Ihrer Seite DXP SQZ Document Header ist
layout
{
area(FactBoxes)
{
part(SQZViewer; "DXP SQZ Viewer Part")
{
ApplicationArea = All;
Caption = 'Viewer';
// Link the FactBox part to the current SQZ header
SubPageLink = "No." = field("No.");
}
}
}
Wenn die SourceTable Ihrer Seite NICHT DXP SQZ Document Header ist
Sie können den Viewer‑Part trotzdem einbetten, sofern Ihr Datensatz ein Feld enthält, das die SQUEEZE Header‑Nummer speichert.
Beispiel: Ihr Datensatz hat ein Feld "SQZ Document No." : Code[20]
layout
{
area(FactBoxes)
{
part(SQZViewer; "DXP SQZ Viewer Part")
{
ApplicationArea = All;
Caption = 'Viewer';
// Link the viewer part’s "No." to your field
SubPageLink = "No." = field("SQZ Document No.");
}
}
}
Wichtig:
- Setze am Part nicht
Enabled = false. Wenn eine FactBox‑Part deaktiviert ist, kann das verhindern, dass das Control Add-in „ready“ wird – dadurch bleibt der Viewer ggf. dauerhaft leer.
2.2 Viewer dynamisch binden (ohne SubPageLink)
Wenn Ihre Host‑Seite keinen einfachen SubPageLink anbieten kann (z. B. weil es sich um eine Zielbeleg‑Seite wie Purchase Invoice handelt und dort die SQUEEZE Belegnummer nicht gespeichert ist), können Sie den Viewer‑Part trotzdem einbetten und im Code binden.
Konzept:
- Viewer‑Part als FactBox hinzufügen – ohne
SubPageLink. - In
OnAfterGetCurrRecord()den relevantenRecord "DXP SQZ Document Header"auflösen. - Den Datensatz per
CurrPage.<PartName>.Page.SetSqzRecord(SqzHeader);in den Part pushen.
Warum SetSqzRecord?
- Im DEXPRO SQUEEZE Viewer‑Part läuft das Laden der URL in
OnAfterGetCurrRecord(). - Beim Navigieren innerhalb einer Seite (z. B. Wechsel zwischen Purchase Invoices) kann
SetRecord(...)allein dazu führen, dass die FactBox nicht sofort neu lädt. SetSqzRecord(...)ist dafür gedacht, den Header sauber neu zu binden und einen Refresh auszulösen, sodass die korrekte Preview ohne Schließen/Öffnen der Seite geladen wird.
Beispiel‑Gerüst:
layout
{
area(FactBoxes)
{
part(SQZViewer; "DXP SQZ Viewer Part")
{
ApplicationArea = All;
Caption = 'Viewer';
}
}
}
trigger OnAfterGetCurrRecord()
var
SqzHeader: Record "DXP SQZ Document Header";
begin
if TryResolveSqzHeaderForCurrentRecord(SqzHeader) then
CurrPage.SQZViewer.Page.SetSqzRecord(SqzHeader);
end;
Dieses Pattern ist besonders für Third‑party Pages hilfreich, weil es vermeidet, „SQUEEZE Document No.“ als Tabellenfeld persistieren zu müssen.
3) Viewer‑Modus (Belegübersicht / Positionsfokus / Abgedockt)
Wenn der Viewer als FactBox eingebettet ist, sind Felder typischerweise nicht editierbar. Deshalb bietet der Viewer‑Part Actions, um den Viewer‑Modus zu ändern.
Was beim Wechsel des Modus passiert:
- Belegübersicht: Zeigt eine Übersichtliche Darstellung des Viewers an.
- Positionsfokus: Zeigt eine schmalere Darstellung des Viewers an.
- Abgedockt: Öffnet den Viewer in einem externen Fenster
Für Third‑party Entwickler:
- Sie müssen keine eigene Mode‑Logik implementieren.
- Nutzen Sie die Actions des Viewer‑Parts (erscheinen im FactBox Action‑Menü).
4) Optional, aber wichtig: Zeilen‑Highlighting aktivieren (MarkRow)
Wenn Sie eine Lines‑Subform haben und die Benutzererfahrung „Zeile klicken → Viewer markiert Koordinaten“ wünschen, muss Ihre Lines‑Page Messages über dieselbe Viewer‑Resident‑Instanz posten, die der Viewer‑Part initialisiert.
Warum das wichtig ist:
- Die Viewer‑UI ist eine Control Add‑in Instanz, die in einer Codeunit
DXP SQUEEZE Viewer Residentgespeichert wird. - Wenn Ihre Lines‑Page eine eigene Variable
DXP SQUEEZE Viewer Residentverwendet (die nicht vom Viewer‑Part initialisiert wurde), hatApiMgt.PostMessage(...)keine Control‑Add‑in Instanz zum Ansprechen – es wird nichts markiert.
4.1 Pattern (Host‑Page verdrahtet Viewer‑Resident in Lines)
Annahmen:
- Ihre Header‑Page hat:
part(SQZViewer; "DXP SQZ Viewer Part")part(DocumentLines; <Ihre Lines ListPart>)
- Ihre Lines‑ListPart stellt eine Procedure bereit:
SetViewerResident(var NewResident: Codeunit "DXP SQUEEZE Viewer Resident")
Das ist ein expliziter Vertrag/Annahme für interaktives Line‑Highlighting.
Ergänzen Sie das auf Ihrer Header‑Page:
trigger OnAfterGetCurrRecord()
var
SharedResident: Codeunit "DXP SQUEEZE Viewer Resident";
begin
CurrPage.SQZViewer.Page.GetViewerResident(SharedResident);
CurrPage.DocumentLines.Page.SetViewerResident(SharedResident);
end;
Hinweise:
- Das gehört in
OnAfterGetCurrRecord()der Host‑Page (dort ist der aktuelle Header stabil, und die Parts existieren). - Wenn Ihre Host‑Page andere Part‑Namen verwendet, passen Sie
CurrPage.<PartName>.Page...entsprechend an.
4.2 Was die Lines‑Subform tun muss
Ihre Lines‑Page sollte MarkRow() in OnAfterGetCurrRecord() aufrufen und MarkRow() ähnlich zur Standard‑Seite implementieren:
- Wenn der externe Viewer aktiv ist (Abgedockt), nur die State‑Tabelle aktualisieren.
- Ansonsten
ApiMgt.PostMessage(ApiMgt.MarkFieldByCoordinatesObj(Rec.RecordId), Resident)aufrufen.
Das ist die Mindestanforderung für interaktives Highlighting.
4.3 Referenz‑Implementierung: minimale Lines‑Subform mit MarkRow
Das ist ein bekannt funktionierendes Minimal‑Pattern (basierend auf der Standard SQUEEZE Lines‑Subform). Third‑party Entwickler können es kopieren und Felder/Repeater‑Spalten nach Bedarf anpassen:
page 70954672 "DXP SQZ Doc. Generic Subform"
{
Caption = 'Lines';
PageType = ListPart;
AutoSplitKey = true;
DelayedInsert = true;
SourceTable = "DXP SQZ Document Line";
RefreshOnActivate = true;
layout
{
area(content)
{
repeater(General)
{
field("Line No."; Rec."Line No.")
{
ApplicationArea = All;
Visible = false;
}
// Add your visible SQZ fields here...
}
}
}
trigger OnAfterGetCurrRecord()
begin
MarkRow();
end;
var
ApiMgt: Codeunit "DXP SQZ API Mgt.";
Resident: Codeunit "DXP SQUEEZE Viewer Resident";
MarkedLineNo: Integer;
procedure SetViewerResident(var NewResident: Codeunit "DXP SQUEEZE Viewer Resident")
begin
Resident := NewResident;
MarkedLineNo := 0;
end;
local procedure MarkRow()
var
DocHeader: Record "DXP SQZ Document Header";
LineCoordMgt: Codeunit "DXP SQZ Coordinates Mgt.";
ViewerState: Codeunit "DXP Viewer State";
begin
if MarkedLineNo = Rec."Line No." then
exit;
if not LineCoordMgt.CoordinateExists(Rec.RecordId) then
exit;
// Detached viewer: update the state table only
if IsExternalViewerActive() then begin
if DocHeader.Get(Rec."Document No.") then
ViewerState.UpdateViewerStateWithCoordinates(DocHeader."API Document ID", Rec.RecordId);
end else
// Internal viewer: post message to the control add-in instance via the shared resident
ApiMgt.PostMessage(ApiMgt.MarkFieldByCoordinatesObj(Rec.RecordId), Resident);
MarkedLineNo := Rec."Line No.";
end;
local procedure IsExternalViewerActive(): Boolean
var
ViewerState: Codeunit "DXP Viewer State";
begin
exit(ViewerState.GetViewerMode() = "DXP SQZ Viewer Mode"::Detached);
end;
}
5) Fehlersuche (Viewer bleibt leer)
5.1 Der Viewer lädt nie eine Preview
Prüfe zuerst:
- Ist das SQUEEZE Modul aktiviert und das Setup vollständig?
- Hat der verknüpfte DXP SQZ Document Header Datensatz eine gültige
"API Document ID"? - Ist der Viewer‑Modus aktuell Abgedockt und ein externer Viewer offen?
- Der interne Viewer lädt nicht, solange der externe Viewer geöffnet ist.
5.2 FactBox ist sichtbar, aber Control Add-in initialisiert nicht
- Deaktiviere den Viewer‑FactBox‑Part nicht über
Enabled = false. - Stelle sicher, dass die FactBox tatsächlich angezeigt wird (FactBoxes können vom Benutzer eingeklappt/ausgeblendet werden).
5.3 MarkRow hebt nichts hervor
- Stelle sicher, dass die Lines‑Page dieselbe Viewer‑Resident‑Instanz wie der Viewer‑Part nutzt (siehe Abschnitt 4).
- Stelle sicher, dass Koordinaten für den Zeilen‑Datensatz existieren (
DXP SQZ Coordinates Mgt.muss Koordinaten zuRec.RecordIdfinden).
6) Minimales Beispiel: Viewer in eine eigene Page Extension einbauen
pageextension 50100 "MY SQZ Doc Ext." extends "DXP SQZ Document (Generic)"
{
layout
{
addfirst(FactBoxes)
{
part(MySQZViewer; "DXP SQZ Viewer Part")
{
ApplicationArea = All;
Caption = 'Viewer';
SubPageLink = "No." = field("No.");
}
}
}
}
7) Hinweise / Einschränkungen
- Der Viewer‑State ist session‑getrieben (via
DXP Viewer State). Wenn mehrere Seiten mit Viewer gleichzeitig offen sind, können Modus/External‑Viewer‑State alle beeinflussen. - Der Viewer‑Part ist für Datensätze mit SQUEEZE‑Bezug gedacht; er versucht nicht, automatisch „irgendeinen“ Header zu suchen.
8) Beispiel: Viewer auf einem Zielbeleg anzeigen (Purchase Invoice)
Dieser Abschnitt adressiert ein häufiges Third‑party Szenario:
Annahmen:
- Der Zielbeleg (z. B. Purchase Invoice) wurde aus einem Core‑Beleg erstellt.
- Der Core‑Beleg ist weiterhin in der Tabelle "DXP Document" verfügbar.
- Der Core‑Beleg speichert eine Rückverknüpfung zum Ziel‑Datensatz im Feld "Linked-to Record Id".
- Der SQUEEZE Quell‑Header existiert noch und kann aus dem Core‑Beleg aufgelöst werden (typischerweise über Core Document No.).
8.1 Core‑Beleg aus dem Ziel‑Datensatz auflösen
local procedure TryGetCoreDocumentForTarget(TargetRecId: RecordId; var CoreDoc: Record "DXP Document"): Boolean
begin
CoreDoc.Reset();
CoreDoc.SetRange("Linked-to Record Id", TargetRecId);
exit(CoreDoc.FindFirst());
end;
8.2 SQUEEZE Header aus dem Core‑Beleg auflösen
local procedure TryGetSqzHeaderForCore(CoreDocNo: Code[20]; var SqzHeader: Record "DXP SQZ Document Header"): Boolean
begin
SqzHeader.Reset();
SqzHeader.SetRange("Core Document No.", CoreDocNo);
exit(SqzHeader.FindFirst());
end;
8.3 Viewer einbetten (dynamisches Binden)
Beispiel: Purchase Invoice erweitern (konzeptionell; Basis‑Page‑Name ggf. an Ihre Umgebung anpassen).
Diese Implementierung verwendet:
- Einen FactBox‑Part
- Ein
ShowViewer‑Flag, um den Part auszublenden, wenn kein SQZ Header aufgelöst werden kann. SetSqzRecord(...), damit die Preview beim Wechsel des Belegs sofort korrekt neu lädt.- Ein
SingleInstanceContext‑Codeunit, um Viewer‑Resident und SQZ‑Kontext mit der Purch. Invoice Subform zu teilen (FactBoxes und Subforms sind separate Pages).
pageextension 50100 "MY Purch. Invoice Viewer" extends "Purchase Invoice"
{
layout
{
addfirst(FactBoxes)
{
part(DXPSQZViewer; "DXP SQZ Viewer Part")
{
ApplicationArea = All;
Caption = 'SQUEEZE Viewer';
Visible = ShowViewer;
}
}
}
var
ShowViewer: Boolean;
trigger OnAfterGetCurrRecord()
var
CoreDoc: Record "DXP Document";
SqzHeader: Record "DXP SQZ Document Header";
SharedResident: Codeunit "DXP SQUEEZE Viewer Resident";
ViewerCtx: Codeunit "DXP SQZ Purch. Inv. Viewer Ctx";
begin
ShowViewer := false;
if not TryGetCoreDocumentForTarget(Rec.RecordId, CoreDoc) then
exit;
if not TryGetSqzHeaderForCore(CoreDoc."No.", SqzHeader) then
exit;
ShowViewer := true;
CurrPage.DXPSQZViewer.Page.SetSqzRecord(SqzHeader);
CurrPage.DXPSQZViewer.Page.GetViewerResident(SharedResident);
ViewerCtx.SetResident(SharedResident);
ViewerCtx.SetSqzContextForPurchInv(Rec."No.", SqzHeader."No.", SqzHeader."API Document ID");
end;
local procedure TryGetCoreDocumentForTarget(TargetRecId: RecordId; var CoreDoc: Record "DXP Document"): Boolean
begin
CoreDoc.Reset();
CoreDoc.SetRange("Linked-to Record Id", TargetRecId);
exit(CoreDoc.FindFirst());
end;
local procedure TryGetSqzHeaderForCore(CoreDocNo: Code[20]; var SqzHeader: Record "DXP SQZ Document Header"): Boolean
begin
SqzHeader.Reset();
SqzHeader.SetRange("Core Document No.", CoreDocNo);
exit(SqzHeader.FindFirst());
end;
}
Zugehöriges Context‑Codeunit (speichert Resident + SQZ‑Kontext, damit die Subform korrekt highlighten kann):
codeunit 50101 "MY SQZ Purch. Inv. Viewer Ctx"
{
SingleInstance = true;
procedure SetResident(var NewResident: Codeunit "DXP SQUEEZE Viewer Resident")
begin
Resident := NewResident;
HasResident := true;
end;
procedure TryGetResident(var OutResident: Codeunit "DXP SQUEEZE Viewer Resident"): Boolean
begin
if not HasResident then
exit(false);
OutResident := Resident;
exit(true);
end;
procedure SetSqzContextForPurchInv(PurchInvNo: Code[20]; SqzDocNo: Code[20]; ApiDocumentId: Text[50])
begin
PurchInvToSqzDocNo.Set(PurchInvNo, SqzDocNo);
PurchInvToApiDocumentId.Set(PurchInvNo, ApiDocumentId);
end;
procedure TryGetSqzContextForPurchInv(PurchInvNo: Code[20]; var SqzDocNo: Code[20]; var ApiDocumentId: Text[50]): Boolean
begin
if not PurchInvToSqzDocNo.Get(PurchInvNo, SqzDocNo) then
exit(false);
PurchInvToApiDocumentId.Get(PurchInvNo, ApiDocumentId);
exit(true);
end;
var
Resident: Codeunit "DXP SQUEEZE Viewer Resident";
HasResident: Boolean;
PurchInvToSqzDocNo: Dictionary of [Code[20], Code[20]];
PurchInvToApiDocumentId: Dictionary of [Code[20], Text[50]];
}
8.4 Wichtiger Hinweis: SQUEEZE Header‑Status nach Verarbeitung
Sie haben ein häufiges Setup beschrieben, in dem der SQUEEZE Header nach dem Erstellen des Zielbelegs noch existiert, aber Status = Deleted ist.
Der Viewer‑Part kann die Preview trotzdem laden, solange "API Document ID" vorhanden ist und SQUEEZE aktiviert ist.
Optionen:
- Wenn Sie die Preview nach Zielbeleg‑Erstellung nicht anzeigen möchten, bauen Sie eine Guard‑Logik auf der Host‑Page ein (Viewer‑Part dann nicht binden, wenn der SQZ Header „Deleted“ ist).
Welche Option korrekt ist, hängt von Ihren Prozessanforderungen ab.
9) Sonderfall: Hervorheben aus der Purchase Invoice Subform (Purchase Line → SQZ line)
Manchmal mächten Sie diese Benutzererfahrung auf einer Zielbeleg‑Seite:
In dieser Situation basiert Ihre Lines‑Page nicht auf "DXP SQZ Document Line", daher können Sie MarkFieldByCoordinatesObj(Rec.RecordId) nicht direkt aufrufen.
Stattdessen müssen Sie die passende SQUEEZE Line auflösen und danach mit der RecordId dieser SQUEEZE Line highlighten.
9.1 Zwei mögliche Verknüpfungsstrategien
Es gibt zwei realistische Wege, eine Einkaufszeile wieder auf eine SQUEEZE Line zu mappen:
-
Expliziter Allocation‑Link (nur in manchen Szenarien verfügbar)
- Verwendet Felder auf
"DXP SQZ Document Line":"Allocated Document Type"(enum"DXP Order Match Document Type")"Allocated Document No.""Allocated Document Line No."
- Verwendet Felder auf
-
Heuristischer Match nach Inhalt (empfohlen als Fallback)
- Verwendet Felder, die typischerweise auf beiden Seiten existieren (Type/No/Quantity/Unit Cost/Line Amount/Description).
- Das funktioniert auch dann, wenn keine Allocation‑Beziehung gepflegt wird.
- Es ist nicht mathematisch perfekt (Duplikate sind möglich), daher sollten Matching‑Regeln auf Ihren Use Case angepasst werden.
9.2 Empfohlenes Wiring (Host‑Page übergibt SQZ‑Kontext + Resident)
Sie benötigen in der Purchase‑Lines‑Subpage zwei Dinge:
- Den Viewer‑Resident (damit
PostMessage(...)die richtige Control‑Add‑in Instanz trifft) - Den aktuellen SQZ Header‑Kontext (damit Sie SQZ Lines auf das richtige Dokument filtern)
Empfohlenes Pattern auf der Host‑Page (nachdem Sie SqzHeader aufgelöst haben):
trigger OnAfterGetCurrRecord()
var
SharedResident: Codeunit "DXP SQUEEZE Viewer Resident";
SqzHeader: Record "DXP SQZ Document Header";
begin
if not TryResolveSqzHeaderForCurrentRecord(SqzHeader) then
exit;
CurrPage.DXPSQZViewer.Page.SetSqzRecord(SqzHeader);
CurrPage.DXPSQZViewer.Page.GetViewerResident(SharedResident);
ViewerCtx.SetResident(SharedResident);
ViewerCtx.SetSqzContextForPurchInv(Rec."No.", SqzHeader."No.", SqzHeader."API Document ID");
end;
9.3 Minimales Beispiel: Purch. Invoice Subform triggert Highlighting im Viewer
Dieses Beispiel erweitert Purch. Invoice Subform. Beachten Sie: der Datensatz der Basis‑Subform ist ein Record "Purchase Line", daher müssen die Resolver‑Prozeduren Record "Purchase Line" akzeptieren.
Das hier gezeigte Allocation‑Mapping ist die funktionierende Implementierung für Purchase‑Invoice‑Szenarien: es verknüpft über Receipt ("Receipt No." / "Receipt Line No.") statt über Belegnummer/Zeilennummer der Rechnung.
pageextension 50101 "MY Purch. Inv. Sf Mark" extends "Purch. Invoice Subform"
{
trigger OnAfterGetCurrRecord()
begin
MarkSqzLineForPurchInvLine();
end;
var
ApiMgt: Codeunit "DXP SQZ API Mgt.";
Resident: Codeunit "DXP SQUEEZE Viewer Resident";
ViewerState: Codeunit "DXP Viewer State";
LineCoordMgt: Codeunit "DXP SQZ Coordinates Mgt.";
ViewerCtx: Codeunit "DXP SQZ Purch. Inv. Viewer Ctx";
SqzDocNo: Code[20];
ApiDocumentId: Text[50];
local procedure MarkSqzLineForPurchInvLine()
var
SqzLine: Record "DXP SQZ Document Line";
begin
if not ViewerCtx.TryGetSqzContextForPurchInv(Rec."Document No.", SqzDocNo, ApiDocumentId) then
exit;
if not ViewerCtx.TryGetResident(Resident) then
exit;
if not TryResolveSqzLineForPurchLine(SqzDocNo, Rec, SqzLine) then
exit;
if not LineCoordMgt.CoordinateExists(SqzLine.RecordId) then
exit;
if ViewerState.GetViewerMode() = "DXP SQZ Viewer Mode"::Detached then
ViewerState.UpdateViewerStateWithCoordinates(ApiDocumentId, SqzLine.RecordId)
else
ApiMgt.PostMessage(ApiMgt.MarkFieldByCoordinatesObj(SqzLine.RecordId), Resident);
end;
local procedure TryResolveSqzLineForPurchLine(SqzDocNo: Code[20]; PurchLine: Record "Purchase Line"; var SqzLine: Record "DXP SQZ Document Line"): Boolean
begin
// 1) Allocation link via receipt (Purchase Invoice Szenario)
SqzLine.Reset();
SqzLine.SetRange("Document No.", SqzDocNo);
SqzLine.SetFilter("Allocated Document No.", '%1&<>%2', PurchLine."Receipt No.", '');
SqzLine.SetFilter("Allocated Document Line No.", '%1&<>%2', PurchLine."Receipt Line No.", 0);
if SqzLine.FindFirst() then
exit(true);
// 2) Fallback: content-based match (Type/No/Amounts)
exit(TryResolveSqzLineByContent(SqzDocNo, PurchLine, SqzLine));
end;
local procedure TryResolveSqzLineByContent(SqzDocNo: Code[20]; PurchLine: Record "Purchase Line"; var BestSqzLine: Record "DXP SQZ Document Line"): Boolean
var
Candidate: Record "DXP SQZ Document Line";
BestScore: Integer;
Score: Integer;
QtyTol: Decimal;
AmtTol: Decimal;
begin
QtyTol := 0.00001;
AmtTol := 0.01;
BestScore := 0;
Candidate.Reset();
Candidate.SetRange("Document No.", SqzDocNo);
// Prefer narrowing early on stable fields
Candidate.SetRange(Type, PurchLine.Type);
if PurchLine."No." <> '' then
Candidate.SetRange("No.", PurchLine."No.");
if not Candidate.FindSet() then
exit(false);
repeat
Score := 0;
// Strong signals
if Candidate.Type = PurchLine.Type then
Score += 20;
if (PurchLine."No." <> '') and (Candidate."No." = PurchLine."No.") then
Score += 40;
// Weak/medium signals (tolerant)
if (Candidate.Quantity <> 0) and (Abs(Candidate.Quantity - PurchLine.Quantity) <= QtyTol) then
Score += 10;
if (Candidate."Direct Unit Cost" <> 0) and (Abs(Candidate."Direct Unit Cost" - PurchLine."Direct Unit Cost") <= AmtTol) then
Score += 10;
if (Candidate."Line Amount" <> 0) and (Abs(Candidate."Line Amount" - PurchLine."Line Amount") <= AmtTol) then
Score += 10;
if (Candidate.Description <> '') and (PurchLine.Description <> '') and (Candidate.Description = PurchLine.Description) then
Score += 5;
if Score > BestScore then begin
BestScore := Score;
BestSqzLine := Candidate;
end;
until Candidate.Next() = 0;
// Guard: avoid picking a random line when nothing matches well.
exit(BestScore >= 30);
end;
}
Events after creating a SQUEEZE document (CreateSource)
Audience
This documentation is intended for third-party developers who want to extend the creation process of a SQUEEZE document (tables DXP SQZ Document Header/DXP SQZ Document Line).
The focus is on the integration events that are raised after or during ISourceDocument.CreateSource(...).
Context: where is CreateSource called?
The entry point for the SQUEEZE integration is in the DXP SQZ API Mgt. codeunit:
- A Core document (
DXP Document) is created (viaDXP Document Mgt.). - The
DXP ISource Documentinterface is then resolved via the document class andCreateSource(Document)is executed.
Simplified flow (from CreateCoreAndSQUEEZEDocument):
DocumentMgt.AddDocument(DocClass, ...)(create the Core document)ISourceDocument.CreateSource(Document)(create the SQUEEZE document)- Process the import queue / "finish" the SQUEEZE document
Document classes & default behavior
Which CreateSource implementation is executed is defined in the enum extension DXP Document Class Ext..
DXP Invoice / Credit Memo→ codeunitDXP SQZ P. Inv/Crdt Memo Impl.DXP Order Confirmation→ codeunitDXP SQZ P. Order Conf. Impl.
Default (when no specific implementation is set): The DXP Document Class enum defines a DefaultImplementation on DXP Source Document Def. Impl.. This default implementation:
- does not create a SQUEEZE document,
CreateSource(...)returnsfalse.
Common pipeline inside CreateSource
Both SQUEEZE implementations follow (in short) this pattern:
- Guard: the Core document must have
Status = ImportedandJSON Rawmust exist. DXP SQZ Document Mgt.:CreateSQUEEZEDocHeader(Document, DocHeader, RawJson)CreateSQUEEZEDocLine(DocHeader, RawJson)
DXP Document Mgt.:UpdateDocumentAfterTransfer(...)(set the Core document to "Transferred" and link it to the SQZ header)- Order match / autocomplete / attachments / validation (depending on document class and setup)
The most important events are therefore attached to:
- the header/line creation in
DXP SQZ Document Mgt. - the completion of the
CreateSourceprocedure in the respective implementations - optional steps such as order match or validation
Event matrix (short overview)
| Area | Event | Timing | Control | Typical use cases |
|---|---|---|---|---|
DXP SQZ API Mgt. |
OnAfterInitMapping(...) |
after initializing the mapping/token context | – | extend/check mapping |
DXP SQZ API Mgt. |
OnAfterSaveAttachment(...) |
after an attachment has been saved successfully | – | post-processing/indexing |
DXP SQZ API Mgt. |
OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment(...) |
before the attachment record is modified | – | influence file name/origin |
DXP SQZ P. Inv/Crdt Memo Impl. |
OnAfterCreateDocLines |
directly after CreateSQUEEZEDocLine(...) |
– | evaluate lines/RawJson after creation |
DXP SQZ P. Inv/Crdt Memo Impl. |
OnAfterCreateSource |
at the end of CreateSource(...) |
– | post actions after autocomplete/order match/validation |
DXP SQZ P. Inv/Crdt Memo Impl. |
OnBeforeDoPlausibilityChecks |
before the plausibility checks | IsHandled |
replace/extend checks |
DXP SQZ P. Inv/Crdt Memo Impl. |
OnAfterDoPlausibilityChecks |
after the plausibility checks | – | reuse the result (entries) |
DXP SQZ P. Order Conf. Impl. |
OnBeforeDoPlausibilityChecks |
before the plausibility checks | IsHandled |
replace/extend checks |
DXP SQZ Document Mgt. |
OnBeforeModifySQUEEZEDocumentHeader |
in CreateSQUEEZEDocHeader(...) before the final modify |
– | add/normalize header values from RawJson |
DXP SQZ Document Mgt. |
OnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues |
in standard line post-processing before TransferRecognizedValues |
– | correct recognized values per line |
DXP SQZ Document Mgt. |
OnAfterTransferRecognizedValuesOnBeforeInsertDocLine |
in standard line post-processing after TransferRecognizedValues |
– | line-based derivations (account assignment/dimensions) |
DXP SQZ Document Mgt. |
OnBeforePerformAutomaticOrdermatch |
before the automatic order match | IsHandled |
take over order match completely/adjust OrderNoList |
| JSON creation | OnBeforeAddLineToDocumentJObj |
before the line structure is built | – | extend the header JSON |
| JSON creation | OnBeforeAddLineJObjToLineJArray |
before LineJArray.Add(LineJObj) |
– | extend the line JSON |
Events in DXP SQZ P. Inv/Crdt Memo Impl. (document class: Invoice / Credit Memo)
This codeunit provides the strongest extension points directly in the CreateSource flow.
OnAfterCreateDocLines
Raised directly after CreateSQUEEZEDocLine(...) (that is, once the header & lines exist).
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterCreateDocLines(DocHeader: Record "DXP SQZ Document Header"; RawJson: JsonObject)
Typical use cases:
- additional header initialization (for example custom flags/fields)
- additional line-based evaluations directly after creation
- extract custom metadata from
RawJson
OnAfterCreateSource
Raised at the end of CreateSource(...) (after autocomplete/order match/optional attachment download/optional auto-validate).
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterCreateSource(DocHeader: Record "DXP SQZ Document Header"; RawJson: JsonObject)
Typical use cases:
- start downstream automations (workflows, notifications)
- additional data validation or custom status logic
Plausibility check events
These events relate to the IsSourceDataPlausible(...) method and are relevant if you extend/replace plausibility checks.
Signatures (excerpt):
[IntegrationEvent(false, false)]
local procedure OnBeforeDoPlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; var TempPlausibilityCheckEntry: Record "DXP Plausibility Check Entry" temporary; var IsHandled: Boolean)
[IntegrationEvent(false, false)]
local procedure OnBeforeDoHeaderPlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
[IntegrationEvent(false, false)]
local procedure OnBeforeDoLinePlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; DocLine: Record "DXP SQZ Document Line"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
[IntegrationEvent(false, false)]
local procedure OnAfterDoPlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; var TempPlausibilityCheckEntry: Record "DXP Plausibility Check Entry" temporary)
JSON creation (Processed JSON from the SQZ document)
The implementation contains events before line JSON is added to the header structure:
OnBeforeAddLineToDocumentJObj
[IntegrationEvent(false, false)]
local procedure OnBeforeAddLineToDocumentJObj(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
OnBeforeAddLineJObjToLineJArray
[IntegrationEvent(false, false)]
local procedure OnBeforeAddLineJObjToLineJArray(var LineJObj: JsonObject; DocLine: Record "DXP SQZ Document Line")
Use cases:
- write additional fields into the header JSON
- add additional fields per line (custom tokens)
Events in DXP SQZ P. Order Conf. Impl. (document class: Order Confirmation)
This codeunit has no dedicated OnAfterCreateSource events in CreateSource(...). Extensions here typically happen via:
DXP SQZ Document Mgt.(header/line creation, order match)- the JSON creation (
CreateProcessedJsonFromSource) - plausibility check events
JSON creation
[IntegrationEvent(false, false)]
local procedure OnBeforeAddLineToDocumentJObj(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
[IntegrationEvent(false, false)]
local procedure OnBeforeAddLineJObjToLineJArray(var LineJObj: JsonObject; DocLine: Record "DXP SQZ Document Line")
Plausibility check events
Analogous to the Invoice/Credit Memo implementation:
OnBeforeDoPlausibilityChecksOnBeforeDoHeaderPlausibilityChecksOnBeforeDoLinePlausibilityChecksOnAfterDoPlausibilityChecks
Events in DXP SQZ Document Mgt. (common to all document classes)
These events are especially relevant because they are raised within the header/line creation and order match pipeline.
OnBeforeModifySQUEEZEDocumentHeader
Raised in CreateSQUEEZEDocHeader(...), after values have been assigned via field mapping and before the header is finally modified.
Signature:
[IntegrationEvent(false, false)]
local procedure OnBeforeModifySQUEEZEDocumentHeader(var DocHeader: Record "DXP SQZ Document Header"; RawJson: JsonObject)
Use cases:
- adjust header fields before the standard post-processing runs
- take additional values from
RawJsoninto the header
OnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues
OnAfterTransferRecognizedValuesOnBeforeInsertDocLine
Raised in the standard line post-processing (for standard document classes) around TransferRecognizedValues(DocLine).
Signatures:
[IntegrationEvent(false, false)]
local procedure OnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues(var DocLine: Record "DXP SQZ Document Line"; RowJTok: JsonToken)
[IntegrationEvent(false, false)]
local procedure OnAfterTransferRecognizedValuesOnBeforeInsertDocLine(var DocLine: Record "DXP SQZ Document Line")
Use cases:
- correct/normalize recognized values per line
- prepare additional derivations (for example account assignment, dimensions)
OnBeforePerformAutomaticOrdermatch
Raised in PerformAutomaticOrdermatch(...), after the order number list (OrderNoList) has been filled and before the automatic order match runs.
Signature:
[IntegrationEvent(false, false)]
local procedure OnBeforePerformAutomaticOrdermatch(DocHeader: Record "DXP SQZ Document Header"; var OrderNoList: List of [Code[20]]; var IsHandled: Boolean)
Use cases:
- take over the automatic order match logic completely (
IsHandled := true) - adjust the OrderNoList (for example filter/extend)
Events in DXP SQZ API Mgt. (close to creation/attachments)
These events sit "around" the API communication (for example attachments).
OnAfterInitMapping
Raised in DownloadFieldMapping(...), after the document class has initialized the default mapping via the DXP ISource Document interface (ISourceDocument.InitFieldMapping(...)) and before the field list from SQUEEZE is synchronized into the BC field mapping structures.
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterInitMapping(var HeaderMapping: Dictionary of [Text, Integer]; var LineMapping: Dictionary of [Text, Integer]; DocClass: Enum "DXP Document Class")
Parameters:
HeaderMapping/LineMapping: dictionaries with the default mapping SQZ field name → BC field no. (modifiable viavar)DocClass: the document class for which the mapping is loaded
Typical use cases:
- add additional (customer-specific) field names as default mapping
- correct the default mapping (for example when SQZ field names have changed)
- mapping validation/logging per document class
OnAfterSaveAttachment
Raised in SaveAttachments(...) per attachment, after the attachment has been created via DXP Document Attachment Mgt. and – if the attachment is marked as origin file – after the Modify(true) of the record.
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterSaveAttachment(DocumentAttachment: Record "DXP Document Attachment")
DocumentAttachment is not passed as var. If you want to change fields on the record persistently, you must reload the record in the subscriber and explicitly call Modify(...).
Typical use cases:
- index/archive the attachment externally
- derive metadata (for example classification by MIME type/file extension)
- trigger follow-up processes (for example OCR/preview generation in a third-party system)
OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment
Raised in SaveAttachments(...), when the attachment is marked as origin file according to the payload and after Is Source File and File Name (origin name) have been set – but before DocumentAttachment.Modify(true) is called.
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment(var DocumentAttachment: Record "DXP Document Attachment")
Typical use cases:
- normalize the origin file name (for example remove forbidden characters, enforce a naming scheme)
- set additional fields on the attachment before it is saved
Minimal examples: EventSubscriber for DXP SQZ API Mgt.
The following examples show typical, small extensions. Adjust the object IDs/names to your namespace.
codeunit 50101 "My SQZ API Mgt. Subs"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnAfterInitMapping', '', false, false)]
local procedure OnAfterInitMapping(var HeaderMapping: Dictionary of [Text, Integer]; var LineMapping: Dictionary of [Text, Integer]; DocClass: Enum "DXP Document Class")
var
SqzDocHeader: Record "DXP SQZ Document Header";
SqzDocLine: Record "DXP SQZ Document Line";
begin
// Example: add a new SQZ field as default mapping.
// Important: The integer value is the BC field no. of the target field (depends on your mapping concept).
if not HeaderMapping.ContainsKey('my_custom_header_token') then
HeaderMapping.Add('my_custom_header_token', SqzDocHeader.FieldNo("Vendor No."));
if not LineMapping.ContainsKey('my_custom_line_token') then
LineMapping.Add('my_custom_line_token', SqzDocLine.FieldNo(Description));
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment', '', false, false)]
local procedure OnAfterSetOriginFileNameOnBeforeModifyDocumentAttachment(var DocumentAttachment: Record "DXP Document Attachment")
begin
// Example: normalize the origin file name (remove whitespace).
DocumentAttachment."File Name" := DelChr(DocumentAttachment."File Name", '=', ' ');
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnAfterSaveAttachment', '', false, false)]
local procedure OnAfterSaveAttachment(DocumentAttachment: Record "DXP Document Attachment")
begin
// Example: kick off post-processing.
// If you want to change the attachment persistently, you can also modify the parameter record directly.
// DocumentAttachment.Validate("Your Field", ...);
// DocumentAttachment.Modify(true);
end;
}
Events after creating a Core document (DXP Document)
Audience
This documentation is intended for third-party developers who want to extend the lifecycle of the Core document DXP Document – in particular at the points where:
- the status / "Next Process Step" is evaluated,
JSON Raw/JSON Processedis updated,- standard documents are created (for example purchase documents),
- attachments are transferred.
Context: Core document creation vs. further processing
1) The Core document is created (AddDocument)
A DXP Document is created in DXP Document Mgt. via AddDocument(...):
- The status is set to Imported.
JSON Rawis written (if present) to a BLOB field.
2) Afterwards: transfer/linking and processing
Typical next steps (depending on the app flow):
- Linking to a source document (for example a SQUEEZE header) via
UpdateDocumentAfterTransfer(...) - Creating a standard document and writing
JSON ProcessedviaUpdateDocument(...)
Most extension points are therefore not directly at the insert, but in the update/processing flow.
Event matrix (short overview)
| Area | Event | Timing | Control | Typical use cases |
|---|---|---|---|---|
DXP Document Mgt. |
OnAfterSetLinkedToRecIdOnBeforeModify |
in UpdateDocumentAfterTransfer(...) before Modify(true) |
– | set additional info as soon as Linked-to Record Id is set |
DXP Document Mgt. |
OnBeforeGetDocumentOnBeforeCheckStatusAndNextProcessStep |
early in UpdateDocument(...) before Document.Get(DocNo) |
IsHandled |
overwrite status/next step, take over processing |
DXP Document Mgt. |
OnAfterGetDocumentOnBeforeCheckStatusAndNextProcessStep |
in UpdateDocument(...) after Get(...) before standard document creation |
Handled |
decide depending on the document, adjust JSON/status |
DXP Document Mgt. |
OnAfterUpdateRawJson |
after writing JSON Raw in UpdateRawJsonJson(...) |
– | normalize raw JSON/additional derivations |
DXP Document Mgt. |
OnAfterWriteProcessedJsonToBlob |
after writing JSON Processed in UpdateStatusAndProcessedJson(...) |
– | finalize processed JSON, add tokens |
DXP Document Mgt. |
OnBeforeTransferCoreAttachmentsToStandardDocument |
before the attachment transfer | IsHandled |
replace/selectively control the transfer |
DXP Document Transfer Mgt. |
OnBeforeExitProcessedJsonDocHeader |
when creating header JSON (before exit(JObject)) |
– | add/normalize standard tokens |
DXP Document Transfer Mgt. |
OnBeforeExitProcessedJsonDocLine |
when creating line JSON (before exit(JObject)) |
– | add/normalize line tokens |
DXP Document Transfer Mgt. |
OnAfterCreatePurchaseHeader |
after creating/validating the purchase header | – | add header fields, post-processing |
DXP Document Transfer Mgt. |
OnBeforeInsertPurchaseLine |
before inserting a purchase line | – | adjust purchase line (for example account assignment, type/no.) |
DXP Document Transfer Mgt. |
OnAfterInsertPurchaseLineOnBeforeAddDimensions |
after insert, before dimensions | – | prepare dimensions/additional info |
DXP Document Transfer Mgt. |
OnAfterOnBeforeAddLineDimensions |
for receipt lines: before the dimensions of the line to be updated | – | adjust update cases (receipt) |
DXP Document Transfer Mgt. |
OnAfterProcessAdditionalInformation |
after transferring additional information | – | add add-on fields per target RecordRef |
DXP Document Transfer Mgt. |
OnAfterCreatePurchaseDoc |
after the complete creation of the purchase document | – | final hook (logging, follow-up actions) |
Events in DXP Document Mgt. (Core lifecycle)
OnAfterSetLinkedToRecIdOnBeforeModify
Raised in UpdateDocumentAfterTransfer(...) – after the status/Linked-to Record Id have been set, but before the DXP Document is saved.
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterSetLinkedToRecIdOnBeforeModify(var Document: Record "DXP Document")
Use cases:
- set additional information on the Core document as soon as it is linked to a source document
- add your own logging/audit information
OnBeforeGetDocumentOnBeforeCheckStatusAndNextProcessStep
Called in UpdateDocument(...) as the earliest hook – even before the DXP Document is loaded via Get(DocNo).
Signature:
[IntegrationEvent(false, false)]
local procedure OnBeforeGetDocumentOnBeforeCheckStatusAndNextProcessStep(
DocNo: Code[20];
var Status: Enum "DXP Document Status";
var NextStep: Enum "DXP Next Process Step";
JObject: JsonObject;
var IsHandled: Boolean)
Use cases:
- overwrite status/next step in advance (for example "Custom Processing")
- take over the complete processing (
IsHandled := true)
OnAfterGetDocumentOnBeforeCheckStatusAndNextProcessStep
Raised in UpdateDocument(...) after Document.Get(DocNo), before standard documents are created.
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterGetDocumentOnBeforeCheckStatusAndNextProcessStep(
var Document: Record "DXP Document";
NewStatus: Enum "DXP Document Status";
NextStep: Enum "DXP Next process step";
var ProcessedJSONObj: JsonObject;
var Handled: Boolean)
Use cases:
- decide, depending on the current Core document, whether processing should be taken over
- adjust the
ProcessedJSONObjbefore further processing
OnAfterUpdateRawJson
Raised in UpdateRawJsonJson(...) after writing JSON Raw.
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterUpdateRawJson(var Document: Record "DXP Document"; var RawJSONObj: JsonObject)
Use cases:
- validation/normalization steps on
RawJSONObj - additional data extraction directly after the raw update
OnAfterWriteProcessedJsonToBlob
Raised in UpdateStatusAndProcessedJson(...) after JSON Processed has been written to the BLOB.
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterWriteProcessedJsonToBlob(var Document: Record "DXP Document"; var ProcessedJSONObj: JsonObject)
Use cases:
- add additional tokens
- finalize the data structure before downstream processes access it
OnBeforeTransferCoreAttachmentsToStandardDocument
Raised in TransferCoreAttachmentsToStandardDocument(...) before the transfer. The standard logic can be replaced via IsHandled.
Signature:
[IntegrationEvent(false, false)]
local procedure OnBeforeTransferCoreAttachmentsToStandardDocument(Document: Record "DXP Document"; var IsHandled: Boolean)
Use cases:
- take over the attachment transfer completely (for example different storage/naming)
- restrict the transfer selectively
Events in DXP Document Transfer Mgt. (standard documents from JSON Processed)
This codeunit is relevant as soon as standard documents (for example purchase documents) are created from JSON Processed.
OnBeforeExitProcessedJsonDocHeader
Raised in CreateDocHeaderJson(...) immediately before exit(JObject).
Signature:
[IntegrationEvent(false, false)]
local procedure OnBeforeExitProcessedJsonDocHeader(var JObject: JsonObject)
Use cases:
- add additional standard tokens
- normalize token values (for example type/text representation)
OnBeforeExitProcessedJsonDocLine
Raised in CreateDocLineJson(...) immediately before exit(JObject).
Signature:
[IntegrationEvent(false, false)]
local procedure OnBeforeExitProcessedJsonDocLine(var JObject: JsonObject)
Use cases:
- add/normalize line tokens
OnAfterCreatePurchaseHeader
Raised after the purchase header is created/validated.
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterCreatePurchaseHeader(var JObject: JsonObject; PurchaseHeader: Record "Purchase Header")
Use cases:
- set additional fields on the
Purchase Header(for example custom references) - post-processing/logging
OnBeforeInsertPurchaseLine
Raised immediately before a purchase line is inserted.
Signature:
[IntegrationEvent(false, false)]
local procedure OnBeforeInsertPurchaseLine(var JObjectLine: JsonObject; var PurchaseLine: Record "Purchase Line")
Use cases:
- adjust the purchase line (for example account assignment, type/no.)
OnAfterInsertPurchaseLineOnBeforeAddDimensions
Raised after the purchase line is inserted, but before dimension validation.
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterInsertPurchaseLineOnBeforeAddDimensions(var JObjectLine: JsonObject; PurchaseLine: Record "Purchase Line")
Use cases:
- prepare dimensions/additional info
OnAfterOnBeforeAddLineDimensions
Raised in the receipt line update case, before dimensions are set for the purchase line to be updated.
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterOnBeforeAddLineDimensions(var JObjectLine: JsonObject; PurchaseLineToUpdate: Record "Purchase Line")
Use cases:
- adjust update cases (receipt)
OnAfterProcessAdditionalInformation
Raised after the transfer of additional information (ProcessAdditionalInformation).
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterProcessAdditionalInformation(var RecRef: RecordRef; SourceJsonObject: JsonObject)
Use cases:
- add add-on fields for different target tables (header/line)
OnAfterCreatePurchaseDoc
Raised after the complete creation of the purchase document.
Signature:
[IntegrationEvent(false, false)]
local procedure OnAfterCreatePurchaseDoc(var JObject: JsonObject; PurchaseHeader: Record "Purchase Header")
Use cases:
- final hook (post-processing, logging, follow-up actions)
Minimal example: influencing status/next step toward "Custom Processing"
Example: set Status to "Custom Processing" before the standard processing:
codeunit 50101 "My Core Document Hooks"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Mgt.", 'OnBeforeGetDocumentOnBeforeCheckStatusAndNextProcessStep', '', false, false)]
local procedure OnBeforeGetDoc(
DocNo: Code[20];
var Status: Enum "DXP Document Status";
var NextStep: Enum "DXP Next Process Step";
JObject: JsonObject;
var IsHandled: Boolean)
begin
Status := Status::"Custom Processing";
// optionally adjust NextStep
end;
}
[Best Practice] Use IsHandled := true only when you fully replace the standard logic – otherwise it can lead to inconsistent status/JSON states.
Adding a field in the validation (up to Squeeze BC App version 1.*)
The following uses an example extension to show how, as a developer, you can add fields in the validation.
Required integration events including example procedures:
codeunit 50100 EventSubs
{
//
//Any header field that you want to transfer to the resulting document, has to be added to the source JSON Object
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforeAddLineToDocumentJObj', '', false, false)]
local procedure SQZDocMgtOnBeforeAddLineToDocumentJObj(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
begin
AddCustomHeaderFieldsToJson(DocumentJObj, DocHeader);
end;
//
// Any line field that you want to transfer to the resulting document, has to be added to the source JSON Object
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforeAddLineJObjToLineJArray', '', false, false)]
local procedure SQZDocMgtOnBeforeAddLineJObjToLineJArray(var LineJObj: JsonObject; DocLine: Record "DXP SQZ Document Line")
begin
AddCustomLineFieldsToJson(LineJObj, DocLine);
end;
//
// [If you want to perform plausibility checks on the newly added header field, this is the place]
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnAfterDoHeaderPlausibilityChecks', '', false, false)]
local procedure SQZDocMgtOnBeforeDoHeaderPlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
begin
CheckCustomer(PlausibilityCheck, DocHeader."Customer No.");
end;
//
// [If you want to perform plausibility checks on the newly added line field, this is the place]
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnAfterDoLinePlausibilityChecks', '', false, false)]
local procedure SQZDocMgtOnAfterDoLinePlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; DocLine: Record "DXP SQZ Document Line"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
begin
CheckCustomer(PlausibilityCheck, DocLine."Customer No.");
end;
//
// The value of the previously extended JSON Object now has to saved to the corresponding field in the SQUEEZE Document Line
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforeModifySQUEEZEDocumentHeader', '', false, false)]
local procedure SqzDocMgtOnBeforeModifySQUEEZEDocumentHeader(var DocHeader: Record "DXP SQZ Document Header"; RawJson: JsonObject)
var
JSONHelper: Codeunit "DXP Json Helper";
SQZApiTokenMgt: Codeunit "DXP SQZ API Token Mgt.";
TokenMgt: Codeunit TokenMgt;
begin
DocHeader."Customer No." := COPYSTR(JsonHelper.ValAsTxt(RawJson, SQZApiTokenMgt.GetHeaderValueByNameTok(TokenMgt.GetCustomerNoTok()), false), 1, MaxStrLen(DocHeader."Customer No."));
end;
//
// The value of the previously extended JSON Object now has to saved to the corresponding field in the SQUEEZE Document Header
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues', '', false, false)]
local procedure SQZDocMgtOnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues(RowJTok: JsonToken; var DocLine: Record "DXP SQZ Document Line")
var
JSONHelper: Codeunit "DXP Json Helper";
SQZApiTokenMgt: Codeunit "DXP SQZ API Token Mgt.";
TokenMgt: Codeunit TokenMgt;
begin
DocLine."Customer No." := CopyStr(JsonHelper.ValAsTxt(RowJTok.AsObject(), SQZApiTokenMgt.GetCellValueTok(TokenMgt.GetCustomerNoTok()), false), 1, MaxStrLen(DocLine."Customer No."));
end;
//
// The newly added fields now have to be added to the field mapping
// Otherwise the recognized values will be saved as meta data
// This step is necessary to make sure that the fields can be highlighted in the SQUEEZE Viewer
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnAfterInitMapping', '', false, false)]
local procedure SQZApiMgtOnAfterInitMapping(var HeaderMapping: Dictionary of [Text, Integer]; var LineMapping: Dictionary of [Text, Integer]; DocClass: Enum "DXP Document Class")
var
SQZDocHeader: Record "DXP SQZ Document Header";
SQZDocLine: Record "DXP SQZ Document Line";
TokenMgt: Codeunit TokenMgt;
begin
// The standard (without installed extensions) supports one Document Class
case DocClass of
DocClass::"DXP Invoice / Credit Memo":
begin
HeaderMapping.Add(TokenMgt.GetCustomerNoTok(), SQZDocHeader.FieldNo("Customer No."));
LineMapping.Add(TokenMgt.GetCustomerNoTok(), SQZDocLine.FieldNo("Customer No."));
end;
end;
end;
//
// The value in the JSON Object now has to be transferred to the Purchase Header
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Transfer Mgt.", 'OnAfterCreatePurchaseHeader', '', false, false)]
local procedure DocTransferMgtOnAfterCreatePurchaseHeader(var JObject: JsonObject; PurchaseHeader: Record "Purchase Header")
begin
TransferCustomHeaderFieldFromJsonToPurchaseHeader(JObject, PurchaseHeader);
end;
//
// The value in the JSON Object now has to be transferred to the Purchase Line
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Transfer Mgt.", 'OnBeforeInsertPurchaseLine', '', false, false)]
local procedure DocTransferMgtOnAfterInsertPurchaseLineOnBeforeAddDimensions(var JObjectLine: JsonObject; var PurchaseLine: Record "Purchase Line")
begin
TransferCustomHeaderFieldFromJsonToPurchaseLine(JObjectLine, PurchaseLine);
end;
local procedure TransferCustomHeaderFieldFromJsonToPurchaseHeader(var JObject: JsonObject; PurchaseHeader: Record "Purchase Header")
var
JsonHelper: Codeunit "DXP Json Helper";
TokenMgt: Codeunit TokenMgt;
begin
PurchaseHeader.Validate("DXP Customer No.", JsonHelper.ValAsTxt(JObject, TokenMgt.GetCustomerNoTok(), false));
PurchaseHeader.Modify();
end;
local procedure TransferCustomHeaderFieldFromJsonToPurchaseLine(var JObjectLine: JsonObject; var PurchaseLine: Record "Purchase Line")
var
JsonHelper: Codeunit "DXP Json Helper";
TokenMgt: Codeunit TokenMgt;
begin
PurchaseLine.Validate("DXP Customer No.", JsonHelper.ValAsTxt(JObjectLine, TokenMgt.GetCustomerNoTok(), false));
end;
local procedure AddCustomHeaderFieldsToJson(var DocumentJObj: JsonObject; SQZDocumentHeader: Record "DXP SQZ Document Header")
var
TokenMgt: Codeunit TokenMgt;
begin
DocumentJObj.Add(TokenMgt.GetCustomerNoTok(), SQZDocumentHeader."Customer No.");
end;
local procedure AddCustomLineFieldsToJson(var LineJObj: JsonObject; SQZDocumentLine: Record "DXP SQZ Document Line")
var
TokenMgt: Codeunit TokenMgt;
begin
LineJObj.Add(TokenMgt.GetCustomerNoTok(), SQZDocumentLine."Customer No.");
end;
[TryFunction]
local procedure CustomerExists(CustomerNo: Code[20])
var
Customer: Record Customer;
begin
Customer.Get(CustomerNo);
end;
local procedure CheckCustomer(var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt."; CustomerNo: Code[20]): Boolean
begin
if CustomerNo = '' then
exit(false);
if not CustomerExists(CustomerNo) then begin
PlausibilityCheck.AddPlausibilityCheckEntry(GetLastErrorText(), Page::"Customer List");
exit(false);
end;
exit(true);
end;
}
Table Extensions:
SQUEEZE 4 BC
tableextension 50100 "SQZ Doc. Header Ext." extends "DXP SQZ Document Header"
{
fields
{
field(50100; "Customer No."; Code[20])
{
TableRelation = Customer;
ValidateTableRelation = false;
Caption = 'Customer No.';
}
}
}
tableextension 50101 "SQZ Doc. Line Ext." extends "DXP SQZ Document Line"
{
fields
{
field(50100; "Customer No."; Code[20])
{
TableRelation = Customer;
ValidateTableRelation = false;
Caption = 'Customer No.';
}
}
}
Target document (here: purchase document)
tableextension 50102 "Purchase Header Ext." extends "Purchase Header"
{
fields
{
field(50100; "DXP Customer No."; Code[20])
{
DataClassification = CustomerContent;
TableRelation = Customer;
Caption = 'DEXPRO Customer No.';
}
}
}
Page Extensions:
SQUEEZE 4 BC
pageextension 50100 "SQZ Document Ext." extends "DXP SQZ Document"
{
layout
{
addafter(BuyFromVendorInternal)
{
field("Customer No. Internal"; Rec."Customer No.")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Customer No. field.';
trigger OnAssistEdit()
begin
MarkField(Rec.FieldNo("Customer No."), Rec."Customer No.", Rec);
end;
trigger OnValidate()
begin
CheckPlausibility();
end;
}
}
addafter(BuyFromVendor)
{
field("Customer No."; Rec."Customer No.")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Customer No. field.';
trigger OnAssistEdit()
var
ApiMgt: Codeunit "DXP SQZ API Mgt.";
ViewerResident: Codeunit "DXP SQUEEZE Viewer Resident";
begin
MarkField(Rec.FieldNo("Customer No."), Rec."Customer No.", Rec);
end;
trigger OnValidate()
begin
CheckPlausibility();
end;
}
}
}
local procedure MarkField(AppFldNo: Integer; FieldVal: Variant; DocHeader: Record "DXP SQZ Document Header")
var
ApiMgt: Codeunit "DXP SQZ API Mgt.";
ViewerResident: Codeunit "DXP SQUEEZE Viewer Resident";
begin
ViewerResident := GetViewerResident();
ApiMgt.MarkField(AppFldNo, FieldVal, ViewerResident, DocHeader);
end;
}
pageextension 50101 "SQZ Document Sf. Ext." extends "DXP SQZ Document Subform"
{
layout
{
addafter("No.")
{
field("Customer No."; Rec."Customer No.")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Customer No. field.';
}
}
}
}
Target document (here: purchase document)
pageextension 50102 "Purchase Invoice Ext." extends "Purchase Invoice"
{
layout
{
addafter("Buy-from Vendor No.")
{
field("DXP Customer No."; Rec."DXP Customer No.")
{
ToolTip = 'Specifies the value of the DEXPRO Customer No. field.';
ApplicationArea = all;
}
}
}
}
pageextension 50103 "Purchase Inv. Sf Ext." extends "Purch. Invoice Subform"
{
layout
{
addafter("No.")
{
field("DXP Customer No."; Rec."DXP Customer No.")
{
ToolTip = 'Specifies the value of the DEXPRO Customer No. field.';
ApplicationArea = all;
}
}
}
}
Optional helper objects:
codeunit 50101 TokenMgt
{
procedure GetCustomerNoTok(): Text
begin
exit(CustomerNoTok);
end;
var
CustomerNoTok: Label 'customerNo', Locked = true;
}
Adding a field in the validation (from version 2.0)
The following uses an example extension to show how, as a developer, you can add fields in the validation (here: document class Invoice/Credit Memo).
Required integration events including example procedures:
codeunit 50100 EventSubs
{
//
//Any header field that you want to transfer to the resulting document, has to be added to the source JSON Object
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnBeforeAddLineToDocumentJObj', '', false, false)]
local procedure SQZPInvCrdtMemoImplOnBeforeAddLineToDocumentJObj(var DocumentJObj: JsonObject; DocHeader: Record "DXP SQZ Document Header")
begin
AddCustomHeaderFieldsToJson(DocumentJObj, DocHeader);
end;
//
// Any line field that you want to transfer to the resulting document, has to be added to the source JSON Object
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnBeforeAddLineJObjToLineJArray', '', false, false)]
local procedure SQZPInvCrdtMemoImplOnBeforeAddLineJObjToLineJArray(var LineJObj: JsonObject; DocLine: Record "DXP SQZ Document Line")
begin
AddCustomLineFieldsToJson(LineJObj, DocLine);
end;
//
// [If you want to perform plausibility checks on the newly added header field, this is the place]
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnAfterDoHeaderPlausibilityChecks', '', false, false)]
local procedure SQZPInvCrdtMemoImplOnBeforeDoHeaderPlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
begin
CheckCustomer(PlausibilityCheck, DocHeader."Customer No.");
end;
//
// [If you want to perform plausibility checks on the newly added line field, this is the place]
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.", 'OnAfterDoLinePlausibilityChecks', '', false, false)]
local procedure SQZPInvCrdtMemoImplOnAfterDoLinePlausibilityChecks(DocHeader: Record "DXP SQZ Document Header"; DocLine: Record "DXP SQZ Document Line"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
begin
CheckCustomer(PlausibilityCheck, DocLine."Customer No.");
end;
//
// The value of the previously extended JSON Object now has to saved to the corresponding field in the SQUEEZE Document Line
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforeModifySQUEEZEDocumentHeader', '', false, false)]
local procedure SqzDocMgtOnBeforeModifySQUEEZEDocumentHeader(var DocHeader: Record "DXP SQZ Document Header"; RawJson: JsonObject)
var
JSONHelper: Codeunit "DXP Json Helper";
SQZApiTokenMgt: Codeunit "DXP SQZ API Token Mgt.";
TokenMgt: Codeunit TokenMgt;
begin
DocHeader."Customer No." := COPYSTR(JsonHelper.ValAsTxt(RawJson, SQZApiTokenMgt.GetHeaderValueByNameTok(TokenMgt.GetCustomerNoTok()), false), 1, MaxStrLen(DocHeader."Customer No."));
end;
//
// The value of the previously extended JSON Object now has to saved to the corresponding field in the SQUEEZE Document Header
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.", 'OnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues', '', false, false)]
local procedure SQZDocMgtOnBeforeTransferRecognizedValuesOnAfterAssignRecognizedValues(RowJTok: JsonToken; var DocLine: Record "DXP SQZ Document Line")
var
JSONHelper: Codeunit "DXP Json Helper";
SQZApiTokenMgt: Codeunit "DXP SQZ API Token Mgt.";
TokenMgt: Codeunit TokenMgt;
begin
DocLine."Customer No." := CopyStr(JsonHelper.ValAsTxt(RowJTok.AsObject(), SQZApiTokenMgt.GetCellValueTok(TokenMgt.GetCustomerNoTok()), false), 1, MaxStrLen(DocLine."Customer No."));
end;
//
// The newly added fields now have to be added to the field mapping
// Otherwise the recognized values will be saved as meta data
// This step is also necessary to make sure that the fields can be highlighted in the SQUEEZE Viewer
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnAfterInitMapping', '', false, false)]
local procedure SQZApiMgtOnAfterInitMapping(var HeaderMapping: Dictionary of [Text, Integer]; var LineMapping: Dictionary of [Text, Integer]; DocClass: Enum "DXP Document Class")
var
SQZDocHeader: Record "DXP SQZ Document Header";
SQZDocLine: Record "DXP SQZ Document Line";
TokenMgt: Codeunit TokenMgt;
begin
case DocClass of
DocClass::"DXP Invoice / Credit Memo":
begin
HeaderMapping.Add(TokenMgt.GetCustomerNoTok(), SQZDocHeader.FieldNo("Customer No."));
LineMapping.Add(TokenMgt.GetCustomerNoTok(), SQZDocLine.FieldNo("Customer No."));
end;
end;
end;
//
// The value in the JSON Object now has to be transferred to the Purchase Header
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Transfer Mgt.", 'OnAfterCreatePurchaseHeader', '', false, false)]
local procedure DocTransferMgtOnAfterCreatePurchaseHeader(var JObject: JsonObject; PurchaseHeader: Record "Purchase Header")
begin
TransferCustomHeaderFieldFromJsonToPurchaseHeader(JObject, PurchaseHeader);
end;
//
// The value in the JSON Object now has to be transferred to the Purchase Line
//
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP Document Transfer Mgt.", 'OnBeforeInsertPurchaseLine', '', false, false)]
local procedure DocTransferMgtOnAfterInsertPurchaseLineOnBeforeAddDimensions(var JObjectLine: JsonObject; var PurchaseLine: Record "Purchase Line")
begin
TransferCustomHeaderFieldFromJsonToPurchaseLine(JObjectLine, PurchaseLine);
end;
local procedure TransferCustomHeaderFieldFromJsonToPurchaseHeader(var JObject: JsonObject; PurchaseHeader: Record "Purchase Header")
var
JsonHelper: Codeunit "DXP Json Helper";
TokenMgt: Codeunit TokenMgt;
begin
PurchaseHeader.Validate("DXP Customer No.", JsonHelper.ValAsTxt(JObject, TokenMgt.GetCustomerNoTok(), false));
PurchaseHeader.Modify();
end;
local procedure TransferCustomHeaderFieldFromJsonToPurchaseLine(var JObjectLine: JsonObject; var PurchaseLine: Record "Purchase Line")
var
JsonHelper: Codeunit "DXP Json Helper";
TokenMgt: Codeunit TokenMgt;
begin
PurchaseLine.Validate("DXP Customer No.", JsonHelper.ValAsTxt(JObjectLine, TokenMgt.GetCustomerNoTok(), false));
end;
local procedure AddCustomHeaderFieldsToJson(var DocumentJObj: JsonObject; SQZDocumentHeader: Record "DXP SQZ Document Header")
var
TokenMgt: Codeunit TokenMgt;
begin
DocumentJObj.Add(TokenMgt.GetCustomerNoTok(), SQZDocumentHeader."Customer No.");
end;
local procedure AddCustomLineFieldsToJson(var LineJObj: JsonObject; SQZDocumentLine: Record "DXP SQZ Document Line")
var
TokenMgt: Codeunit TokenMgt;
begin
LineJObj.Add(TokenMgt.GetCustomerNoTok(), SQZDocumentLine."Customer No.");
end;
[TryFunction]
local procedure CustomerExists(CustomerNo: Code[20])
var
Customer: Record Customer;
begin
Customer.Get(CustomerNo);
end;
local procedure CheckCustomer(var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt."; CustomerNo: Code[20]): Boolean
begin
if CustomerNo = '' then
exit(false);
if not CustomerExists(CustomerNo) then begin
PlausibilityCheck.AddPlausibilityCheckEntry(GetLastErrorText(), Page::"Customer List");
exit(false);
end;
exit(true);
end;
}
Table Extensions:
SQUEEZE 4 BC
tableextension 50100 "SQZ Doc. Header Ext." extends "DXP SQZ Document Header"
{
fields
{
field(50100; "Customer No."; Code[20])
{
TableRelation = Customer;
ValidateTableRelation = false;
Caption = 'Customer No.';
}
}
}
tableextension 50101 "SQZ Doc. Line Ext." extends "DXP SQZ Document Line"
{
fields
{
field(50100; "Customer No."; Code[20])
{
TableRelation = Customer;
ValidateTableRelation = false;
Caption = 'Customer No.';
}
}
}
Target document (here: purchase document)
tableextension 50102 "Purchase Header Ext." extends "Purchase Header"
{
fields
{
field(50100; "DXP Customer No."; Code[20])
{
DataClassification = CustomerContent;
TableRelation = Customer;
Caption = 'DEXPRO Customer No.';
}
}
}
Page Extensions:
SQUEEZE 4 BC
pageextension 50100 "SQZ Document Ext." extends "DXP SQZ Document v2"
{
layout
{
addafter(BuyFromVendorInternal)
{
field("Customer No. Internal"; Rec."Customer No.")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Customer No. field.';
trigger OnAssistEdit()
begin
MarkField(Rec.FieldNo("Customer No."), Rec."Customer No.", Rec);
end;
trigger OnValidate()
begin
CheckPlausibility();
end;
}
}
addafter(BuyFromVendorExternal)
{
field("Customer No."; Rec."Customer No.")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Customer No. field.';
trigger OnAssistEdit()
var
ApiMgt: Codeunit "DXP SQZ API Mgt.";
ViewerResident: Codeunit "DXP SQUEEZE Viewer Resident";
begin
MarkField(Rec.FieldNo("Customer No."), Rec."Customer No.", Rec);
end;
trigger OnValidate()
begin
CheckPlausibility();
end;
}
}
}
local procedure MarkField(AppFldNo: Integer; FieldVal: Variant; DocHeader: Record "DXP SQZ Document Header")
var
ApiMgt: Codeunit "DXP SQZ API Mgt.";
ViewerResident: Codeunit "DXP SQUEEZE Viewer Resident";
begin
ViewerResident := GetViewerResident();
ApiMgt.MarkField(AppFldNo, FieldVal, ViewerResident, DocHeader);
end;
}
pageextension 50101 "SQZ Document Sf. Ext." extends "DXP SQZ Document Subform"
{
layout
{
addafter("No.")
{
field("Customer No."; Rec."Customer No.")
{
ApplicationArea = All;
ToolTip = 'Specifies the value of the Customer No. field.';
}
}
}
}
Target document (here: purchase document)
pageextension 50102 "Purchase Invoice Ext." extends "Purchase Invoice"
{
layout
{
addafter("Buy-from Vendor No.")
{
field("DXP Customer No."; Rec."DXP Customer No.")
{
ToolTip = 'Specifies the value of the DEXPRO Customer No. field.';
ApplicationArea = all;
}
}
}
}
pageextension 50103 "Purchase Inv. Sf Ext." extends "Purch. Invoice Subform"
{
layout
{
addafter("No.")
{
field("DXP Customer No."; Rec."DXP Customer No.")
{
ToolTip = 'Specifies the value of the DEXPRO Customer No. field.';
ApplicationArea = all;
}
}
}
}
Optional helper objects:
codeunit 50101 TokenMgt
{
procedure GetCustomerNoTok(): Text
begin
exit(CustomerNoTok);
end;
var
CustomerNoTok: Label 'customerNo', Locked = true;
}
Events when validating a SQUEEZE document (Validate action)
Target audience
This documentation is aimed at third-party developers who want to run their own logic when a user triggers the Validate action on the DXP SQZ Document v2 page (page 70954659) – for example additional custom checks before validation, setting further values on the SQUEEZE document, or follow-up actions on the target document that was created.
The events described here are available from SQUEEZE for Dynamics 365 BC version 2.50.
Context: what happens when you choose Validate?
The Validate action calls two routines of the document page in sequence:
1) CheckPlausibility()
Runs the plausibility check of the respective document class and updates the plausibility entries in the lower part of the page. This routine runs not only when you choose Validate, but after almost every field change. It is therefore not a suitable hook for "once, on validation".
To extend the check itself, use the already documented events OnBeforeDoPlausibilityChecks, OnAfterDoHeaderPlausibilityChecks, OnAfterDoLinePlausibilityChecks and OnAfterDoPlausibilityChecks.
2) FinishValidation()
Stops with an error message as long as plausibility entries are still open, handles the duplicate check where applicable, and then calls the ValidateSQUEEZEDocument method of the DXP SQZ Document Mgt. codeunit (70954632). That is where the processed JSON is created, the Core document (DXP Document) is processed further, the target document is created and the SQUEEZE document is completed.
Both events described here are raised in DXP SQZ Document Mgt.ValidateSQUEEZEDocument. They therefore apply uniformly to:
- the Validate action on DXP SQZ Document v2 (
70954659), - the Validate action on DXP SQZ Document (Generic) (
70954670) and the older document pages, - the automatic validation (for example from the processing queue).
There is deliberately no event directly on the action. If you want to distinguish between a manual click and automatic validation, evaluate the AutomaticValidation parameter.
Event matrix (overview)
| Area | Event | Timing | Control | Typical use cases |
|---|---|---|---|---|
DXP SQZ Document Mgt. |
OnBeforeValidateSQUEEZEDocument |
After the plausibility and duplicate checks, before the processed JSON is created and the target document is generated | var IsHandled: Boolean cancels the validation; var DocHeader allows changes to the document |
Custom additional checks with cancellation, filling fields, starting an approval workflow, querying external systems |
DXP SQZ Document Mgt. |
OnAfterValidateSQUEEZEDocument |
After the Core document has been processed and the target document has been created, before the validation dialog | Cancellation not possible | Post-processing the target document, writing custom log tables, sending notifications, informing third-party systems |
Distinction from OnAfterWriteProcessedJsonToBlob
For logic that runs after validation, a Core event already exists. It is described on the page Creating custom documents (custom processing):
[IntegrationEvent(false, false)]
local procedure OnAfterWriteProcessedJsonToBlob(var Document: Record "DXP Document"; var ProcessedJSONObj: JsonObject)
It is raised in the DXP Document Mgt. codeunit (Core) after the processed JSON has been written to the Core document – within the validation process, so it fires when Validate is chosen as well.
OnAfterWriteProcessedJsonToBlob (Core) |
OnAfterValidateSQUEEZEDocument (SQUEEZE) |
|
|---|---|---|
| Raised in | DXP Document Mgt. |
DXP SQZ Document Mgt. |
| Applies to | all Core documents – including those from other DEXPRO source apps. You have to filter by document class yourself. | SQUEEZE documents only |
| Context | DXP Document and the processed JSON |
additionally the complete DXP SQZ Document Header with all recognised values, order matching and splitting information |
| Manual vs. automatic | cannot be distinguished | can be distinguished via AutomaticValidation |
| Timing | directly after the JSON is written, still inside the Core processing | at the end of the validation, after the SQUEEZE document has been completed |
| JSON modifiable | yes (var ProcessedJSONObj) |
no |
Recommendation:
- You want to modify the processed JSON or hand a document over to custom processing →
OnAfterWriteProcessedJsonToBlob. - You want to evaluate SQUEEZE-specific values or react to manual validation only →
OnAfterValidateSQUEEZEDocument. - You want to intervene before the target document is created, or to cancel the validation →
OnBeforeValidateSQUEEZEDocument. There is no Core equivalent for this.
OnBeforeValidateSQUEEZEDocument
[IntegrationEvent(false, false)]
local procedure OnBeforeValidateSQUEEZEDocument(var DocHeader: Record "DXP SQZ Document Header"; AutomaticValidation: Boolean; var IsHandled: Boolean)
| Parameter | Description |
|---|---|
var DocHeader |
The SQUEEZE document to be validated. Changes to fields are written afterwards and flow into the target document. |
AutomaticValidation |
false when Validate is chosen, true for automatic validation. |
var IsHandled |
Set to true to cancel the validation. The SQUEEZE document stays open, unchanged. |
The event is raised after:
- the document has been re-read and has neither the Deleted nor the Rejected status,
- the plausibility check succeeded, in the case of automatic validation,
and before the processed JSON is created. At this point the target document does not exist yet.
OnAfterValidateSQUEEZEDocument
[IntegrationEvent(false, false)]
local procedure OnAfterValidateSQUEEZEDocument(DocHeader: Record "DXP SQZ Document Header"; CoreDocument: Record "DXP Document"; AutomaticValidation: Boolean)
| Parameter | Description |
|---|---|
DocHeader |
The validated SQUEEZE document. At this point its status is already Deleted – this is the regular final state of a validated document and not an error. |
CoreDocument |
The corresponding Core document (DXP Document), including Status and Linked-to Record Id of the target document that was created. |
AutomaticValidation |
false when Validate is chosen, true for automatic validation. |
Use the Core document to determine whether the target document was actually created:
CoreDocument.Status = CoreDocument.Status::Transferred– the target document was created andCoreDocument."Linked-to Record Id"points to it.CoreDocument.Status = CoreDocument.Status::"Custom Processing"– the document was handed over to custom processing; no standard target document exists.
Important: custom checks do not belong in this event
OnBeforeValidateSQUEEZEDocument is the wrong place for checks that are meant to prevent a validation. An Error raised here only appears as a dialog after the user chose Validate – so the user learns about the problem at the very end of their work. Use one of the two approaches below instead. Both make the problem appear as a plausibility entry and disable the Validate action.
Approach 1 (no development): plausibility rule group
In the document class setup, define rules under Plausibility Rule Group. Every active and currently valid rule whose conditions match describes a problem and automatically creates a plausibility entry. Rules that reference the document lines are evaluated per line, so the entry points to the line concerned.
For checks such as "Shortcut Dimension 1 Code must not be empty" this is the preferred approach – no code, no extension, maintainable by the user.
Approach 2 (with development): OnAfterDoPlausibilityChecks
This event is published by all document classes with var TempPlausibilityCheckEntry and is therefore the reliable route. You insert your entry into the temporary record directly:
codeunit 50100 "My SQZ Plausibility Checks"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.",
'OnAfterDoPlausibilityChecks', '', false, false)]
local procedure OnAfterChecks(DocHeader: Record "DXP SQZ Document Header"; var TempPlausibilityCheckEntry: Record "DXP Plausibility Check Entry" temporary)
var
CostCenterMissingLbl: Label 'Please enter a cost center.';
begin
if DocHeader."Shortcut Dimension 1 Code" <> '' then
exit;
AddEntry(TempPlausibilityCheckEntry, CostCenterMissingLbl, DocHeader.RecordId(), 0);
end;
local procedure AddEntry(var TempPlausibilityCheckEntry: Record "DXP Plausibility Check Entry" temporary; MessageText: Text; LinkedToRecId: RecordId; LookupPageId: Integer)
var
NewEntryNo: Integer;
begin
NewEntryNo := 1;
TempPlausibilityCheckEntry.Reset();
TempPlausibilityCheckEntry.SetCurrentKey("Entry No.");
if TempPlausibilityCheckEntry.FindLast() then
NewEntryNo := TempPlausibilityCheckEntry."Entry No." + 1;
TempPlausibilityCheckEntry.Init();
TempPlausibilityCheckEntry."Entry No." := NewEntryNo;
TempPlausibilityCheckEntry.Text := CopyStr(MessageText, 1, MaxStrLen(TempPlausibilityCheckEntry.Text));
TempPlausibilityCheckEntry."Linked to Record Id" := LinkedToRecId;
TempPlausibilityCheckEntry."Lookup Page Id" := LookupPageId;
if TempPlausibilityCheckEntry.Insert() then;
end;
}
The publisher object depends on the document class:
| Document class | Codeunit |
|---|---|
| Purchase invoice / credit memo | DXP SQZ P. Inv/Crdt Memo Impl. |
| Order confirmation | DXP SQZ P. Order Conf. Impl. |
| Generic document classes | DXP SQZ Document Mgt. |
Set Linked to Record Id and Lookup Page Id (for lines: Page::"DXP SQZ Document Subform") so that the plausibility entry becomes clickable and jumps to the record concerned.
Alternative: OnAfterDoHeaderPlausibilityChecks
PlausibilityCheck.AddPlausibilityCheckEntry(...) offers a more convenient API that saves you the manual insert into the temporary record:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ P. Inv/Crdt Memo Impl.",
'OnAfterDoHeaderPlausibilityChecks', '', false, false)]
local procedure OnAfterHeaderChecks(DocHeader: Record "DXP SQZ Document Header"; var PlausibilityCheck: Codeunit "DXP Plausiblity Check Mgt.")
var
CostCenterMissingLbl: Label 'Please enter a cost center.';
begin
if DocHeader."Shortcut Dimension 1 Code" = '' then
PlausibilityCheck.AddPlausibilityCheckEntry(CostCenterMissingLbl, 0);
end;
OnAfterDoLinePlausibilityChecks works the same way for line checks; the AddPlausibilityCheckEntry(Text; LinkedToRecId; LookupPageId) overload makes the entry clickable.
Caution for the order confirmation document class: in
DXP SQZ P. Order Conf. Impl., the eventsOnBeforeDoHeaderPlausibilityChecks,OnBeforeDoLinePlausibilityChecks,OnAfterDoHeaderPlausibilityChecksandOnAfterDoLinePlausibilityCheckswere originally published withoutvaron thePlausibilityCheckparameter. A subscriber receives a copy there, so entries it adds are lost. For this document class, use the variants with theV2suffix from version 2.50 onwards – for exampleOnAfterDoHeaderPlausibilityChecksV2. Their signature is identical to the events of the other document classes. The old events remain available but are marked as obsolete.
Example 1: adding values before the handover to the target document
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.",
'OnBeforeValidateSQUEEZEDocument', '', false, false)]
local procedure OnBeforeValidateSetPostingDescription(var DocHeader: Record "DXP SQZ Document Header"; AutomaticValidation: Boolean; var IsHandled: Boolean)
begin
if DocHeader."Posting Description" = '' then
DocHeader."Posting Description" := CopyStr(DocHeader."Document Reference", 1, MaxStrLen(DocHeader."Posting Description"));
// No Modify required – the document is written afterwards.
end;
Modifying the target document: please use the Core events
If you want to change the target document that was created, neither event on this page is the right place. Core provides events that hand you the target document directly – most of them even as a typed Purchase Header, so you can skip the detour via Linked-to Record Id and a RecordRef. These events are described on the page Events after the creation of a Core document (DXP Document):
| Target document | Codeunit | Event |
|---|---|---|
| Purchase invoice / credit memo | DXP P. Inv. CredMemo. Creation |
OnAfterProcessStandardDocument(JObject; DocRecRef) |
| Purchase document, general | DXP Document Transfer Mgt. |
OnAfterCreatePurchaseDoc(var JObject; PurchaseHeader) |
| Purchase header | DXP Document Transfer Mgt. |
OnAfterCreatePurchaseHeader(var JObject; PurchaseHeader) |
| Purchase order | DXP P. Order Creation |
OnAfterCreatePurchaseOrder(var JObject; var PurchaseHeader) |
| Order confirmation | DXP P. Order Confirmation |
OnAfterProcessStandardDocument(JObject; var PurchaseHeader) |
OnAfterValidateSQUEEZEDocument is only the better choice if you additionally need values from the DXP SQZ Document Header – for example recognised values, order matching or splitting information that is not contained in the JSON of the Core document.
Example 2: reacting to manual validation only
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ Document Mgt.",
'OnAfterValidateSQUEEZEDocument', '', false, false)]
local procedure OnAfterValidateNotifyUser(DocHeader: Record "DXP SQZ Document Header"; CoreDocument: Record "DXP Document"; AutomaticValidation: Boolean)
var
ValidatedMsg: Label 'Document %1 has been forwarded.', Comment = '%1 = SQUEEZE document no.';
begin
if AutomaticValidation then
exit;
if not GuiAllowed() then
exit;
Message(ValidatedMsg, DocHeader."No.");
end;
Notes and limitations
- No dialogs in automatic processing. Always check
AutomaticValidationandGuiAllowed()beforeMessage,ConfirmorPage.RunModal. Otherwise the processing queue runs into an error. OnBeforeValidateSQUEEZEDocumentruns after the plausibility check. Custom checks that are supposed to appear in the page's plausibility entries and to disable the Validate action belong inOnAfterDoHeaderPlausibilityChecksorOnAfterDoLinePlausibilityChecksinstead.IsHandled := truecancels completely. No target document is created, the Core document stays unchanged and the SQUEEZE document keeps its status. The user does not get a message – issue one yourself if required.- No event for deleted or rejected documents. If the document has the Deleted or Rejected status, validation ends beforehand and neither event is raised.
OnAfterValidateSQUEEZEDocumentruns after aCommit. AnErrorin a subscriber does not roll the validation back; it leaves a validated document plus an error message for the user.
Related pages
- Creating custom documents (custom processing)
- Events after the creation of a SQUEEZE document (CreateSource)
- Events after the creation of a Core document (DXP Document)
- Rule-based next process step (routing documents into custom processing)
Events before retrieving and creating SQUEEZE documents
Audience
This documentation is for third-party developers who want to control whether Business Central retrieves documents from SQUEEZE – typically to prevent a test system from retrieving them.
It focuses on the two integration events in codeunit DXP SQZ API Mgt. that are raised before documents are retrieved.
Both events are available from SQUEEZE version 2.54.0.0.
Background: why a test system should not retrieve documents
If a test system is connected to the same SQUEEZE instance as the production system – for example because it was created as a copy of production – it retrieves documents from there as well.
When a document is created, it is set to FINISHED in SQUEEZE. A document retrieved by a test system is no longer available to the production system.
If the test system should not communicate with SQUEEZE at all, no extension is needed, see Test system retrieves documents from Squeeze. The events are meant for cases where the test system should detect and prevent the retrieval by itself, or where only certain documents should be held back.
Context: how documents are retrieved
Retrieval runs in two steps, both in codeunit DXP SQZ API Mgt.:
DownloadDocumentDatafetches the ids of the documents waiting for export in SQUEEZE and adds them to the import queue. It also fetches documents with status Resynchronize again.CreateCoreAndSQUEEZEDocumentfetches the document for each open import queue entry, creates the Core document (DXP Document) and the SQUEEZE document, marks the entry as processed and sets the document toFINISHEDin SQUEEZE.
Both steps run in the job queue and with the Download Documents now action on the Squeeze Documents, Squeeze Import Queue Entries and Squeeze Queue pages. Each step is preceded by an event with IsHandled.
Event matrix (overview)
| Event | When | Control | Typical use cases |
|---|---|---|---|
OnBeforeDownloadDocumentData |
before the document ids are fetched from SQUEEZE | IsHandled |
prevent all retrieval in a test system |
OnBeforeCreateCoreAndSQUEEZEDocumentV2 |
before a single document is fetched and created | IsHandled |
do not create documents that are already queued; hold back certain document classes or documents |
Events in DXP SQZ API Mgt.
OnBeforeDownloadDocumentData
Raised at the very start of DownloadDocumentData, before any connection to SQUEEZE is made.
Signature:
[IntegrationEvent(false, false)]
local procedure OnBeforeDownloadDocumentData(var IsHandled: Boolean)
With IsHandled := true the whole step is skipped: no document ids are fetched, no import queue entries are written and no documents with status Resynchronize are fetched again.
OnBeforeCreateCoreAndSQUEEZEDocumentV2
Raised in CreateCoreAndSQUEEZEDocument for each document, before it is fetched from SQUEEZE.
Signature:
[IntegrationEvent(false, false)]
local procedure OnBeforeCreateCoreAndSQUEEZEDocumentV2(DocClass: Enum "DXP Document Class"; SQUEEZEDocId: Text[50]; var IsHandled: Boolean)
Parameters:
DocClass– document class of the document, e.g.DXP Invoice / Credit MemoorDXP Order ConfirmationSQUEEZEDocId– id of the document in SQUEEZEIsHandled– set totrueto skip the document
With IsHandled := true:
- neither a Core document nor a SQUEEZE document is created,
- the document in SQUEEZE stays unchanged – it is not set to
FINISHED, - the import queue entry stays unprocessed and is offered again on the next run.
To shield a test system completely, you need this event in addition to OnBeforeDownloadDocumentData: a copy of the production system also brings along the entries that were already in its import queue. Without this event they would be created in the test system.
Skipped entries are retried on every run and count towards the up to 10 entries a run processes. If you hold back only some documents, these entries can delay the processing of the others.
The older event OnBeforeCreateCoreAndSQUEEZEDocument is obsolete and no longer raised. Use OnBeforeCreateCoreAndSQUEEZEDocumentV2 instead.
Minimal example: prevent retrieval in a sandbox
The example treats every sandbox as a test system. Adapt object ids/names to your namespace and the check in IsTestSystem to your rules.
codeunit 50102 "My SQZ Test System Guard"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnBeforeDownloadDocumentData', '', false, false)]
local procedure SkipDownloadInTestSystem(var IsHandled: Boolean)
begin
if IsTestSystem() then
IsHandled := true;
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP SQZ API Mgt.", 'OnBeforeCreateCoreAndSQUEEZEDocumentV2', '', false, false)]
local procedure SkipCreationInTestSystem(DocClass: Enum "DXP Document Class"; SQUEEZEDocId: Text[50]; var IsHandled: Boolean)
begin
if IsTestSystem() then
IsHandled := true;
end;
local procedure IsTestSystem(): Boolean
var
EnvironmentInformation: Codeunit "Environment Information";
begin
// Example: every sandbox counts as a test system.
// On-premises or for other rules, use a check of your own,
// e.g. a flag in a setup table of your own.
exit(EnvironmentInformation.IsSandbox());
end;
}
With the Download Documents now action the user gets no feedback that retrieval was skipped. If you want that, show a message in the subscriber – only when GuiAllowed() is true, so the job queue is not affected.