@jon12
Here you go!
USE AT YOUR OWN RISK.
We cannot guarantee, and take no responsibility for, the quality and or functionality of the code shared below.
This has been working for us for a few years now.
There is more code here in this file than what I had recalled. I haven’t reviewed it to see if all of it is necessary to allow unfinishing configurations for in-progress or completed production orders.
There are some references to other files in the solution, but I don’t think it’s anything besides a check to see if this action is allowed for the current prod order type, and a few references to a central error messages file.
Please let me know if you need anything else.
using PX.Data;
using PX.Objects.AM;
using PX.Objects.AM.Attributes;
using PX.Objects.CS;
using PX.Objects.CR;
using SW.Common.CacheExtensions.AM;
using SW.Common.Descriptor;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Messages = PX.Objects.AM.Messages;
namespace PVBMFGCustomizations.Extensions.AM
{
public class SWConfigurationEntryExt : PXGraphExtension<ConfigurationEntry>
{
public static bool IsActive() => PXAccess.FeatureInstalled<FeaturesSet.manufacturing>();
#region Override Actions
public delegate IEnumerable finishDelegate(PXAdapter a);
[PXOverride]
public IEnumerable finish(PXAdapter a, finishDelegate baseMethod)
{
AMProdItem prodItem = Base.ProdItemRef.Select();
AMOrderType orderType = GetOrderType(prodItem?.OrderType);
if (orderType != null)
{
var orderTypeExt = orderType.GetExtension<SWAMOrderTypeExt>();
if (!orderTypeExt.UsrSWAllowChangeConfiguration.GetValueOrDefault())
{
return baseMethod(a);
}
else
{
return HandleFinishWithOrderType(orderTypeExt, prodItem, a);
}
}
return baseMethod(a);
}
#endregion
#region Event Handlers
protected virtual void _(Events.RowSelected<AMConfigurationResults> e, PXRowSelected baseHandler)
{
if (e.Row is AMConfigurationResults row)
{
var orderType = GetOrderType();
if (orderType != null)
{
HandleOrderType(orderType, e, baseHandler);
var isTestConfig = IsTestConfig(row);
UpdateUIFields(row, e, isTestConfig);
}
else
{
baseHandler?.Invoke(e.Cache, e.Args);
}
}
}
#region Service Methods
/// <summary>
/// Handles the completion or removal of configuration results based on certain validation checks.
/// Determines if the production item status and configuration results are valid. If there are errors,
/// this method returns immediately. If there are no errors, this method validates the document and either
/// marks the configuration result as completed or not depending on its current state.
/// </summary>
/// <param name="orderTypeExt">Instance of SWAMOrderTypeExt class.</param>
/// <param name="prodItem">Instance of AMProdItem class.</param>
/// <param name="adapter">Instance of PXAdapter class.</param>
/// <returns>An IEnumerable of the new configuration result and its related production bill of materials.</returns>
private IEnumerable HandleFinishWithOrderType(SWAMOrderTypeExt orderTypeExt, AMProdItem prodItem, PXAdapter adapter)
{
AMConfigurationResults results = Base.Results.Current;
if (!ValidateProdItemStatus(prodItem, results))
return adapter.Get();
if (!ValidateConfigResults(results))
return adapter.Get();
if (!ValidateProdClockEntry(prodItem))
return adapter.Get();
if (results.Completed != true)
{
bool documentValid = ValidateDocument(out string errorMessage);
if (!documentValid)
{
throw new PXException(errorMessage);
}
results.Completed = true;
}
else
{
ConfigSupplementalItemsHelper.RemoveSupplementalLineItems(Base, results);
results.Completed = false;
}
results = Base.Results.Update(results);
if (results != null && results.IsConfigurationTesting != true)
{
return SaveAndBuildProductionBom(results, prodItem, adapter);
}
return adapter.Get();
}
/// <summary>
/// Validates production clock entries for a given production item, ensuring no open clock entries exist for orders on hold or planned.
/// </summary>
/// <param name="prodItem">The production item to validate clock entries against.</param>
/// <returns>
/// Returns true if the validation passes, meaning no open clock entries were found for orders in inappropriate statuses.
/// </returns>
/// <exception cref="PXException">
/// Throws an exception if an open clock entry is found for an employee on a production order that is on hold or planned,
/// indicating that clock entries need to be closed before the production order can proceed.
/// </exception>
/// <remarks>
/// This method checks if the provided production item is either on hold or planned. If so, it retrieves all associated clock transactions.
/// It then checks each transaction for open entries (start time without an end time) and throws an exception with detailed information
/// about the employee and operation needing attention. This ensures production orders do not proceed with pending time-related discrepancies.
/// </remarks>
private bool ValidateProdClockEntry(AMProdItem prodItem)
{
if (prodItem.StatusID == ProductionOrderStatus.Hold || prodItem.StatusID == ProductionOrderStatus.Planned)
{
var amClockTrans = GetAMClockTrans(prodItem.OrderType, prodItem.ProdOrdID);
foreach (AMClockTran amClockTran in amClockTrans)
{
var bAccount = GetBAccount(amClockTran.EmployeeID);
var amProdOper = GetAMProdOper(amClockTran.OrderType, amClockTran.ProdOrdID, amClockTran.OperationID);
if (amClockTran.StartTime != null && amClockTran.EndTime == null)
{
throw new PXException(PXMessages.LocalizeFormat(SWMessages.Manufacturing.ClockInOutValidation, bAccount.AcctCD.Trim(), amProdOper.OperationCD));
}
if (amClockTran.EndTime != null)
{
throw new PXException(SWMessages.Manufacturing.HoldAndInProcessValidation);
}
}
}
return true;
}
/// <summary>
/// Determines if the AMProdItem instance is null or if the ProductionOrderStatus is Released. If the ProductionOrderStatus is Released, a PXException is thrown.
/// If the ProductionOrderStatus is InProcess, Completed, or Closed and the Completed property of the AMConfigurationResults instance is set to true,
/// displays a message box. If the user cancels this prompt via the No button, the method returns false. If cancelled via the Yes button or other statuses of the
/// ProductionOrderStatus property, method returns true.
/// </summary>
/// <param name="prodItem">The AMProdItem instance.</param>
/// <param name="results">The current AMConfigurationResults instance.</param>
/// <returns>Returns a boolean value indicating if there were no errors during validation.</returns>
private bool ValidateProdItemStatus(AMProdItem prodItem, AMConfigurationResults results)
{
var isValid = true;
if (prodItem == null) return isValid;
string statusID = prodItem.StatusID;
if (statusID == ProductionOrderStatus.Released)
{
throw new PXException(PXMessages.Localize(SWMessages.Manufacturing.ProdOrderStatusPlanned));
}
else if ((statusID == ProductionOrderStatus.InProcess || statusID == ProductionOrderStatus.Completed || statusID == ProductionOrderStatus.Closed) && results.Completed == true)
{
if (Base.Results.Ask(SWMessages.Manufacturing.Warning, SWMessages.Manufacturing.ProdOrderStatusInProgress, MessageButtons.YesNo, MessageIcon.Warning) == WebDialogResult.No)
{
isValid = false;
return isValid;
}
}
return isValid;
}
private bool ValidateConfigResults(AMConfigurationResults results)
{
if (results == null /*|| results.Closed.GetValueOrDefault()*/)
{
return false;
}
return true;
}
/// <summary>
/// Saves the current state of the calling graph and builds a production bill of materials (BOM) for a given production item if it exists.
/// If the current configuration filter's `ShowCanTestPersist` flag is set to true, the function simply returns the result of the `Save` action without building the production BOM.
/// Otherwise, if a valid production item is provided, the function creates an instance of the `ProdMaint` graph, updates the production item to enable BOM building,
/// sets the `ItemConfiguration` for the new graph to the provided configuration results, and saves the changes.
/// </summary>
/// <param name="results">The configuration results for the production item.</param>
/// <param name="prodItem">The production item for which the production BOM should be built.</param>
/// <param name="adapter">The adapter for the calling action.</param>
/// <returns>Returns the result of the `Save` action of the calling graph.</returns>
private IEnumerable SaveAndBuildProductionBom(AMConfigurationResults results, AMProdItem prodItem, PXAdapter adapter)
{
var retSave = Base.Save.Press(adapter);
if (Base.ConfigFilter?.Current?.ShowCanTestPersist == true)
{
return retSave;
}
if (prodItem != null)
{
var prodGraph = PXGraph.CreateInstance<ProdMaint>();
prodItem.BuildProductionBom = true;
prodGraph.ProdMaintRecords.Current = prodGraph.ProdMaintRecords.Update(prodItem);
prodGraph.ItemConfiguration.Current = prodGraph.ItemConfiguration.Select();
prodGraph.ItemConfiguration.Current = results;
prodGraph.Actions.PressSave();
}
return retSave;
}
/// <summary>
/// Invokes the base row selected event for the configuration result when the UsrSWAllowChangeConfiguration
/// field of the AMOrderType extension is either null or false, thus allowing the UI event to be raised.
/// </summary>
/// <param name="orderType">Instance of the AMOrderType object</param>
/// <param name="e">Instance of the AMConfigurationResults' row selected event</param>
/// <param name="baseHandler">Delegate row selected event handler</param>
private void HandleOrderType(AMOrderType orderType, Events.RowSelected<AMConfigurationResults> e, PXRowSelected baseHandler)
{
var orderTypeExt = orderType.GetExtension<SWAMOrderTypeExt>();
if (orderTypeExt.UsrSWAllowChangeConfiguration == null || orderTypeExt.UsrSWAllowChangeConfiguration == false)
{
baseHandler?.Invoke(e.Cache, e.Args);
}
}
private bool IsTestConfig(AMConfigurationResults row)
{
return row.IsConfigurationTesting.GetValueOrDefault() || Base.ConfigFilter?.Current?.ShowCanTestPersist == true;
}
/// <summary>
/// Updates the user interface fields of the AMConfigurationResults class based on the state of the instance and isTestConfig parameter.
/// </summary>
/// <param name="row">Instance of the AMConfigurationResults class</param>
/// <param name="e">Instance of the AMConfigurationResults' row selected event</param>
/// <param name="isTestConfig">
/// A boolean value determining if the UI fields should be set for testing or production.
/// </param>
private void UpdateUIFields(AMConfigurationResults row, Events.RowSelected<AMConfigurationResults> e, bool isTestConfig)
{
var notClosed = true; // !row.Closed.GetValueOrDefault();
var notCompleted = !row.Completed.GetValueOrDefault();
var canFinish = true;
// All header records not allowed for update/insert in UI
PXUIFieldAttribute.SetEnabled(e.Cache, row, false);
SetFieldAttributes<AMConfigurationResults.customerID>(e.Cache, row, isTestConfig);
SetFieldAttributes<AMConfigurationResults.isConfigurationTesting>(e.Cache, row, isTestConfig);
SetFieldAttributes<AMConfigurationResults.siteID>(e.Cache, row, isTestConfig);
PXUIFieldAttribute.SetVisible<AMConfigResultsOption.inventoryID>(Base.Options.Cache, null, isTestConfig);
PXUIFieldAttribute.SetVisible<AMConfigResultsOption.subItemID>(Base.Options.Cache, null, isTestConfig);
Base.Options.AllowUpdate = Base.Results.AllowUpdate = Base.Attributes.AllowUpdate = notClosed && notCompleted && canFinish;
Base.Finish.SetEnabled(notClosed && canFinish);
Base.Finish.SetCaption(row.Completed != true ? Messages.Finish : Messages.Unfinish);
Base.Save.SetEnabled(!row.IsConfigurationTesting.GetValueOrDefault());
Base.Cancel.SetEnabled(!isTestConfig);
Base.SaveClose.SetCaption(!isTestConfig ? Messages.SaveAndClose : Messages.CloseTesting);
Base.ShowAll.SetCaption(Base.OptionsSelectFilter.Current.ShowAll.GetValueOrDefault() ? PX.Objects.CA.Messages.HideTran : Messages.ShowAll);
}
/// <summary>
/// Sets the PXUIFieldAttribute.Enabled and PXUIFieldAttribute.Visible UI attributes on the
/// field based on the isTestConfig parameter passed.
/// </summary>
/// <typeparam name="TField">The type of IBqlField that inherits from `IBqlField` interface.</typeparam>
/// <param name="cache">Instance of `PXCache` class.</param>
/// <param name="row">Instance of `AMConfigurationResults` class.</param>
/// <param name="isTestConfig">Boolean value determining if the field UI attributes should be accessible.</param>
private void SetFieldAttributes<TField>(PXCache cache, AMConfigurationResults row, bool isTestConfig) where TField : IBqlField
{
PXUIFieldAttribute.SetEnabled<TField>(cache, row, isTestConfig);
PXUIFieldAttribute.SetVisible<TField>(cache, row, isTestConfig);
}
#endregion
/// <summary>
/// Determines the validity of the current configuration document.
///
/// <para>This method takes an output parameter <paramref name="errorMessage"/> that is passed out as a string and holds any error messages, if any.</para>
/// </summary>
/// <param name="errorMessage">If validation fails, this parameter holds the aggregated error messages as a string.</param>
/// <returns>True if configuration document is valid, otherwise False.</returns>
public bool ValidateDocument(out string errorMessage)
{
bool isValid = true;
List<string> errorMessages = new List<string>();
foreach (PXResult<AMConfigResultsFeature> result in Base.Results.ResultFeatures.Select())
{
AMConfigResultsFeature feature = result;
isValid &= ValidateFeatureOptionAndAddErrors(feature, errorMessages);
}
foreach (PXResult<AMConfigResultsAttribute> result in Base.Results.ResultAttributes.Select())
{
AMConfigResultsAttribute attribute = result;
isValid &= ValidateAttributeAndAddErrors(attribute, errorMessages);
}
errorMessage = string.Join(", ", errorMessages);
return isValid;
}
/// <summary>
/// Validates the <paramref name="feature"/> option and adds any errors to the specified <paramref name="errorMessages"/> list.
///
/// <para>The method returns True indicating whether or not the supplied <paramref name="feature"/> is valid.</para>
/// </summary>
/// <param name="feature">The feature option to validate.</param>
/// <param name="errorMessages">List of errors to add to.</param>
/// <returns>A Boolean value indicating whether or not the <paramref name="feature"/> option is valid.</returns>
private bool ValidateFeatureOptionAndAddErrors(AMConfigResultsFeature feature, List<string> errorMessages)
{
bool isValid = IsFeatureOptionValid(feature, out string featureError);
if (!string.IsNullOrEmpty(featureError))
{
errorMessages.Add(featureError);
}
return isValid;
}
/// <summary>
/// Validates the given <paramref name="attribute"/> whether it is valid or not and adds any received error messages to the specified <paramref name="errorMessages"/> list.
///
/// <para>The method returns True indicating whether or not the supplied <paramref name="attribute"/> is valid.</para>
/// </summary>
/// <param name="attribute">The attribute to validate.</param>
/// <param name="errorMessages">List of errors to add to.</param>
/// <returns>A Boolean value indicating whether or not the supplied <paramref name="attribute"/> is valid.</returns>
private bool ValidateAttributeAndAddErrors(AMConfigResultsAttribute attribute, List<string> errorMessages)
{
bool isValid = Base.Results.ValidateAttribute(attribute, out string attributeError);
if (!string.IsNullOrEmpty(attributeError))
{
errorMessages.Add(attributeError);
}
return isValid;
}
/// <summary>
/// Determines if a selected options set in <paramref name="feature"/> is valid or not, and updates any existing error message to <paramref name="errorMessage"/>.
/// The valid options are then added to <paramref name="errorMessage"/> when <paramref name="aggregateOptionErrorMessages"/> is true.
/// </summary>
/// <param name="feature">The feature with result options set to validate.</param>
/// <param name="errorMessage">The output error message, if any. If there is no error, this value is null.</param>
/// <param name="aggregateOptionErrorMessages">Set to true to aggregate all possible errors to the output <paramref name="errorMessage"/>.</param>
/// <returns>True is returned if the selection are valid, otherwise false.</returns>
public bool IsFeatureOptionValid(AMConfigResultsFeature feature, out string errorMessage, bool aggregateOptionErrorMessages = true)
{
bool isValid = true;
List<string> errorMessages = new List<string>();
isValid &= Base.Results.ValidateFeature(feature, out errorMessage);
AggregateErrorMessage(errorMessage, errorMessages);
int selectedCount = Base.Results.ResultOptions
.Select(Base.Results.Current.ConfigResultsID, feature.FeatureLineNbr)
.RowCast<AMConfigResultsOption>()
.Count(opt => opt.Included == true);
if (feature.MinSelection.HasValue && feature.MinSelection > selectedCount)
{
errorMessages.Add(Messages.GetLocal("The selection ({0}) must be greater than the minimum selection ({1}).", selectedCount, feature.MinSelection));
isValid = false;
}
else if (feature.MaxSelection.HasValue && feature.MaxSelection < selectedCount)
{
errorMessages.Add(Messages.GetLocal("The selection ({0}) must be smaller than the maximum selection ({1}).", selectedCount, feature.MaxSelection));
isValid = false;
}
foreach (AMConfigResultsOption option in Base.Results.ResultOptions.Select(Base.Results.Current.ConfigResultsID, feature.FeatureLineNbr).RowCast<AMConfigResultsOption>())
{
isValid &= Base.Results.ValidateOption(option, out errorMessage);
if (aggregateOptionErrorMessages)
{
AggregateErrorMessage(errorMessage, errorMessages);
}
}
errorMessage = string.Join(" / ", errorMessages);
return isValid;
}
/// <summary>
/// Aggregates the <paramref name="errorMessage"/> to the <paramref name="errorList"/> list and resets it to an empty string.
/// </summary>
/// <param name="errorMessage">The error message to aggregate.</param>
/// <param name="errorList">The list to add the error message to.</param>
public void AggregateErrorMessage(string errorMessage, List<string> errorList)
{
if (!string.IsNullOrEmpty(errorMessage))
{
errorList.Add(errorMessage);
errorMessage = string.Empty;
}
}
#endregion
#region ServiceQueries
/// <summary>
/// Retrieves the order type instance from the AMOrderType table based on the AMProdItem OrderType value.
/// </summary>
/// <returns>The AMOrderType object that matches the AMProdItem OrderType value, or null if it doesn't exist.</returns>
private AMOrderType GetOrderType()
{
AMProdItem prodItem = Base.ProdItemRef.Select();
return PXSelect<AMOrderType, Where<AMOrderType.orderType, Equal<Required<AMOrderType.orderType>>>>.Select(Base, prodItem?.OrderType);
}
private AMOrderType GetOrderType(string orderType)
{
return PXSelect<AMOrderType, Where<AMOrderType.orderType, Equal<Required<AMOrderType.orderType>>>>.Select(Base, orderType);
}
private IEnumerable<AMClockTran> GetAMClockTrans(string orderType, string prodOrdID)
{
return PXSelect<
AMClockTran,
Where<AMClockTran.orderType, Equal<Required<AMClockTran.orderType>>,
And<AMClockTran.prodOrdID, Equal<Required<AMClockTran.prodOrdID>>>>>
.Select(Base, orderType, prodOrdID)
.RowCast<AMClockTran>();
}
private BAccount GetBAccount(int? bAccountID)
{
return PXSelect<BAccount, Where<BAccount.bAccountID, Equal<Required<BAccount.bAccountID>>>>.Select(Base, bAccountID);
}
private AMProdOper GetAMProdOper(string orderType, string prodOrdID, int? operationID)
{
return PXSelect<
AMProdOper,
Where<AMProdOper.orderType, Equal<Required<AMProdOper.orderType>>,
And<AMProdOper.prodOrdID, Equal<Required<AMProdOper.prodOrdID>>,
And<AMProdOper.operationID, Equal<Required<AMProdOper.operationID>>>>>>
.Select(Base, orderType, prodOrdID, operationID);
}
#endregion
}
}
Enjoy!