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

  • 2 October 2023
  • 1 reply
  • 186 views

Userlevel 4
Badge +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!


1 reply

Userlevel 7
Badge

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

Reply


About Acumatica ERP system
Acumatica Cloud ERP provides the best business management solution for transforming your company to thrive in the new digital economy. Built on a future-proof platform with open architecture for rapid integrations, scalability, and ease of use, Acumatica delivers unparalleled value to small and midmarket organizations. Connected Business. Delivered.
© 2008 — 2024  Acumatica, Inc. All rights reserved