Custom consumption scan logic

Shop Floor Production (BC)  •  Article  •  8/27/2026  • 

Shop Floor Production supports several built-in consumption scanning modes: consuming an entire license plate with exact quantities, consuming an entire license plate with quantity variations, and consuming product via a GS1-compliant barcode. These modes cover most use cases, but may not fit customers who use non-GS1-compliant labels or have unique scanning workflows.

To address this, Shop Floor Production supports a Custom scanning mode that lets partners and developers extend the scanning experience via a per-tenant extension (PTE), entirely in AL.

Enabling custom scanning

Set the License Plate Consumption Mode field on the Shop Floor Production Setup page to Custom. On the Inputs screen of a production order operation, the + License Plate button is then replaced by a + Custom Scan button.

A work center can define its own License Plate Consumption Mode on the Work Center card, and that value overrides the one on Shop Floor Production Setup. Custom is not available at work center level, so a work center that specifies a mode of its own cannot use custom scanning. Leave the field on the work center blank so that the work center inherits Custom from Shop Floor Production Setup.

Events

Custom consumption scanning is driven by two integration events.

Validation

Selecting + Custom Scan opens the Custom Scan dialog. When the user enters or scans a value and selects Confirm, Shop Floor Production first validates the entered data:

SFPBCCustomConsScan14FDW.Codeunit.al

[IntegrationEvent(false, false)]
local procedure OnValidateCustomConsumptionScan(ProductionOrderNo: Code[20]; ProdOrderLineNo: Integer; ScanValue: Text)
begin
end;

ScanValue is the value entered by the user. Throwing an Error() in this event rejects the scan and displays the error message.

Pre-population

After the scan is validated, Shop Floor Production uses the scanned value to pre-populate fields on the Register Input screen:

SFPBCCustomConsScan14FDW.Codeunit.al

[IntegrationEvent(false, false)]
local procedure OnSetConsumptionRegistrationCustomScanValues(ProductionOrderNo: Code[20]; ProdOrderLineNo: Integer; ScanValue: Text; var DefaultConsumptionValues: Dictionary of [Text, Text]; var CustomFormFields: Dictionary of [Text, Text]; var ProdOrderComponent: Record "Prod. Order Component")
begin
end;
  • DefaultConsumptionValues is a dictionary of pre-populated values for standard consumption fields such as lot number, license plate number, and quantity.
  • CustomFormFields is a dictionary of pre-populated values for custom form fields defined via the custom form field extensibility events.
  • ProdOrderComponent lets the PTE specify which production order component should be consumed.

The following helper functions simplify populating DefaultConsumptionValues:

SFPBCCustomConsScan14FDW.SetDefaultLotNo(var DefaultFormValues: Dictionary of [Text, Text]; LotNo: Text)
SFPBCCustomConsScan14FDW.SetDefaultInventory(var DefaultFormValues: Dictionary of [Text, Text]; Inventory: Text)
SFPBCCustomConsScan14FDW.SetDefaultExpirationDate(var DefaultFormValues: Dictionary of [Text, Text]; ExpirationDate: Text)
SFPBCCustomConsScan14FDW.SetDefaultQuantity(var DefaultFormValues: Dictionary of [Text, Text]; Quantity: Text)
SFPBCCustomConsScan14FDW.SetDefaultBinCode(var DefaultFormValues: Dictionary of [Text, Text]; BinCode: Text)
SFPBCCustomConsScan14FDW.SetDefaultLicensePlateNo(var DefaultFormValues: Dictionary of [Text, Text]; LicensePlateNo: Text)
SFPBCCustomConsScan14FDW.SetDefaultWeightQty(var DefaultFormValues: Dictionary of [Text, Text]; WeightQty: Text)

To add a default value directly, add the field key and value to the dictionary:

DefaultConsumptionValues.Add('lotNo', ScanValue);

To pre-populate a custom form field, add its key and value to CustomFormFields, either directly or via the helper function:

CustomFormFields.Add('caseNo', ScanValue);

// or

SFPBCCustomConsScan14FDW.AddCustomField(CustomFormFields, 'caseNo', ScanValue);

For a default value to work, the field key added to each dictionary must match the field key defined for that field via the custom form fields events.

Choosing a component

If a production order has multiple components, control which one is consumed by returning the desired record via the ProdOrderComponent parameter of OnSetConsumptionRegistrationCustomScanValues.

Example

The following example:

  1. Rejects the scan with an error if the word REJECT is entered.
  2. Adds a custom caseNo form element to the Register Input screen (see custom form fields).
  3. Selects the last production order component for consumption if GOTO2 is entered; otherwise selects the first component.
  4. Pre-populates the custom caseNo field and the standard Lot No. field with the scanned value.
codeunit 50100 SFPCustomChanges
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::SFPBCCustomConsScan14FDW, OnValidateCustomConsumptionScan, '', false, false)]
    local procedure ValidateCustomConsumptionScan(ScanValue: Text)
    var
        ScanRejectedErr: Label 'Scan value of "%1" is not allowed.', Comment = '%1 = the value entered by the user';
    begin
        if UpperCase(ScanValue) = 'REJECT' then
            Error(ScanRejectedErr, ScanValue);
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::SFPBCCustomConsScan14FDW, OnSetConsumptionRegistrationCustomScanValues, '', false, false)]
    local procedure SetCustomScanValues(ProductionOrderNo: Code[20]; ProdOrderLineNo: Integer; ScanValue: Text; var DefaultConsumptionValues: Dictionary of [Text, Text]; var CustomFormFields: Dictionary of [Text, Text]; var ProdOrderComponent: Record "Prod. Order Component")
    var
        ProdOrderComponentLine: Record "Prod. Order Component";
        SFPBCCustomConsScan: Codeunit SFPBCCustomConsScan14FDW;
    begin
        SFPBCCustomConsScan.SetDefaultLotNo(DefaultConsumptionValues, ScanValue);
        CustomFormFields.Add('caseNo', ScanValue);

        ProdOrderComponentLine.SetRange(Status, ProdOrderComponentLine.Status::Released);
        ProdOrderComponentLine.SetRange("Prod. Order No.", ProductionOrderNo);
        ProdOrderComponentLine.SetRange("Prod. Order Line No.", ProdOrderLineNo);
        if ProdOrderComponentLine.FindFirst() then;
        if UpperCase(ScanValue) = 'GOTO2' then
            if ProdOrderComponentLine.FindLast() then;

        ProdOrderComponent := ProdOrderComponentLine;
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::SFPBCGetFormElements14FDW, OnAddCustomFormElementsForConsumption, '', false, false)]
    local procedure AddCustomFormElements(var TempCustomFormElements: Record TempCustomFormElements14FDW temporary; var CustomFormFields: Dictionary of [Text, Text])
    var
        DefaultCaseNo: Text;
    begin
        if CustomFormFields.ContainsKey('caseNo') then
            DefaultCaseNo := CustomFormFields.Get('caseNo');

        TempCustomFormElements.AddNewCustomFormElement('caseNo', 'Case No.', FormElementTypes14FDW::Text, false, true, false);
        TempCustomFormElements.SetDefaultValue(DefaultCaseNo);
    end;
}

caseNo is used consistently in both SetCustomScanValues (to store the scanned value) and AddCustomFormElements (to define the field and set its default). This matching key connects the scanned input to the rendered form field.