Skip to main content
Solved

Override "Generate" Button in PO Receipt Line Details

  • September 24, 2025
  • 2 replies
  • 72 views

Forum|alt.badge.img+3

Hi Everyone,

Has anyone overridden the "Generate" button in the PO Receipts > Line Details popup?

I need to split a line quantity (e.g., 190) based on a custom field BreakQty (e.g., 60), so it generates lines like:

  • Line 1 – 60

  • Line 2 – 60

  • Line 3 – 60

  • Line 4 – 10

Each with a new sequential lot number.

Thanks in advance!

Best answer by Naveen Boga

@SaiKrishnaV   I recently worked on a similar requirement and thought it would be helpful to share this as a reference.

Please find the relevant source code below. The implementation follows a similar approach and should give you a good starting point for your scenario.

While integrating this, please ensure that any dependent methods or supporting logic from the base code are also included within the CreateNumbers method. This is important to maintain consistency with the original behavior and to avoid any missing dependencies or runtime issues.

Additionally, you may want to review how the base implementation handles validations and edge cases, and incorporate those as needed to ensure a complete and robust solution.

 

     public PXAction<POReceipt> GenerateLotSize;

[PXButton]
[PXUIField(DisplayName = "Generate Lot", MapEnableRights = PXCacheRights.Select,
MapViewRights = PXCacheRights.Select)]
public virtual IEnumerable generateLotSize(PXAdapter adapter)
{
POReceiptLine line = Base.transactions.Current;
LotSerOptions lotSerOptions = Base.Caches<LotSerOptions>().Current as LotSerOptions;
WMSPOLotSerOptionsExt lotSerOptionsExt = lotSerOptions?.GetExtension<WMSPOLotSerOptionsExt>();

if (line != null && line.ReceiptQty > 0 && lotSerOptionsExt != null && lotSerOptionsExt.UsrWMSItemLotSize > 0
// && Base.Document.Current?.ReceiptNbr.Contains(PX.Objects.AP.Messages.NewKey) == true
)
{
decimal? breakQty = lotSerOptionsExt.UsrWMSItemLotSize;
decimal? remainingQty = line.BaseReceiptQty - line.UnassignedQty;

// 1️⃣ First split: update the original line
decimal? currentQty = remainingQty >= breakQty ? breakQty : remainingQty;

// Update existing line to first lot-size chunk
line.BaseQty = currentQty;
SetLineQtyFromBase(line);
// Persist change to the existing line
Base.transactions.Update(line);

remainingQty -= currentQty;

while (remainingQty > 0)
{
decimal? nextQty = remainingQty >= breakQty ? breakQty : remainingQty;

// Clone line to avoid overwriting original
POReceiptLine newLine = PXCache<POReceiptLine>.CreateCopy(line);
newLine.BaseQty = nextQty;
CreateNumbers(newLine, Convert.ToDecimal(nextQty), false);
remainingQty -= nextQty;
}
}
return adapter.Get();
}



public virtual void CreateNumbers(POReceiptLine line, decimal deltaBaseQty, bool forceAutoNextNbr)
{
PXResult<InventoryItem, INLotSerClass> item = ReadInventoryItem(line.InventoryID);
POReceiptLineSplit split = base.Base1.LineToSplit(line);
INLotSerClass lsClass = item;

if (line != null)
LineCounters.Remove(line);

INLotSerTrack.Mode mode = GetTranTrackMode(line, lsClass);

if (!forceAutoNextNbr
&& lsClass.LotSerTrack == INLotSerTrack.SerialNumbered
&& lsClass.AutoSerialMaxCount > 0
&& lsClass.AutoSerialMaxCount < deltaBaseQty
&& (mode & INLotSerTrack.Mode.Create) > 0)
deltaBaseQty = lsClass.AutoSerialMaxCount ?? 0;

ILotSerNumVal lotSerNum = ReadLotSerNumVal(item);
foreach (POReceiptLineSplit lssplit in INLotSerialNbrAttribute.CreateNumbers<POReceiptLineSplit>(base.Base1.LineCache, lsClass, lotSerNum, mode, forceAutoNextNbr, deltaBaseQty))
{
string LotSerTrack = (mode & INLotSerTrack.Mode.Create) > 0
? lsClass.LotSerTrack
: INLotSerTrack.NotNumbered;

split.SplitLineNbr = null;
split.LotSerialNbr = lssplit.LotSerialNbr;
split.AssignedNbr = lssplit.AssignedNbr;
split.LotSerClassID = lssplit.LotSerClassID;
if (split is ILSGeneratedDetail gsplit && lssplit is ILSGeneratedDetail glssplit)
gsplit.HasGeneratedLotSerialNbr = glssplit.HasGeneratedLotSerialNbr;


if (!string.IsNullOrEmpty(line.LotSerialNbr) && (LotSerTrack == INLotSerTrack.LotNumbered || LotSerTrack == INLotSerTrack.SerialNumbered && line.Qty == 1m))
{
split.LotSerialNbr = line.LotSerialNbr;
}

if (LotSerTrack == INLotSerTrack.SerialNumbered)
{
split.UOM = null;
split.Qty = 1m;
split.BaseQty = 1m;
}
else
{
split.UOM = null;
split.BaseQty = deltaBaseQty;
split.Qty = deltaBaseQty;
}

if (lsClass.LotSerTrackExpiration == true)
split.ExpireDate = ExpireDateByLot(split, line);

PXCache<POReceiptLineSplit>.Insert(Base, base.Base1.Clone(split));
deltaBaseQty -= split.BaseQty.Value;
}

if (deltaBaseQty > 0m && (lsClass.LotSerTrack != INLotSerTrack.SerialNumbered || decimal.Remainder(deltaBaseQty, 1m) == 0m))
{
line.UnassignedQty += deltaBaseQty;
}
else if (deltaBaseQty > 0m)
{
POReceiptLine oldLine = PXCache<POReceiptLine>.CreateCopy(line);

line.BaseQty -= deltaBaseQty;
SetLineQtyFromBase(line);

if (Math.Abs(oldLine.Qty.Value - line.Qty.Value) >= 0.0000005m)
{
base.Base1.LineCache.RaiseFieldUpdated(LineQtyField.Name, line, oldLine.Qty);
base.Base1.LineCache.RaiseRowUpdated(line, oldLine);
}
}

if (line.UnassignedQty > 0)
RaiseUnassignedExceptionHandling(line);
}

 

2 replies

Forum|alt.badge.img
  • Semi-Pro I
  • October 14, 2025

@svwk05 

  1. Create a Custom Field

    • Add a custom field to the PO Receipt Line screen (e.g., UsrBreakQty) to store the break quantity.

    • Ensure it’s accessible in the Line Details popup.

  2. Override the Generate Button

    • In Customization Project, navigate to PO Receipt (POReceiptEntry) → Line Details (POReceiptLineSplit).

    • Add a graph extension and create an event handler for the Generate button.

  3. Custom Generate Logic

    • Instead of the standard logic, loop through the total UnassignedQty and split it according to BreakQty.

    • For each split:

      • Create a new POReceiptLineSplit record.

      • Assign the quantity (BreakQty or remaining).

      • Assign a new sequential Lot/Serial Number.

      • Insert the split record into the transaction.

Pseudo-Code Example

 

public class POReceiptEntry_Extension : PXGraphExtension<POReceiptEntry> { public PXAction<POReceiptLine> customGenerate; [PXButton(CommitChanges = true)] [PXUIField(DisplayName = "Generate Custom")] protected void CustomGenerate() { var row = Base.ReceiptLines.Current; if (row == null) return; decimal remainingQty = row.UnassignedQty ?? 0m; decimal breakQty = row.GetExtension<POReceiptLineExt>().UsrBreakQty ?? 1m; int lotNumber = 0; while (remainingQty > 0) { decimal qtyToGenerate = Math.Min(breakQty, remainingQty); lotNumber++; POReceiptLineSplit split = new POReceiptLineSplit { ReceiptType = row.ReceiptType, ReceiptNbr = row.ReceiptNbr, LineNbr = row.LineNbr, Quantity = qtyToGenerate, LotSerialNbr = (row.StartLotSerialNbr ?? "0000") + lotNumber.ToString("D2") }; Base.splits.Insert(split); remainingQty -= qtyToGenerate; } Base.splits.View.RequestRefresh(); } }


Naveen Boga
Captain II
Forum|alt.badge.img+20
  • Captain II
  • Answer
  • April 23, 2026

@SaiKrishnaV   I recently worked on a similar requirement and thought it would be helpful to share this as a reference.

Please find the relevant source code below. The implementation follows a similar approach and should give you a good starting point for your scenario.

While integrating this, please ensure that any dependent methods or supporting logic from the base code are also included within the CreateNumbers method. This is important to maintain consistency with the original behavior and to avoid any missing dependencies or runtime issues.

Additionally, you may want to review how the base implementation handles validations and edge cases, and incorporate those as needed to ensure a complete and robust solution.

 

     public PXAction<POReceipt> GenerateLotSize;

[PXButton]
[PXUIField(DisplayName = "Generate Lot", MapEnableRights = PXCacheRights.Select,
MapViewRights = PXCacheRights.Select)]
public virtual IEnumerable generateLotSize(PXAdapter adapter)
{
POReceiptLine line = Base.transactions.Current;
LotSerOptions lotSerOptions = Base.Caches<LotSerOptions>().Current as LotSerOptions;
WMSPOLotSerOptionsExt lotSerOptionsExt = lotSerOptions?.GetExtension<WMSPOLotSerOptionsExt>();

if (line != null && line.ReceiptQty > 0 && lotSerOptionsExt != null && lotSerOptionsExt.UsrWMSItemLotSize > 0
// && Base.Document.Current?.ReceiptNbr.Contains(PX.Objects.AP.Messages.NewKey) == true
)
{
decimal? breakQty = lotSerOptionsExt.UsrWMSItemLotSize;
decimal? remainingQty = line.BaseReceiptQty - line.UnassignedQty;

// 1️⃣ First split: update the original line
decimal? currentQty = remainingQty >= breakQty ? breakQty : remainingQty;

// Update existing line to first lot-size chunk
line.BaseQty = currentQty;
SetLineQtyFromBase(line);
// Persist change to the existing line
Base.transactions.Update(line);

remainingQty -= currentQty;

while (remainingQty > 0)
{
decimal? nextQty = remainingQty >= breakQty ? breakQty : remainingQty;

// Clone line to avoid overwriting original
POReceiptLine newLine = PXCache<POReceiptLine>.CreateCopy(line);
newLine.BaseQty = nextQty;
CreateNumbers(newLine, Convert.ToDecimal(nextQty), false);
remainingQty -= nextQty;
}
}
return adapter.Get();
}



public virtual void CreateNumbers(POReceiptLine line, decimal deltaBaseQty, bool forceAutoNextNbr)
{
PXResult<InventoryItem, INLotSerClass> item = ReadInventoryItem(line.InventoryID);
POReceiptLineSplit split = base.Base1.LineToSplit(line);
INLotSerClass lsClass = item;

if (line != null)
LineCounters.Remove(line);

INLotSerTrack.Mode mode = GetTranTrackMode(line, lsClass);

if (!forceAutoNextNbr
&& lsClass.LotSerTrack == INLotSerTrack.SerialNumbered
&& lsClass.AutoSerialMaxCount > 0
&& lsClass.AutoSerialMaxCount < deltaBaseQty
&& (mode & INLotSerTrack.Mode.Create) > 0)
deltaBaseQty = lsClass.AutoSerialMaxCount ?? 0;

ILotSerNumVal lotSerNum = ReadLotSerNumVal(item);
foreach (POReceiptLineSplit lssplit in INLotSerialNbrAttribute.CreateNumbers<POReceiptLineSplit>(base.Base1.LineCache, lsClass, lotSerNum, mode, forceAutoNextNbr, deltaBaseQty))
{
string LotSerTrack = (mode & INLotSerTrack.Mode.Create) > 0
? lsClass.LotSerTrack
: INLotSerTrack.NotNumbered;

split.SplitLineNbr = null;
split.LotSerialNbr = lssplit.LotSerialNbr;
split.AssignedNbr = lssplit.AssignedNbr;
split.LotSerClassID = lssplit.LotSerClassID;
if (split is ILSGeneratedDetail gsplit && lssplit is ILSGeneratedDetail glssplit)
gsplit.HasGeneratedLotSerialNbr = glssplit.HasGeneratedLotSerialNbr;


if (!string.IsNullOrEmpty(line.LotSerialNbr) && (LotSerTrack == INLotSerTrack.LotNumbered || LotSerTrack == INLotSerTrack.SerialNumbered && line.Qty == 1m))
{
split.LotSerialNbr = line.LotSerialNbr;
}

if (LotSerTrack == INLotSerTrack.SerialNumbered)
{
split.UOM = null;
split.Qty = 1m;
split.BaseQty = 1m;
}
else
{
split.UOM = null;
split.BaseQty = deltaBaseQty;
split.Qty = deltaBaseQty;
}

if (lsClass.LotSerTrackExpiration == true)
split.ExpireDate = ExpireDateByLot(split, line);

PXCache<POReceiptLineSplit>.Insert(Base, base.Base1.Clone(split));
deltaBaseQty -= split.BaseQty.Value;
}

if (deltaBaseQty > 0m && (lsClass.LotSerTrack != INLotSerTrack.SerialNumbered || decimal.Remainder(deltaBaseQty, 1m) == 0m))
{
line.UnassignedQty += deltaBaseQty;
}
else if (deltaBaseQty > 0m)
{
POReceiptLine oldLine = PXCache<POReceiptLine>.CreateCopy(line);

line.BaseQty -= deltaBaseQty;
SetLineQtyFromBase(line);

if (Math.Abs(oldLine.Qty.Value - line.Qty.Value) >= 0.0000005m)
{
base.Base1.LineCache.RaiseFieldUpdated(LineQtyField.Name, line, oldLine.Qty);
base.Base1.LineCache.RaiseRowUpdated(line, oldLine);
}
}

if (line.UnassignedQty > 0)
RaiseUnassignedExceptionHandling(line);
}