Skip to main content

Workflow Api - Adding additional Status & Changing workflow to Shipment Screen from Code

  • October 2, 2023
  • 8 replies
  • 664 views

Keith Richardson
Semi-Pro I
Forum|alt.badge.img+2

This is a follow up to a previous blog post I made - 


Here, I added a se​​​​​​t Delivered action to the SO Quick Process that adds it into the middle of the SOShipment workflow. Today, I will go into more detail on how I was able to change the workflow, completely from the Extension Library.

The use case is that we want orders to be delivered before they are invoiced. We set them as confirmed when picked, and they may sit and wait for the delivery date, or longer, depending on the local delivery route.  We added an action to ensure that it is delivered before invoicing. This action may be pressed on each shipment directly, or triggered from another screen in our custom delivery application.

To accomplish this, we need to override some of the existing workflow, as well as adding a custom event handler.

The first step, is adding our Custom event handler to a graph extension on the SOShipment table.
 


public class SOShipmentExt : PXCacheExtension<PX.Objects.SO.SOShipment>
{
public static bool IsActive()
{
return true;
}

public class Events : PXEntityEvent<SOShipment>.Container<Events>
{
public PXEntityEvent<SOShipment> ShipmentDelivered;
}

}

Next, we need to tie this to a workflow event handler on a SOShipment Entry graph extension. Once we add this, we can then update our SetDelivered action to fire this event.

 

public class SOShipmentEntry_Extension : PXGraphExtension<SOShipmentEntry>
{
public PXWorkflowEventHandler<SOShipment> OnShipmentDelivered;

public PXAction<SOShipment> setDelivered;
[PXUIField(DisplayName = "Set Delivered", MapEnableRights = PXCacheRights.Select, MapViewRights = PXCacheRights.Select)]
[PXButton]
protected virtual IEnumerable SetDelivered(PXAdapter adapter)
{

HaunWelding.Delivery.SOShipmentExt.Events
.Select(e => e.ShipmentDelivered)
.FireOn(Base, Base.Document.Current);

return adapter.Get();
}
}

You can see that the action selects the event that we created in the DAC, and fires the event on our current document. The workflow engine will now take over.

In our workflow extension, we need to add multiple things

  1. Add our custom status for Delivered
  2. Set our SetDelivered action’s desired visibility
  3. Register our event handler
  4. Update the default workflow with our new status & transitions

 


using State = SOShipmentStatus;
using StateExt = SOShipmentStatusExt;
using static SOShipment;
using static BoundedTo<SOShipmentEntry, SOShipment>;



//find the code that generates the configuration and override that. In this case, it is the base screen configuration for SOOrderEntry.
public class SOShipmentEntryWorkFlowExt : PXGraphExtension<PX.Objects.SO.SOShipmentEntry_Workflow, SOShipmentEntry_Extension, SOShipmentEntry>
{
public class Conditions : Condition.Pack
{
//add a condition to check if it is a transfer order. We do not require setting delivered on a transfer order.
public Condition IsTransferOrder => GetOrCreate(b => b.FromBql<
Where<SOShipment.shipmentType.IsEqual<SOShipmentType.transfer>>
>());
}
public override void Configure(PXScreenConfiguration config) => Configure(config.GetScreenConfigurationContext<SOShipmentEntry, SOShipment>());

public virtual void Configure(WorkflowContext<SOShipmentEntry, SOShipment> context)
{
//define an object for the new conditions set above
var newConditions = context.Conditions.GetPack<Conditions>();
//bring in the base conditions
var baseConditions = context.Conditions.GetPack<PX.Objects.SO.SOShipmentEntry_Workflow.Conditions>();
//update the workflow engine....
context.UpdateScreenConfigurationFor(screen =>
{
return screen
.WithFieldStates(fieldstates => {
//add the delivered value
fieldstates.Add<SOShipment.status>(fieldstate => fieldstate.SetComboValue(SOShipmentStatusExt.Delivered, "Delivered"));
})
.WithActions(actions =>
{
//declare visibility/enablement on our set delivered action
actions.Add<SOShipmentEntry_Extension>(g => g.setDelivered, c => c.IsDisabledWhen(!baseConditions.IsConfirmed).IsHiddenWhen(newConditions.IsTransferOrder));
})
.WithHandlers(handlers =>
{
handlers.Add(handler =>
{
//declare our event handler
return handler
.WithTargetOf<SOShipment>()
.OfEntityEvent<Delivery.SOShipmentExt.Events>(e => e.ShipmentDelivered)
.Is<SOShipmentEntry_Extension>(g => g.OnShipmentDelivered)
.UsesTargetAsPrimaryEntity()
.DisplayName("Shipment Delivered");
}); // Shipment Delivered
})
//update the default workflow..
.UpdateDefaultFlow(flow => flow
.WithFlowStates(fss =>
{
//update the confirmed status flow
fss.Update<State.confirmed>(flowState =>
{
return flowState
.WithActions(actions =>
{
//remove create invoice and add set delivered
actions.Remove(g => g.createInvoice);
actions.Add<SOShipmentEntry_Extension>(g => g.setDelivered);
})
.WithEventHandlers(handlers =>
{
//make sure the new event handler is tied to this state
handlers.Add<SOShipmentEntry_Extension>(g => g.OnShipmentDelivered);
});
});
//add our new state....
fss.Add<StateExt.delivered>(flowState =>
{
//add everyting that should be done on the delivered state - copied from Confirmed state
return flowState
.WithActions(actions =>
{
actions.Add(g => g.createInvoice, a => a.IsDuplicatedInToolbar().WithConnotation(ActionConnotation.Success));
actions.Add(g => g.printShipmentConfirmation);
actions.Add(g => g.correctShipmentAction);
actions.Add(g => g.printLabels);
actions.Add(g => g.printCommercialInvoices);
actions.Add(g => g.validateAddresses);
actions.Add(g => g.emailShipment);
actions.Add<PX.Objects.SO.GraphExtensions.SOShipmentEntryExt.Intercompany>(e => e.generatePOReceipt);
actions.Add(g => g.UpdateIN);
})
//ensure that the same event handlers on confirmed are on delivered
.WithEventHandlers(handlers =>
{
handlers.Add(g => g.OnInvoiceLinked);
handlers.Add(g => g.OnShipmentCorrected);
})
.WithFieldStates(DisableWholeScreen);
});
})
//now we update our transitions
.WithTransitions(transitions =>
{
transitions.UpdateGroupFrom<SOShipmentStatus.confirmed>(ts =>
{
//remove the transitions from confirmed that we no longer want
ts.Remove(t => t.To<State.completed>().IsTriggeredOn(g => g.UpdateIN).When(baseConditions.IsNotBillableAndReleased));
ts.Remove(t => t.To<State.invoiced>().IsTriggeredOn(g => g.OnInvoiceLinked).When(baseConditions.IsInvoiced));
ts.Remove(t => t.To<State.partiallyInvoiced>().IsTriggeredOn(g => g.OnInvoiceLinked).When(baseConditions.IsPartiallyInvoiced));
//add the transition from confirmed to delivered when the event handler is triggered
ts.Add(t => t.To<StateExt.delivered>().IsTriggeredOn<SOShipmentEntry_Extension>(g => g.OnShipmentDelivered));
});
//add the new transitions from delivered
transitions.AddGroupFrom<StateExt.delivered>(ts =>
{
ts.Add(t => t.To<State.open>().IsTriggeredOn(g => g.OnShipmentCorrected).When(!baseConditions.IsConfirmed));
ts.Add(t => t.To<State.completed>().IsTriggeredOn(g => g.UpdateIN).When(baseConditions.IsNotBillableAndReleased));
ts.Add(t => t.To<State.invoiced>().IsTriggeredOn(g => g.OnInvoiceLinked).When(baseConditions.IsInvoiced));
ts.Add(t => t.To<State.partiallyInvoiced>().IsTriggeredOn(g => g.OnInvoiceLinked).When(baseConditions.IsPartiallyInvoiced));
});
})
);
});
}



protected virtual void DisableWholeScreen(FieldState.IContainerFillerFields states)
{
states.AddAllFields<SOShipment>(state => state.IsDisabled());
states.AddField<SOShipment.shipmentNbr>();
states.AddField<SOShipment.excludeFromIntercompanyProc>();
states.AddTable<SOShipLine>(state => state.IsDisabled());
states.AddTable<SOShipLineSplit>(state => state.IsDisabled());
states.AddTable<SOShipmentAddress>(state => state.IsDisabled());
states.AddTable<SOShipmentContact>(state => state.IsDisabled());
states.AddTable<SOOrderShipment>(state => state.IsDisabled());
}

}

 

Now, because we use an event handler, we can call the delivered object from another graph. Our custom delivery application will set the record as delivered from a driver handheld, which in turn, will set the same action on the shipment. Here is a code snippet that would accomplish this from the other screen:



public delegate void AfterSetDeliveredDelegate(HWCYHistoryDoc Doc);

[PXOverride]
public virtual void AfterSetDelivered(HWCYHistoryDoc Doc, AfterSetDeliveredDelegate del)
{
del?.Invoke(Doc);
SOShipmentEntry docgraph = PXGraph.CreateInstance<SOShipmentEntry>();
docgraph.Document.Current = docgraph.Document.Search<SOShipment.shipmentNbr>(Doc.ShipmentNbr);

HaunWelding.Delivery.SOShipmentExt.Events
.Select(e => e.ShipmentDelivered)
.FireOn(docgraph, docgraph.Document.Current);
docgraph.Save.Press();

}

This will load the attached shipment in a new graph instance, and fire the event. The workflow is then triggered, moving the order to the delivered status. 

Using the workflow handlers to handle status updates makes it easy to change statuses from other documents. It also allows us to add conditions to the status changes that will ensure every piece of code that may trigger a status update does bypass any new conditions added. It makes our modifications much more maintainable, as well as makes them easier to be interoperable with other modifications.

If you have any questions, please feel free to comment!

8 replies

Chris Hackett
Community Manager
Forum|alt.badge.img
  • Acumatica Community Manager
  • October 2, 2023

Thank you for sharing this information with the community @Keith Richardson!


Forum|alt.badge.img
  • Varsity I
  • April 10, 2025

Hi,
could you share the code for SOShipmentStatusExt


Keith Richardson
Semi-Pro I
Forum|alt.badge.img+2

Hi,
could you share the code for SOShipmentStatusExt


public class SOShipmentStatusExt
{


public const string Delivered = "D";
public class delivered : PX.Data.BQL.BqlString.Constant<delivered> { public delivered() : base(Delivered) {; } }


public const string ReadyForPickup = "P";
public class readyForPickup : PX.Data.BQL.BqlString.Constant<readyForPickup> { public readyForPickup() : base(ReadyForPickup) {; } }

}

 


Forum|alt.badge.img
  • Varsity I
  • April 10, 2025

So how did you change the list on the screen for status? like for the status list did you created a new list to add the attribute on the DAC field?


Keith Richardson
Semi-Pro I
Forum|alt.badge.img+2

So how did you change the list on the screen for status? like for the status list did you created a new list to add the attribute on the DAC field?



In the workflow, its defined on the field states in the workflow update listed above in SOShipmentEntryWorkFlowExt
 

context.UpdateScreenConfigurationFor(screen =>
{
return screen
.WithFieldStates(fieldstates => {
//add the delivered value
fieldstates.Add<SOShipment.status>(fieldstate => fieldstate.SetComboValue(SOShipmentStatusExt.Delivered, "Delivered"));
})

 


Forum|alt.badge.img
  • Varsity I
  • April 10, 2025

ohh thanks a lot. This saves me so much time.


Keith Richardson
Semi-Pro I
Forum|alt.badge.img+2

Note - for 2025r1 - you will need to add this customization to prevent the shipments tab on the sales order screen from throwing the error “The given key was not present in the dictionary”


 

using PX.Data;
using PX.Objects.PO;
using PX.Objects.SO;
using System;
using System.Reflection;

namespace HaunWelding.Delivery
{
[PXProtectedAccess]
//you need to put this attribute on, or it will show the display name as Cst_ on the grid
[PXUIField(DisplayName = "Status", Enabled = false)]
//abstract due to having abstract due to PXProtectedAccess
public abstract class SOOrderShipmentStatusAttachedToExt : PXGraphExtension<PX.Objects.SO.GraphExtensions.SOOrderEntryExt.Status, SOOrderEntry>
{
#region ProtectedAccess
[PXProtectedAccess(typeof(PX.Objects.SO.GraphExtensions.SOOrderEntryExt.Status))]
protected abstract PXFieldState DefaultState(PXCache sender, PXFieldSelectingEventArgs e);
[PXProtectedAccess(typeof(PX.Objects.SO.GraphExtensions.SOOrderEntryExt.Status))]
protected abstract PXFieldState AdjustByAttribute(PXFieldState state, PXUIFieldAttribute uiAttribute);
[PXProtectedAccess(typeof(PX.Objects.SO.GraphExtensions.SOOrderEntryExt.Status))]
protected abstract PXFieldState AdjustStateBySelf(PXFieldState state);
[PXProtectedAccess(typeof(PX.Objects.SO.GraphExtensions.SOOrderEntryExt.Status))]
protected abstract PXFieldState AdjustStateByRow(PXFieldState state, SOOrderShipment row);
#endregion


//these properties are getters only, no need to protected access.
protected PXUIFieldAttribute FieldAttribute => GetType().GetCustomAttribute<PXUIFieldAttribute>();
protected virtual bool SuppressValueSetting => false;
protected virtual String ValueForEmptyRow => default(String);

public override void Initialize()
{
// base method initialize code that we will not call, as I can't get it to remove that event with a proper signature
/* FieldName = GetType().Name.LastSegment('+');
base.Base.Caches<TTable>().Fields.Add(FieldName);
base.Base.FieldSelecting.AddHandler(typeof(TTable), FieldName, FieldSelecting);*/

//do not call initialize
//base.Initialize();

//call code from the base method that we need
Base.Caches<SOOrderShipment>().Fields.Add("Status");
base.Base.FieldSelecting.AddHandler(typeof(SOOrderShipment), "Status", FieldSelecting);
}

//copy of field selecting
public virtual void FieldSelecting(PXCache sender, PXFieldSelectingEventArgs e)
{
PXFieldState state = DefaultState(sender, e);
if (FieldAttribute != null)
{
state = AdjustByAttribute(state, FieldAttribute);
}

state = AdjustStateBySelf(state);
state = AdjustStateByRow(state, (SOOrderShipment)e.Row);
e.ReturnState = state;

if (!SuppressValueSetting)
{
e.ReturnValue = ((e.Row == null) ? ValueForEmptyRow : GetValue((SOOrderShipment)e.Row, sender));
}
}
//copy the base function for GetValue, as I cannot PXOverride it.
public virtual string GetValue(SOOrderShipment row, PXCache sender)
{
if (row.ShipmentType == SOShipmentType.DropShip)
{
using var receipt = PXDatabase.SelectSingle<POReceipt>(new PXDataField<POReceipt.status>(),
new PXDataFieldValue<POReceipt.receiptType>(PXDbType.Char, null,
row.Operation == SOOperation.Issue ? POReceiptType.POReceipt : POReceiptType.POReturn, PXComp.EQ),
new PXDataFieldValue<POReceipt.receiptNbr>(PXDbType.NVarChar, null, row.ShipmentNbr, PXComp.EQ));
if (receipt == null)
{
return null;
}
return (new POReceiptStatus.ListAttribute()).ValueLabelDic[receipt.GetString(0)];
}
else
{
using (new PXReadThroughArchivedScope())
{
using var shipment = PXDatabase.SelectSingle<SOShipment>(new PXDataField<SOShipment.status>(),
new PXDataFieldValue<SOShipment.shipmentType>(PXDbType.Char, null, row.ShipmentType, PXComp.EQ),
new PXDataFieldValue<SOShipment.shipmentNbr>(PXDbType.NVarChar, null, row.ShipmentNbr, PXComp.EQ));
if (shipment != null)
{
//changed to use my new list attribute
return (new SOShipmentStatusExt.ListAttribute()).ValueLabelDic[shipment.GetString(0)];
}
else
{
return PXMessages.LocalizeNoPrefix(PX.Objects.SO.Messages.AutoGenerated);
}
}
}
}
}
}

 


Chris Hackett
Community Manager
Forum|alt.badge.img
  • Acumatica Community Manager
  • July 17, 2025

Thank you for sharing this info with the community ​@Keith Richardson!