Skip to main content

Extending Excellent Importer

We designed Excellent Importer to be extensible so that our customers could handle new scenarios or create additional import types. This section provides information on how to extend Excellent Importer, including the events that are available. Partners can use this information to extend Excellent Importer.

Important

To extend Excellent Importer, you need to have a Premium plan.

Need help or additional events?

If you need some guidance extending Excellent Importer or you want to request an additional event or helper to assist with your extension, please get in touch with us. Remember to tell us what outcome you want to achieve - you never know; we might want to add this feature to Excellent Importer and save you the trouble.

Automating Files Imports

The app now includes an API that allows you to automate the process of importing files using Excellent Importer. You can read more about this here.

Adding a new Import Type

Example: Adding a new Import Type (Basic Example)

This example uses the Standard Cost Worksheet import type as an example, which does not have any special processing.

  • Extend the enum "ImportType_EI_TSL" with your custom import type
enumextension 50000 "ImportType" extends ImportType_EI_TSL
{
value(50000; "Standard Cost Worksheet")
{
Caption = 'Standard Cost Worksheet';
}
}

  • Create a codeunit similar to the one below:
codeunit 50000 "StdCostWorksheetMgt"
{
internal procedure ImportStandardCostWorksheet();
var
ExcelImportManagement: Codeunit "Excel Import Management_EI_TSL";
begin
ExcelImportManagement.Import(Enum::ImportType_EI_TSL::"Standard Cost Worksheet", Database::"Standard Cost Worksheet", false);
end;
}
  • Create a page extension to add the Import action to your page and call your function
pageextension 50000 "StandardCostWrksheet" extends "Standard Cost Worksheet"
{
actions
{
addfirst("F&unctions")
{
action("ImportFromExcel")
{
ApplicationArea = All;
Caption = 'Import';
Ellipsis = true; Image = ImportExcel;
Promoted = true;
PromotedCategory = Process;
PromotedIsBig = true;
ToolTip = 'Allows you to import standard costs from Excel or text in a customisable layout.';

trigger OnAction()
var
StandardCostWrksheetImport: Codeunit StdCostWrksheetMgt;
begin
StandardCostWrksheetImport.ImportStandardCostWorksheet();
end;
}
}
}
}

Adding New Import Types or Changing Existing Imports

This section references events that are available to extend Excellent Importer. It is not a complete list of the available events but includes events that get used frequently. You can use these events when you have more complex processing that must get handled. You can use these events when you create new import types or if you want to change the way existing import types work.

Object - EventPurpose
table "Exl. Mapping Template_EI_TSL"Excel Mapping Template
OnGetCustomTableIDSpecifies the table associated with the import type.
OnInsertCustomTemplateToBufferyou can optionally subscribe to this event if you want to specify additional details about a custom import type
OnCheckIfFieldFromCustomEntityCanBeEditedUse this event if you want to protect certain fields from being updated by the import. Using this event will cause fields to be filtered out of lookups so they cannot be selected.
OnBeforeGetRelatedFieldsAndSettingsForCustomEntityUse this event to specify the available settings as well as any related tables that get updated by the import (e.g. Header/Line). Refer to the example below to see how to use this.
OnBeforeGetRelatedFieldsAndSettingsUse this event to add additional settings to existing templates or if you want to implement different logic to handle related fields and settings for a template. This event is called before the default settings are added.
OnAfterGetDefaultRelatedFieldsAndSettingsUse this event to add additional settings to existing templates, included adding additional related tables.
OnBeforeGetCommaSeparatedDimensionFieldIDTextUse this event associate global/shortcut dimension fields with the dimension code.
codeunit "SettingsHelper_EI_TSL"Settings Helper
HandleCustomSettingTypeThis event allows you to specify settings values for a custom setting type on an existing template. You can use the approach above to specify the allowed values.
OnCustomLookupSettingValueUse this event to specify the available setting values for the setting type in relation to a custom template. This is different to the HandleCustomSettingType event in that it only fires for a custom template. Here is an example on how to use this.
codeunit "Excel Import Management_EI_TSL"Excel Import Management
OnAfterGetExcelToFieldMappingDetailsThis event fires after the Excel-to-field mapping details are loaded for the template. Use this event to validate or modify the field mappings before import processing begins.
OnBeforeTryCheckSettingsThis event fires before the template setting combinations are validated. Use this event to skip the built-in settings check for a custom import type by setting IsHandled := true.
OnAfterTryCheckSettingsThis event fires after the template setting combinations are validated. The CalledDuringImport parameter indicates whether the check was triggered during an import. Use this event to perform additional validation or setup after the standard settings checks complete.
OnGetOpenAsTemporarySettingThis event fires when Excellent Importer determines whether to import data into a temporary record. Use this event to control whether a custom import type uses temporary records for staging — for example, the Physical Inventory Journal import uses this to load data into temporary records before post-processing.
OnBeforeMustValidateFileBeforeImportThis event fires when Excellent Importer determines whether to validate the file before processing it. Set IsHandled := true and populate EnableValidateFileBeforeImport to override the default validation behaviour for your custom import type.
OnAfterMustValidateFileBeforeImportForCustomTemplateThis event fires after the validation check for custom templates. Use this event to enable or disable file validation for a custom template without fully overriding the check.

The example code comes from the sales and purchase document import (it has been changed slightly as build in imports do not use these events).

    [EventSubscriber(ObjectType::Table, Database::"Exl. Mapping Template_EI_TSL", OnBeforeGetRelatedFieldsAndSettingsForCustomEntity, '', false, false)]
local procedure OnBeforeGetRelatedFieldsAndSettingsForCustomEntity(var ExcelImportManagement: Codeunit "Excel Import Management_EI_TSL"; var RelatedTableList: List of [Integer]; var RelatedFieldFilterList: List of [Text]; ImportType: Enum ImportType_EI_TSL)
begin
AddRelatedFieldsAndSettings(ExcelImportManagement, ImportType, RelatedTableList, RelatedFieldFilterList);
end;

/// <summary>
/// Adds the related tables and settings that are applicable for the import
/// </summary>
internal procedure AddRelatedFieldsAndSettings(var ExcelImportManagement: Codeunit "Excel Import Management_EI_TSL"; ImportType: Enum ImportType_EI_TSL; var RelatedTableList: List of [Integer]; var RelatedFieldFilterList: List of [Text]; var RelatedReferenceNo: Code[50]);
var
ItemTrackingMgt: Codeunit ItemTrackingMgt_EI_TSL;
NegativeDocumentTotalsTxt: Label 'Specify how to handle negative document totals - this does not apply to quotes and blanket orders', MaxLength = 100;
ReleaseDocsTxt: Label 'Release the documents or send them for approval after importing.', MaxLength = 100;
Prop_DocNoRuleTextLbl: Label 'Use the imported Document No. (if present) as the Posted Document No.', MaxLength = 100;
Prop_ReplaceDocNoTextLbl: Label 'Replace with the imported Document No. from the Number Series.', MaxLength = 100;
begin
// Add the setting types available for this import and provide some basic info including the default value and what the setting does
ExcelImportManagement.AddSettingToPropertyBuffer(SettingType::"Document No. as Posting No.", SettingValue::No, Prop_DocNoRuleTextLbl);
ExcelImportManagement.AddSettingToPropertyBuffer(SettingType::"Replace Document No.", SettingValue::No, Prop_ReplaceDocNoTextLbl);
ExcelImportManagement.AddSettingToPropertyBuffer(SettingType::"Negative Document Totals", SettingValue::"Do not Validate", NegativeDocumentTotalsTxt);
ExcelImportManagement.AddSettingToPropertyBuffer(SettingType::"Release Documents", SettingValue::No, ReleaseDocsTxt);

// tables
if ImportType = ImportType::Sales then
RelatedTableList.Add(Database::"Sales Header")
else
RelatedTableList.Add(Database::"Purchase Header");
RelatedFieldFilterList.Add(''); // you can optionally include a filter string if you only want to allow certain fields.
ItemTrackingMgt.AddTrackingSpecFields(ImportType, ExcelImportManagement, false, RelatedTableList, RelatedFieldFilterList);
end;

Example - Specifying Allowed Settings Values for a Setting Type on an existing template

This example shows you how to specify allowed values for a custom setting type on an existing template (e.g. Sales Document).

    [EventSubscriber(ObjectType::Codeunit, Codeunit::SettingsHelper_EI_TSL, OnHandleCustomSettingType, '', false, false)]
local procedure OnHandleCustomSettingType(ImportType: Enum ImportType_EI_TSL; SettingType: Enum ImportSettingType_EI_TSL; var SettingsText: Text; var IsHandled: Boolean)
begin
SettingsText := GetSettingValuesAllowedForType(ImportType, SettingType);
IsHandled := true;
end;

/// <summary>
/// Creates a comma separated list of allowed setting values for the setting type.
/// </summary>
internal procedure GetSettingValuesAllowedForType(ImportType: Enum ImportType_EI_TSL; SettingType: Enum ImportSettingType_EI_TSL) SettingsText: Text
begin
case SettingType of
SettingType::"Post Documents Automatically":
begin
SettingsHelper.AddToSettingsText(SettingsText, SettingValue::No);
SettingsHelper.AddToSettingsText(SettingsText, SettingValue::Yes);
end;
end;
end;

Example - Specifying Allowed Settings Values for a Setting Type on a custom template

This example shows you how to specify allowed values for a custom setting type on a custom template you have created.

    [EventSubscriber(ObjectType::Codeunit, Codeunit::SettingsHelper_EI_TSL, OnCustomLookupSettingValue, '', false, false)]
local procedure OnCustomLookupSettingValue(ImportType: Enum ImportType_EI_TSL; SettingType: Enum ImportSettingType_EI_TSL; var SettingsText: Text)
begin
SettingsText := GetSettingValuesAllowedForType(ImportType, SettingType);
end;

/// <summary>
/// Creates a comma separated list of allowed setting values for the setting type.
/// </summary>
internal procedure GetSettingValuesAllowedForType(ImportType: Enum ImportType_EI_TSL; SettingType: Enum ImportSettingType_EI_TSL) SettingsText: Text
begin
case SettingType of
SettingType::"Replace Document No.", SettingType::"Document No. as Posting No.",
SettingType::"Release Documents":
begin
SettingsHelper.AddToSettingsText(SettingsText, SettingValue::No);
SettingsHelper.AddToSettingsText(SettingsText, SettingValue::Yes);
end;
SettingType::"Negative Document Totals":
begin
SettingsHelper.AddToSettingsText(SettingsText, SettingValue::"Do not Validate");
SettingsHelper.AddToSettingsText(SettingsText, SettingValue::"Skip during Import");
SettingsHelper.AddToSettingsText(SettingsText, SettingValue::"Cancel Import");
SettingsHelper.AddToSettingsText(SettingsText, SettingValue::"Order/Invoice to Credit");
SettingsHelper.AddToSettingsText(SettingsText, SettingValue::"Order/Invoice to Return/Credit");
end;
else
SettingsHelper.SettingTypeNotValidForImportTypeError(ImportType, SettingType);
end;
end;

Example - Adding a text setting

It is possible to add a setting that is not an enum but a text value. Below is the example from the General Journal import. The Amount by Dimension setting will hold a dimension value.

    internal procedure AddGeneralJournalRelatedFieldsAndSettings(var ExcelImportManagement: Codeunit "Excel Import Management_EI_TSL");
var
Prop_AmtByDimDescLbl: Label 'Specify this if there are multiple amount columns, each representing a different dimension.', MaxLength = 100;
Prop_ExcludeZeroDescLbl: Label 'Skip lines where the amount is zero', MaxLength = 100;
begin
ExcelImportManagement.AddSettingToPropertyBuffer(SettingType::"Exclude Zero Amounts", SettingValue::No, Prop_ExcludeZeroDescLbl);
ExcelImportManagement.AddSettingToPropertyBuffer(SettingType::"Amount by Dimension", SettingValue::"Text Value in Constant", Prop_AmtByDimDescLbl);
end;
  • You can subscribe to OnBeforeValidateSettingValuesForSettingType to validate input while creating a template.
    internal procedure ValidateDimensionForAmountByDimension(SettingTypeParam: Enum ImportSettingType_EI_TSL; SelectedValue: Text)
var
Dimension: Record Dimension;
begin
if (SelectedValue = '') or (SettingTypeParam <> SettingTypeParam::"Amount by Dimension") then
exit;
Dimension.Get(SelectedValue);
Dimension.TestField(Blocked, false);
end;
  • You can access the value of the setting at runtime using the GetConstantTextSettingFromStorage procedure.
        AmountDimensionCode := CopyStr(SettingsHelper.GetConstantTextSettingFromStorage(SettingType::"Amount by Dimension", '', false, false), 1, MaxStrLen(AmountDimensionCode));
if AmountDimensionCode = '' then
exit;

Example - Handling Global/Shortcut Dimensions

You use this event so that the Excellent Importer will update the Global/Shortcut Dimension fields as expected.

    [EventSubscriber(ObjectType::Table, Database::"Exl. Mapping Template_EI_TSL", 'OnBeforeGetCommaSeparatedDimensionFieldIDText', '', false, false)]
local procedure OnBeforeGetCommaSeparatedDimensionFieldIDText(ImportType: Enum ImportType_EI_TSL; var CommaSeparatedFieldIDText: Text; var IsHandled: Boolean);
var
ExlAccDimMapLine: Record ExlAccDimMapLine_EI_TSL;
begin
if ImportType <> ImportType::"Account/Dimension Mapping" then
exit;
CommaSeparatedFieldIDText := Format(ExlAccDimMapLine.FieldNo("Shortcut Dimension 1 Code")) + ',' + Format(ExlAccDimMapLine.FieldNo("Shortcut Dimension 2 Code"));
IsHandled := true;
end;

How Excellent Importer Processes a File

Excellent Importer processes the file in up to three stages, controlled by the ImportStage_EI_TSL enum. Which stages run depends on the template configuration:

  1. Data Validation — the file is read and all field values are validated without inserting or modifying any records. Errors are collected and presented to the user so they can fix issues in the source file before committing data. This stage is skipped when the Validate Data before Import setting is not enabled on the template.
  2. Staging — the file is read again to populate staging records. Some import types use this stage to inspect the full contents of the file before making decisions that affect how data is applied. For example, the sales and purchase document import uses this stage to calculate document totals and determine whether a document should be flipped to a credit memo when the total is negative — a decision that can only be made after all lines for a document have been read. Similarly, the bank statement import reads all rows during staging to determine whether to create a new bank account statement or add lines to an existing one. This stage is skipped when the template does not perform any staging.
  3. Apply — the file is read and the data is applied to the database. Records are inserted or modified, dimensions are set, and post-import processing runs. This stage always runs.

Understanding these stages is important when subscribing to events, because most events fire once per stage. If your subscriber has side effects (e.g. inserting records or calling external services), you should check the ImportStage parameter to ensure your logic only runs during the correct stage.

note

The events are shown in the order in which they fire. Most events fire during each import stage (see How Excellent Importer Processes a File above).

OnBeforeReadImportData

This event fires at the very start of the import, before any data is read from the file. Use this event to perform initialisation or setup logic that must happen before the import begins — for example, initialising external service connections or preparing staging tables.

OnBeforeReadStagingData

This event fires at the very start of reading staged data, before Excellent Importer attempts to retrieve the import reference and delete flag from the Data Exchange framework. It applies only when the mapping template is configured to use the Staged Data file type (i.e. Process to Excellent Importer (Staged Data)). For background on how staged data processing works, see Generic Data Exchange Processing.

You can use this event to supply the import reference and delete flag yourself — for example, when you are driving the import programmatically from an XMLport and want to bypass the Data Exchange reference lookup entirely. Set IsHandled := true to prevent Excellent Importer from calling the Data Exchange framework.

The FileInStream parameter contains the InStream for the file the user selected to import. You can populate this parameter so that downstream subscribers or processing logic have access to the raw file content if needed.

In the example below, the subscriber generates a unique import reference, passes FileInStream directly into an XMLport so that it can parse the file and write rows into StagingData_EI_TSL, then sets IsHandled := true so that Excellent Importer skips the Data Exchange reference lookup and uses the reference the subscriber already populated.

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Excel Import Management_EI_TSL", OnBeforeReadStagingData, '', false, false)]
local procedure OnBeforeReadStagingData(var ImportReference: Code[50]; var DeleteStagingDataAfterImport: Boolean; var FileInStream: InStream; var IsHandled: Boolean)
var
MyXmlPort: XmlPort "My Staging Data Import_EI_TSL";
begin
// Generate a unique reference so the staged rows can be identified later.
ImportReference := CopyStr(Format(CreateGuid()), 1, MaxStrLen(ImportReference));
DeleteStagingDataAfterImport := true;

// Pass the file stream (the file the user selected) into the XMLport.
// The XMLport is responsible for reading the file and inserting rows into
// StagingData_EI_TSL using the same ImportReference assigned above.
MyXmlPort.SetSource(FileInStream);
MyXmlPort.SetImportReference(ImportReference);
MyXmlPort.Import();

// Tell Excellent Importer that we have already resolved the reference and
// delete flag — it should not attempt to retrieve them from the Data Exchange
// framework.
IsHandled := true;
end;
info

If IsHandled is set to true but ImportReference is left blank, the import will still fail with a validation error. You must always supply a non-empty ImportReference when handling this event.

The XMLport in this pattern is responsible for:

  1. Reading the raw file from FileInStream.
  2. Mapping each row into a StagingData_EI_TSL record tagged with the same ImportReference.
  3. Inserting those records so that Excellent Importer can read them in the next step.

Once the event completes, Excellent Importer reads the staged rows using ImportReference and processes them through the normal import pipeline.

OnBeforeImportExcelData

Use this event to perform custom logic after the Excel File is loaded but before the processing starts

OnBeforeCheckIfConcatenationIsRequired

This event allows you to skip concatenation in certain scenarios. For example, the General Journal Dimensioned Amount feature, when enabled on a template, disables concatenation for the Amount field.

OnAfterCopyExcelBufferToExlImpBuffer

This event is raised after the data from the current row is transferred into a temporary record, before any processing is done and before the app checks criteria for skipping the row. You can use this event if you want to perform special processing using a footer record. The event includes all buffers so you need to use the record type to determine which before to use. The LastImportedRecordRef will contain the values imported from a previous row as it has not been initialised as this stage.

OnBeforeCheckImportedValuesBeforeUpdate

Use this event if you want to do your validation before importing data. You will use this if you want to suppress some of the standard validation added by Excellent Importer and add your own.

OnAfterCheckImportedValuesBeforeUpdate

Use this event to add additional validation before importing data.

OnBeforeExcludeExcelBufferRowFromImport

Use this event if you want to exclude an Excel row from the import. At this point Excellent Importer has not transferred any values to fields

OnBeforeFindExistingRecord

This event fires before Excellent Importer checks if there is an existing record. You can use this event if you need to add values to the imported data to create/update a record. For example, we use this event in our price list import to find the item number using an item reference. The StagingRecordRef parameter contains values that are mapped to fields.

OnBeforeSkipImportOfRecord

Use this event if you want to skip the import based on values on the line. The StagingRecordRef parameter contains values that are mapped to fields. This is before Excel Import has tried to find an existing record.

OnFindExistingRecord

This event is fired if Excellent Importer finds an existing record with the Record ID as the record being imported. Use this event to:

  1. Allow updates to existing records
  2. Transfer existing values from the existing record so that you only update fields that are actually being imported.
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Excel Import Management_EI_TSL", OnFindExistingRecord, '', false, false)]
local procedure ExcelImportManagement_EI_TSL_OnFindExistingRecord(var ImportedRecRef: RecordRef; var ExistingRecRef: RecordRef; var ExistingValuesTransfered: Boolean; ImportTypeAsEnum: Enum ImportType_EI_TSL);
var
SettingsHelper: Codeunit SettingsHelper_EI_TSL;
SettingValue: Enum ImportSettingValue_EI_TSL;
SettingType: Enum ImportSettingType_EI_TSL;
FieldRef: FieldRef;
NotSetupForUpdateErr: Label '%1 %2 already exists. You cannot update existing records with this template because %3 is set to %4.', Comment = '%1 tablecaption, %2 is the vendor number, %3 is the setting, %4 is the value';
begin
SettingValue := SettingsHelper.GetSettingsValueFromStorage(SettingType::"Update Method");
if not (SettingValue in [SettingValue::"Update/Add", SettingValue::Update]) then // check if this import is configured to allow updates to existing records
Error(NotSetupForUpdateErr, ExistingRecRef.Caption(), FieldRef.Value(), SettingType::"Update Method", SettingValue);

ImportedRecRef := ExistingRecRef;
ExistingValuesTransfered := true;
end;
Important
  • You must subscribe to this event to allow an import to update existing records, as this is where you can transfer existing values to the imported record reference.
  • The app has not transferred the values from the file to the imported record reference. Therefore, the generic approach above is usually all you need to do, as the app will only update fields mapped in the import.

OnInsertImportedData

This event fires before a record is inserted. At this stage no values will be transferred to the NewRecordRef unless the record exists already. You can use StagingRecordRef to get the mapped field values. You can also insert the record if it needs to exist before updating other field values.

OnBeforeCreateLinkToDestinationRecord

This event is only fired for templates configured to add links to the imported file. The import template defines when the link is created. For example, the sales and purchase documents will create the link when the header record is saved, whereas the general journal import will create the link for every imported general journal line.

OnBeforeApplyImportedData

This event fires before the import values are copied to the record.

OnBeforeCopyImportedValuesToRecordRef

This event fires before Excellent Importer starts copying the imported values to the record reference. Set IsHandled := true to completely replace the default field-copying logic with your own implementation. For example, the generic import type uses this event to implement type-agnostic field copying.

CopyImportedValuesToRecordRefOnBeforeHandleField

This event fires for each field before it is processed during the copy from imported values to the record reference. Set IsHandled := true to skip the default field handling for that field and apply your own logic. For example, the price list import uses this event to apply custom field value transformations before field assignment.

OnBeforeCheckIfProtectedFieldSelected

This event fires before Excellent Importer checks whether a field is protected from being updated during import. Set IsHandled := true to bypass the built-in protected field check for a specific field.

OnCheckCustomIfProtectedFieldSelected

This event fires when Excellent Importer checks whether a field is protected for a custom import type. Set ProtectedFieldSeleted := true to prevent the field from being updated by the import.

OnAfterAssignFieldValue

This event gets fired after a value gets assigned to a field. You can use this event to perform special processing after updating a specific field during import.

OnBeforeCheckOrAddMissingDimensionValue

This event fires before Excellent Importer checks whether a dimension value exists and potentially creates it. Set IsHandled := true to bypass the default dimension value creation logic — for example, when you want to handle dimension value creation in a custom way or skip it entirely for certain import types.

OnBeforeInsertDimensionValues

This event fires before a missing dimension value is inserted into the Dimension Value table. Set IsHandled := true to prevent the automatic creation of the dimension value — for example, when you want to validate dimension values against an external system before allowing them to be created.

OnAfterCopyImportedValuesToRecordRef

This event fires after all imported field values have been copied to the record reference, including dimension handling. Use this event to perform additional transformations or calculations that depend on multiple field values being present on the record. For example, the bank statement import uses this event to apply bank statement-specific post-processing after all field values are transferred.

CheckRecordCanBeInserted

This event fires before a new record is inserted, allowing you to prevent the insertion based on custom business rules. Set CandoInsert := false to skip the insertion. For example, the general journal import uses this event to prevent inserting journal lines that fail custom validation.

OnBeforeModifyOrInsertImportedData

This event fires before the record is insert or modified. The dimension set is also not applied that this point. You can use this event to handle the insert or modify yourself or do some processing immediately before the insert/modify.

OnAfterApplyandInsertorModifyRecord

This event fires after the dimension set is applied and the record has been insert/modified. You can use this event to handle processing that needs to happen after the record is updated.

OnAfterImportRecord

This event fires after each record has been fully imported (inserted or modified), at the end of the row-processing loop. Use this event to perform per-row post-processing or validation that needs to happen after the record is saved — for example, verifying that the imported record matches expected values.

OnPostImportData

This event fires after all records have been imported, but before the transaction has been committed. Use this event to handle post import processing that is directly connected to the import (e.g. releasing orders).

OnAfterCheckImportedDataAfterImport

This event fires after Excellent Importer has completed its post-import data checks. Use this event to perform additional aggregate-level validation or data integrity checks after all rows have been imported.

OnFinalizeImportBeforeCommitAndClearTemporaryStorage

This event fires after all post-import processing is complete but before the transaction is committed and temporary storage is cleared. Use this event to perform final processing that must happen within the same transaction — for example, posting journal entries, refreshing production orders, or finalising sales/purchase document processing. You can also use the FeedbackMessage parameter to set a custom completion message.

OnFinalizeImportAfterCommitAndClearTemporaryStorage

This event fires after the transaction has been committed and temporary storage has been cleared. Use this event for post-commit actions such as sending notifications or performing cleanup that does not need to be part of the import transaction. Set IsHandled := true to suppress the default feedback message.

OnBeforeShowFeedbackMessageAfterSuccessfulImport

This event fires at the very end of a successful import, before the feedback message is displayed to the user. Set IsHandled := true to suppress the default message — for example, the customer remittance import uses this event to display a custom completion message instead of the standard one. You can also modify the FeedbankMessageText parameter to change the message text.

Sales and Purchase Document Events

EventPurpose
Sales Documents
OnBeforeInsertSalesHeaderThis event allows you to directly update any fields that need to be updated before the sales header is inserted.
OnAfterInsertNewSalesHeaderBeforeModifyAllows you to implement custom logic directly after all fields are transferred to a sales header before it gets modified.
OnAfterGetOrCreateSalesHeader_BeforeInsertLineAllows you to implement custom logic after the sales header is created before the line is inserted.
OnAfterInsertSalesLineBeforeUpdateFieldsAllows you to implement custom logic after the sales line is inserted with the initial values but before the line gets updated with the rest of the imported values.
Purchase Documents
OnBeforeInsertPurchaseHeaderThis event allows you to directly update any fields that need to be updated before the purchase header is inserted.
OnAfterInsertNewPurchaseHeaderBeforeModifyAllows you to implement custom logic directly after all fields are transferred to a purchase header before being modified.
OnAfterGetOrCreatePurchaseHeader_BeforeInsertLineAllows you to implement custom logic after the purchase header is created but before the line gets inserted.
OnAfterInsertPurchaseLineBeforeUpdateFieldsAllows you to implement custom logic after the purchase line is inserted with the initial values but before the line gets updated with the rest of the imported values.
CommonThese can be used for purchases and sales documents.
OnBeforeAssignDocumentNoUse this event to change how the document number gets assigned. If you assign a value to the NewDocNo parameter, the app will get that sales header.
OnAfterFieldCannotBeEditedThe app has built-in rules to prevent certain fields from being updated directly. Use this event to restrict more fields. Fields that are restricted do not show in lookups
OnBeforeFieldCannotBeEditedSimilar to the previous event, however, this event allows you to override the built-in rules
OnAfterImportSalesOrPurchaseLineAllows you to implement custom logic directly after all fields are transferred to a sales/purchase line and the line has been inserted.
OnBeforeCheckDocumentTotalThis event relates to the handling of negative document totals during the import of sales and purchase documents. It triggers before the document total is validated. Use this event to prevent changes to the document type or to bypass the validation process when the document total is negative.
OnPostImportSalesPurchaseDocsBeforeDataReleaseThis event fires after all the documents have been imported before the documents get released. This event allows you to process the imported documents before they get released or sent for approval. This event will fire even if the template is not configured to release the documents. The TempRecordRef parameter contains a record for every sales or purchase header created during the import.

Physical Inventory Journal Events

EventPurpose
OnClearPhysicalQuantityOnJournalBeforeUpdatePhysQtyYou can use this event to handle specific logic required when resetting the physical quantity on the Physical Inventory Journal before importing the recording.

Item Tracking Events

EventPurpose
OnBeforeCreateReservationEntryForItemTrackingFires before a reservation entry is created for item tracking. At this point all imported fields have already been copied to TempTrackingSpecification. Use this event to modify the tracking specification or configure the CreateReservEntry codeunit before the reservation entry is inserted.
OnAfterCreateReservationEntryForItemTrackingFires directly after the reservation entry is inserted. Use this event to find and update the reservation entry — for example, to populate custom fields. Use TempTrackingSpecification to filter for the entry that was just created and call Modify() to save changes.

Example - Copying a Custom Field to the Reservation Entry

Because Excellent Importer already copies all imported field values to the Tracking Specification before creating the reservation entry, the recommended approach is to subscribe to standard Business Central events that fire as part of that creation flow. This avoids querying the database yourself and keeps the logic consistent with the standard BC item tracking pipeline.

Three standard events to consider are:

  • OnAfterCopyTrackingFromTrackingSpec on table Reservation Entry — fires immediately after tracking fields (lot number, serial number, package number) are copied from the tracking specification to the reservation entry via CopyTrackingFromSpec. Use this event to read or modify the reservation entry at the point tracking values are assigned.
  • OnAfterMoveFields on codeunit Create Reserv. Entry — fires after fields are moved from the tracking specification into the reservation entry. The Reservation Entry record is passed directly, so you can copy your custom fields at the same time the standard fields are transferred.
  • OnSyncActivItemTrkgOnBeforeTempTrackingSpecModify on codeunit Item Tracking Management — fires before a temporary tracking specification record is modified during synchronisation. This is useful when you need custom values to flow through to active tracking entries.

The example below shows OnAfterMoveFields being used to copy "My Custom Field" from the Tracking Specification to the Reservation Entry:

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Create Reserv. Entry", OnAfterMoveFields, '', false, false)]
local procedure CreateReservEntry_OnAfterMoveFields(var TempTrackingSpecification: Record "Tracking Specification"; var ReservationEntry: Record "Reservation Entry")
begin
ReservationEntry."My Custom Field" := TempTrackingSpecification."My Custom Field";
end;
tip

This is the preferred approach. Because the import has already populated TempTrackingSpecification with the mapped field values, the standard BC event receives both the specification and the entry as parameters — no additional filtering or database lookups are needed.

If for any reason the standard BC events do not fire in your scenario (for example, due to a custom creation flow in an ISV extension), you can fall back to the OnAfterCreateReservationEntryForItemTracking event provided by Excellent Importer. In this case the subscriber must locate the reservation entry itself using the source and tracking fields from TempTrackingSpecification:

[EventSubscriber(ObjectType::Codeunit, Codeunit::ItemTrackingMgt_EI_TSL, OnAfterCreateReservationEntryForItemTracking, '', false, false)]
local procedure ItemTrackingMgt_OnAfterCreateReservationEntryForItemTracking(var TempTrackingSpecification: Record "Tracking Specification"; var CreateReservEntry: Codeunit "Create Reserv. Entry")
var
PurchaseLine: Record "Purchase Line";
ReservationEntry: Record "Reservation Entry";
begin
if TempTrackingSpecification."Source Type" <> Database::"Purchase Line" then
exit;

PurchaseLine.Get(TempTrackingSpecification."Source Subtype", TempTrackingSpecification."Source ID", TempTrackingSpecification."Source Ref. No.");
PurchaseLine.SetReservationFilters(ReservationEntry);
ReservationEntry.SetRange("Lot No.", TempTrackingSpecification."Lot No.");
ReservationEntry.SetRange("Serial No.", TempTrackingSpecification."Serial No.");
ReservationEntry.SetRange("Reservation Status", ReservationEntry."Reservation Status"::Prospect);
if ReservationEntry.FindLast() then begin
ReservationEntry."My Custom Field" := TempTrackingSpecification."My Custom Field";
ReservationEntry.Modify();
end;
end;
info

The TempTrackingSpecification is a temporary record. Changes made to it inside this event do not persist — always modify and save the ReservationEntry directly.

Helper Functions

FunctionPurpose
Excel Import Management
ProcessRelatedTableFieldsProcesses fields from a related table. The InputSeqFrom and InputSeqTo parameters allow you to specify a range of fields to process. If you specify zero for both, it does not apply a filter. If you specify a value for only one it is either a minimum or maximum. Refer to the example below.

Example - ProcessRelatedTableFields

In this example, we show you how you can use the ProcessRelatedTableFields function to process fields from the purchase header table, which is a related table on the purchase document import.

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales/Purch. Doc. Imp._EI_TSL", OnBeforeInsertPurchaseHeader, '', false, false)]
local procedure "OnBeforeInsertPurchaseHeader"(var PurchaseHeader: Record "Purchase Header"; var ExlImpFieldBuffer: Record "Exl. Imp Field Buffer_EI_TSL")
begin
UpdatePurchaseHeaderFieldsBeforeInsert(PurchaseHeader, ExlImpFieldBuffer);
end;

local procedure UpdatePurchaseHeaderFieldsBeforeInsert(var PurchaseHeader: Record "Purchase Header"; var ExlImpFieldBuffer: Record "Exl. Imp Field Buffer_EI_TSL")
var
RecordRef: RecordRef;
ExcelImportManagement: Codeunit "Excel Import Management_EI_TSL";
FromProcressingOrder: Integer;
ToProcressingOrder: Integer;
begin
// logic to determine which fields to apply before insert
//FromProcressingOrder :=
//ToProcressingOrder :=

RecordRef.GetTable(PurchaseHeader);
ExcelImportManagement.ProcessRelatedTableFields(RecordRef, ExlImpFieldBuffer, FromProcressingOrder, ToProcressingOrder);
RecordRef.SetTable(PurchaseHeader);
end;

Adding a New Import Type

Adding a new import type is very straightforward.

  • Extend the "ImportType_EI_TSL" Enum to add your Import Type
  • Subscribe to the OnGetCustomTableID

Custom Values

Custom values get used to map values required for processing when these values do not relate to a specific field.

ObjectEventDescription
Table CustomValue_EI_TSLOnAfterRefreshCustomValueActions Use this event to publish a new custom action. When you subscribe to this you can use the AddActionToNameValueBuffer function described below
Table CustomValue_EI_TSLOnAfterCheckValueTypeSelectedForProcessingAction
Table CustomValue_EI_TSLOnAfterGetDefaultValueType Allows you to specify the default value type for your custom processing action.
Table CustomValue_EI_TSLOnValidateMappingLineCustomValue This event fires when a custom value is selected on an Excel Mapping Template line. Use this event to validate whether the selected custom value is allowed to be used on a mapping line for the given import type. For example, built-in General Journal imports use this to prevent batch-balancing custom values from being selected on individual lines (they must be configured as a header setting instead).
Table CustomValue_EI_TSLOnBeforeCopyAccountDimensionValuesToSalesLine You can use this event to change how the information gets copied from the account dimension mapping. You can only make changes to the NewSalesLine parameter.
Table CustomValue_EI_TSLOnAfterGetDefaultRoundingPrecision Allows you to specify the rounding precision on the custom value. Only use this if the default rounding precision will only work for you.
Table CustomValue_EI_TSLOnBeforeCreateDefaultCustomValues You can use this event if you want to create default custom values.
Table CustomValue_EI_TSLOnAfterGetDefaultRoundingPrecision You can use this event if you want to update a default custom value before it gets inserted.
Codeunit "Sales/Purch. Doc. Imp._EI_TSL"OnBeforeAddNewSalesUsingValueAsAmount Allows you to add custom logic to the AddNewSalesPurchaseLineUsingValueAsAmount processing action for sales lines before the processing action gets executed.
Codeunit "Sales/Purch. Doc. Imp._EI_TSL"OnBeforeAddNewPurchaseLineUsingValueAsAmountAs above, but applies to purchase documents
Codeunit "Sales/Purch. Doc. Imp._EI_TSL"OnAfterAddNewSalesUsingValueAsAmountBeforeInsertNewSalesLineAllows you to add custom logic to the AddNewSalesPurchaseLineUsingValueAsAmount processing action for sales lines after the processing action gets executed.
Codeunit "Sales/Purch. Doc. Imp._EI_TSL"OnAfterAddNewPurchaseLineUsingValueAsAmountBeforeInsertNewPurchaseLineAs above, but applies to purchase documents.
Codeunit "Sales/Purch. Doc. Imp._EI_TSL"OnCustomSalesPurchaseValueProcessActionAllows you to implement your own custom processing action.
Codeunit "Sales/Purch. Doc. Imp._EI_TSL"OnAfterSales_InitQuantitiesOnCopyLineForCustomValuethis event fires when the new sales line is copied from the existing purchase line.
Codeunit "Sales/Purch. Doc. Imp._EI_TSL"OnAfterPurch_InitQuantitiesOnCopyLineForCustomValuethis event fires when the new purchase line is copied from the existing purchase line.

Helpers for Custom Values

ObjectFunction
Table CustomValue_EI_TSLAddActionToNameValueBuffer
Table CustomValue_EI_TSLGetActionText Used to retrieve the caption or extended description for the given action. You can use this if you want to display a message to the user and provide a user friendly reference to the action.
Table CustomValue_EI_TSLCopyAccountDimensionValuesToSalesLine Use this function if you want your custom processing action to copy values from the account dimension mapping

Example: Adding a new Processing Action

Publishing the Processing Action

The code snippet below shows you how to add a new processing action that you can select in the custom values list. The example adds a processing action that you can use on a sales import template.

var
ProcessingActionIDLbl: Label 'ProcessingActionCode_TSL', Locked = true, MaxLength = 250;
ProcessingActionCaptionTxt: Label 'Add a new sales line or purchase line if the value is not zero', MaxLength = 250;
ExtendedDescriptionTxt: Label 'Specify details about what this action does and how to use it.', MaxLength = 2048;

[EventSubscriber(ObjectType::Table, Database::CustomValue_EI_TSL, 'OnAfterRefreshCustomValueActions', '', false, false)]
local procedure OnAfterRefreshCustomValueActions(ImportType: Enum ImportType_EI_TSL; var TempNameValueBuffer: Record "Name/Value Buffer");
begin
case ImportType of
ImportType::Sales:
AddSalesProcessingActions(TempNameValueBuffer);
end;

end;

local procedure AddSalesProcessingActions(var TempNameValueBuffer: Record "Name/Value Buffer")
var
CustomValue: Record CustomValue_EI_TSL;
begin
CustomValue.AddActionToNameValueBuffer(
TempNameValueBuffer, ProcessingActionIDLbl, ProcessingActionCaptionTxt,
ExtendedDescriptionTxt);
end;

Implementing the Processing Action

Here is an example of how you can implement the processing logic on the sales import template.

    var
ProcessingActionIDLbl: Label 'ProcessingActionCode_TSL', Locked = true, MaxLength = 250;
ProcessingActionCaptionTxt: Label 'Add a new sales line or purchase line if the value is not zero', MaxLength = 250;
ExtendedDescriptionTxt: Label 'Specify details about what this action does and how to use it.', MaxLength = 2048;

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Excel Import Management_EI_TSL", 'OnAfterApplyandSaveData', '', false, false)]
local procedure OnAfterApplyandSaveData(var RecRef: RecordRef; var ValuesImported: Record "Exl. Imp Field Buffer_EI_TSL"; var TableID: Integer; var PropertyBuf2: Record "Name/Value Buffer"; var RowNo: Integer; var FirstRowNo: Integer; var TmpDimSetEntry: Record "Dimension Set Entry"; ImportTypeAsEnum: Enum ImportType_EI_TSL);
begin
if ImportTypeAsEnum = ImportTypeAsEnum::Sales then
SalesOnAfterApplyandSaveData(RecRef, ValuesImported);
end;

local procedure SalesOnAfterApplyandSaveData(var RecordRef: RecordRef; var ExlImpFieldBuffer: Record "Exl. Imp Field Buffer_EI_TSL");
var
SalesLine: Record "Sales Line";
begin
RecordRef.SetTable(SalesLine);
ProcessSalesLineCustomValueActions(SalesLine, ExlImpFieldBuffer);
end;

local procedure ProcessSalesLineCustomValueActions(ImportedSalesLine: Record "Sales Line"; var ExlImpFieldBuffer: Record "Exl. Imp Field Buffer_EI_TSL")
var
TempCustValueExlImpFieldBuffer: Record "Exl. Imp Field Buffer_EI_TSL" temporary;
CustomValue: Record CustomValue_EI_TSL;
begin
TempCustValueExlImpFieldBuffer.FilterOnCustomValuesUsingImportedFields(ExlImpFieldBuffer);
if not TempCustValueExlImpFieldBuffer.FindSet() then
exit;
repeat
case CustomValue."Processing Action" of
ProcessingActionIDLbl:
HandleMyNewProcessingAction(ImportedSalesLine, TempCustValueExlImpFieldBuffer, CustomValue);
end;
until TempCustValueExlImpFieldBuffer.Next() = 0;
end;

local procedure HandleMyNewProcessingAction(ImportedSalesLine: Record "Sales Line"; ExlImpFieldBuffer: Record "Exl. Imp Field Buffer_EI_TSL"; var CustomValue: Record CustomValue_EI_TSL)
var
NewSalesLine: Record "Sales Line";
ExcelImportManagement: Codeunit "Excel Import Management_EI_TSL";
RecordRef: RecordRef;
FieldRef: FieldRef;
TotalLineAmountExcl: Decimal;
begin
if ExlImpFieldBuffer."Cell Value as Text" = '' then
exit;
// convert the text to an amount
RecordRef.Open(Database::"Sales Line");
// the next 4 lines get the decimal value. It uses the functions in Excellent Importer because there are various transformations that can
// be done using the Excel Mapping. It is recommended you use this method instead of handling it yourself
FieldRef := RecordRef.Field(NewSalesLine.FieldNo("Unit Price")); // this can be any decimal field as we only want to get the decimal value
ExlImpFieldBuffer."Validate Field Method" := ExlImpFieldBuffer."Validate Field Method"::Never; // important - we just want the value
ExcelImportManagement.EvaluateFieldRef(FieldRef, ExlImpFieldBuffer);
TotalLineAmountExcl := FieldRef.Value();
if TotalLineAmountExcl = 0 then
exit;
NewSalesLine := ImportedSalesLine;
// do something like add a new line or make changes to the line that was imported
end;