We are implementing a custom WMS scan mode in the Receive Put Away screen to print labels automatically after scanning a PO Receipt, Inventory Item
Requirement:
We need to print labels silently (without opening the report UI) using Device Hub when the user scan the Item.
Environment:
- Version: Acumatica 2025 R2 (25.200.0248)
- Module: WMS (Receive Put Away)
Code is :
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using PX.Common;
using PX.Data;
using PX.Data.BQL;
using PX.Data.BQL.Fluent;
using PX.BarcodeProcessing;
using PX.Objects.AP;
using PX.Objects.IN;
using PX.Objects.IN.WMS;
using PX.SM;
using PX.Objects.Common;
namespace PX.Objects.PO.WMS
{
using static PX.BarcodeProcessing.BarcodeDrivenStateMachine<ReceivePutAway, ReceivePutAway.Host>;
using static PX.Objects.IN.WMS.INScanWarehousePath.ScanPathMode;
using WMSBase = WarehouseManagementSystem<ReceivePutAway, ReceivePutAway.Host>;
public class ReceivePutAwayExts : ReceivePutAway.ScanExtension
{
public static bool IsActive() => true;
public sealed class PrintMode : ReceivePutAway.ScanMode
{
public const string Value = "PRTLBL";
public override string Code => Value;
public override string Description => Msg.Description;
#region State Machine
protected override IEnumerable<ScanState<ReceivePutAway>> CreateStates()
{
yield return new PrintMode.POReceiptState();
yield return new PrintMode.InventoryItemState().Intercept.HandleAbsence.ByAppend((basis, barcode) =>
{
if (basis.TryProcessBy<PrintMode.POReceiptState>(barcode,
StateSubstitutionRule.KeepPositiveReports |
StateSubstitutionRule.KeepApplication))
return AbsenceHandling.Done;
return AbsenceHandling.Skipped;
});
yield return new PrintMode.PrintQtyState();
yield return new PrintMode.ConfirmState();
}
protected override IEnumerable<ScanTransition<ReceivePutAway>> CreateTransitions()
{
return StateFlow(flow => flow
.From<PrintMode.POReceiptState>()
.NextTo<PrintMode.InventoryItemState>()
.NextTo<PrintMode.PrintQtyState>());
}
protected override IEnumerable<ScanRedirect<ReceivePutAway>> CreateRedirects() => AllWMSRedirects.CreateFor<ReceivePutAway>();
protected override void ResetMode(bool fullReset)
{
base.ResetMode(fullReset);
Clear<PrintMode.POReceiptState>(when: fullReset);
Clear<PrintMode.InventoryItemState>();
Clear<PrintMode.PrintQtyState>();
if (fullReset)
{
Basis.RefNbr = null;
Basis.InventoryID = null;
Basis.Qty = null;
Basis.NoteID = null;
}
}
#endregion
#region States
public sealed class POReceiptState : WMSBase.RefNbrState<POReceipt>
{
protected override string StatePrompt => Msg.Prompt;
protected override POReceipt GetByBarcode(string barcode)
{
POReceipt receipt =
SelectFrom<POReceipt>.
LeftJoin<Vendor>.On<POReceipt.vendorID.IsEqual<Vendor.bAccountID>>.SingleTableOnly.
Where<POReceipt.receiptNbr.IsEqual<@P.AsString>>.
View.ReadOnly.Select(Basis, barcode);
return receipt;
}
protected override void Apply(POReceipt receipt)
{
Basis.ReceiptType = receipt.ReceiptType;
Basis.RefNbr = receipt.ReceiptNbr;
Basis.TranDate = receipt.ReceiptDate;
Basis.NoteID = receipt.NoteID;
Basis.Graph.Document.Current = receipt;
}
protected override void ClearState()
{
Basis.Graph.Document.Current = null;
Basis.ReceiptType = null;
Basis.RefNbr = null;
Basis.TranDate = null;
Basis.NoteID = null;
}
protected override void ReportMissing(string barcode) => Basis.ReportError(Msg.Missing, barcode);
protected override void ReportSuccess(POReceipt receipt) => Basis.ReportInfo(Msg.Ready, receipt.ReceiptNbr);
#region Messages
[PXLocalizable]
public abstract class Msg
{
public const string Prompt = "Scan the PO receipt number.";
public const string Ready = "The {0} receipt is loaded and ready to be processed.";
public const string Missing = "The {0} receipt is not found.";
public const string InvalidType = "The {0} receipt cannot be processed because it has the {1} type.";
public const string InvalidOrderType = "The {0} receipt cannot be processed because it has the {1} order type.";
public const string MultiSites = "The {0} receipt should have only one warehouse to be processed.";
public const string HasNonStockKit = "The {0} receipt cannot be processed because it contains a non-stock kit item.";
}
#endregion
}
public sealed class InventoryItemState : EntityState<InventoryItem>
{
public const string Value = "ITEM";
public override string Code => Value;
protected override string StatePrompt => Msg.Prompt;
protected override InventoryItem GetByBarcode(string barcode)
{
InventoryItem item =
SelectFrom<InventoryItem>.
InnerJoin<INItemXRef>.On<INItemXRef.FK.InventoryItem>.
Where<
INItemXRef.alternateID.IsEqual<@P.AsString>.
And<INItemXRef.alternateType.IsEqual<INAlternateType.barcode>>.
And<InventoryItem.itemStatus.IsNotIn<InventoryItemStatus.inactive, InventoryItemStatus.markedForDeletion>>>.
OrderBy<INItemXRef.alternateType.Asc>.
View.ReadOnly
.Select(Basis, barcode).FirstOrDefault();
if (item == null)
{
var inventory = IN.InventoryItem.UK.Find(Basis, barcode);
if (inventory != null && inventory.ItemStatus.IsNotIn(InventoryItemStatus.Inactive, InventoryItemStatus.MarkedForDeletion))
return inventory;
}
return item;
}
protected override Validation Validate(InventoryItem entity)
{
POReceiptLine row = SelectFrom<POReceiptLine>
.Where<POReceiptLine.receiptNbr.IsEqual<@P.AsString>
.And<POReceiptLine.receiptType.IsEqual<@P.AsString>>
.And<POReceiptLine.inventoryID.IsEqual<@P.AsInt>>>
.View.ReadOnly.Select(Basis, Basis.RefNbr, Basis.ReceiptType, entity.InventoryID).FirstOrDefault();
if (row != null)
{
return Validation.Ok;
}
else
{
return Validation.Fail(Msg.NotOnReceipt, entity.InventoryCD.Trim());
}
}
protected override void ReportMissing(string barcode) => Basis.Reporter.Error(Msg.Missing, barcode);
protected override void Apply(InventoryItem entity)
{
Basis.InventoryID = entity.InventoryID;
}
protected override void ClearState()
{
Basis.InventoryID = null;
}
protected override void ReportSuccess(InventoryItem entity) => Basis.Reporter.Info(Msg.Ready, entity.InventoryCD.Trim());
#region Messages
[PXLocalizable]
public abstract class Msg
{
public const string Prompt = "Scan the barcode of an item on the receipt.";
public const string Ready = "The {0} item is selected.";
public const string Missing = "The {0} item barcode is not found.";
public const string NotOnReceipt = "The label cannot be printed. {0} item is not found in the receipt.";
}
#endregion
}
public sealed class PrintQtyState : WMSBase.EntityState<int?>
{
public const string Value = "PRTQTY";
public class value : BqlString.Constant<value> { public value() : base(SetNextIndexState.Value) { } }
public override string Code => Value;
protected override string StatePrompt => Msg.Prompt;
protected override int? GetByBarcode(string barcode) => int.TryParse(barcode, out int qty) ? qty : (int?)null;
protected override void ReportMissing(string barcode) => Basis.ReportError(Msg.BadFormat, barcode);
protected override void Apply(int? qty) => Basis.Qty = qty;
protected override void ClearState() => Basis.Qty = null;
protected override void ReportSuccess(int? qty) => Basis.ReportInfo(Msg.Ready, qty);
#region Messages
[PXLocalizable]
public abstract class Msg
{
public const string Prompt = "Enter the label Qty.";
public const string Ready = "The print qty is set to {0}.";
public const string BadFormat = "{0} is not a valid print qty.";
}
#endregion
}
public sealed class ConfirmState : WMSBase.ConfirmationState
{
public override string Prompt => Basis.Localize(Msg.Prompt, Basis.Qty, Basis.RefNbr, Basis.SightOf<WMSScanHeader.inventoryID>());
protected override FlowStatus PerformConfirmation() => Get<Logic>().Confirm();
public class Logic : ScanExtension
{
public static bool IsActive() => true;
public virtual FlowStatus Confirm()
{
UserPreferences userPreferences =
SelectFrom<UserPreferences>
.Where<UserPreferences.userID.IsEqual<@P.AsGuid>>
.View.Select(this.Graph, this.Graph.Accessinfo.UserID);
PXTrace.WriteInformation($"RefNbr={Basis.RefNbr}, ReceiptType={Basis.ReceiptType}");
if (userPreferences?.DefaultPrinterID == null)
return FlowStatus.Fail(Msg.FailedToPrint, "No default printer.");
if (!PXAccess.FeatureInstalled<CS.FeaturesSet.deviceHub>())
return FlowStatus.Fail(Msg.FailedToPrint, "DeviceHub not enabled.");
var parameters = new Dictionary<string, string>
{
{ "ReceiptNbr", Basis.RefNbr },
{ "ReceiptType", Basis.ReceiptType },
{ "InventoryID", Basis.InventoryID?.ToString() }
};
// throw new PXReportRequiredException(parameters, "PO646000", "Print Label");
try
{
SMPrinter printer = PXSelect<SMPrinter,
Where<SMPrinter.printerID, Equal<Required<SMPrinter.printerID>>>>
.Select(this.Graph, userPreferences.DefaultPrinterID);
if (printer == null)
return FlowStatus.Fail(Msg.FailedToPrint, "Printer not found.");
DeviceHubTools.PrintReportViaDeviceHub<PX.Objects.CR.BAccount>(
this.Graph,
"PO646000",
parameters,
printer.PrinterName,
null,
System.Threading.CancellationToken.None
);
Basis.ReportInfo("Label printed successfully.");
return FlowStatus.Ok;
}
catch (Exception ex)
{
PXTrace.WriteError(ex);
return FlowStatus.Fail(Msg.FailedToPrint, ex.Message);
}
}
}
[PXLocalizable]
new public abstract class Msg
{
public const string Prompt = "Printing {0} labels for {1}: {2}";
public const string ItemOrReceiptPrompt = "Scan the barcode of the item or the next receipt number.";
public const string FailedToPrint = "Failed to print labels: {0}";
}
}
#endregion
#region Redirect
public sealed class RedirectFrom<TForeignBasis> : WMSBase.RedirectFrom<TForeignBasis>.SetMode<PrintMode>
where TForeignBasis : PXGraphExtension, IBarcodeDrivenStateMachine
{
public override string Code => "PRTLBL";
public override string DisplayName => Msg.DisplayName;
private string RefNbr { get; set; }
public override bool IsPossible
{
get
{
return true;
}
}
protected override bool PrepareRedirect()
{
if (Basis is ReceivePutAway rpa && rpa.RefNbr != null)
{
if (rpa.FindMode<ReceivePutAwayExts.PrintMode>().TryValidate(rpa.Receipt).By<ReceivePutAway.ReceiptState>() is Validation valid && valid.IsError == true)
{
rpa.ReportError(valid.Message, valid.MessageArgs);
return false;
}
else
RefNbr = rpa.RefNbr;
}
return true;
}
protected override void CompleteRedirect()
{
if (Basis is ReceivePutAway rpa && rpa.CurrentMode.Code != ReceivePutAway.ReturnMode.Value && this.RefNbr != null)
if (rpa.TryProcessBy(ReceivePutAway.ReceiptState.Value, RefNbr, StateSubstitutionRule.KeepAll & ~StateSubstitutionRule.KeepPositiveReports))
{
rpa.SetDefaultState();
RefNbr = null;
}
}
#region Messages
[PXLocalizable]
public abstract class Msg
{
public const string DisplayName = "Print Label";
}
#endregion
}
#endregion
#region Messages
[PXLocalizable]
public abstract class Msg
{
public const string Description = "Print Labels";
}
#endregion
}
[PXOverride]
public bool get_DocumentIsEditable(Func<bool> base_DocumentIsEditable)
{
if (Base1.Header.Mode == PrintMode.Value)
{
return true;
}
else
{
return base_DocumentIsEditable();
}
}
[PXOverride]
public virtual IEnumerable<ScanMode<ReceivePutAway>> CreateScanModes(Func<IEnumerable<ScanMode<ReceivePutAway>>> base_CreateScanModes)
{
foreach (var mode in base_CreateScanModes())
yield return mode;
yield return new PrintMode();
}
[PXOverride]
public virtual ScanMode<ReceivePutAway> DecorateScanMode(ScanMode<ReceivePutAway> original, Func<ScanMode<ReceivePutAway>, ScanMode<ReceivePutAway>> base_DecorateScanMode)
{
var mode = base_DecorateScanMode(original);
if (mode is ReceivePutAway.ReceiveMode receiveMode)
{
receiveMode
.Intercept.CreateRedirects.ByAppend(() => new ScanRedirect<ReceivePutAway>[] { new PrintMode.RedirectFrom<ReceivePutAway>() });
}
if (mode is ReceivePutAway.PutAwayMode putawayMode)
{
putawayMode
.Intercept.CreateRedirects.ByAppend(() => new ScanRedirect<ReceivePutAway>[] { new PrintMode.RedirectFrom<ReceivePutAway>() });
}
return mode;
}
}
}