Skip to main content
Question

Correction of Product Configurations

  • September 8, 2022
  • 13 replies
  • 433 views

Freeman Helmuth
Semi-Pro I
Forum|alt.badge.img

Hi,

Looking for input on how to correct a product configuration and resulting production order that is in progress or completed.

Scenario: A production order is created and partially completed, when it is decided that there needs to be a small change to the configuration. We manufacture portable storage buildings (pineviewbuildings.com) and in our case, this might be something as simple as changing the paint color due to a customer request or inventory needs changing midstream. 

The more common scenario is that a production employee made a mistake in the construction and now the configuration needs to be corrected to match the physical unit.

 

Currently, there is no option modify the product configuration in any way after the production order has been released. The closest we can come is creating a new production order that consumes the finished good from the first production order, create the correct configuration result and then manually modify the materials. This, as you can imagine is very cumbersome.

Is there any way around this issue?

13 replies

stephenhennelly
Jr Varsity II

@Freeman Helmuth, I have in the past with customers just had them update the production order details with the correct information (place on hold then modify) or the system also allows you to issue material not previously listed on the production order. This shows on the details with a qty required of zero but with a qty actual. This is useful as you can tell what was not originally on the order. I would just issue the correct color/material and receive back the incorrect item or delete from the details. 

Unfortunately when the production order is processed, the configuration is locked but for good reason. This is because the configuration could update something that you have issued/consumed and we would not want that. So this is why its typically easier to just update the production order to reflect the change and leave the configuration as is. If the order is just released, set back to plan and update. Otherwise you will most likely need to do the above. 

Hope this helps. 


Freeman Helmuth
Semi-Pro I
Forum|alt.badge.img

@stephenhennelly Thanks for the reply. I’m already aware of how to change the materials, that’s not the issue. The problem I have is the configuration.

All of our individual products are unique and therefore require configuration. We need a single place where we have a “source of truth” about a product configuration and the plan has been to use the configuration attached to the production order, so we have to have a way to modify it.


jdobish
Pro III
Forum|alt.badge.img+2
  • Pro III
  • September 16, 2022

Would it be possible to do a negative move on the inventory and put the correct inventory item into the system? I am not sure how that would work with the configuration, but might allow you to get the data that you need. 


Chris Hackett
Community Manager
Forum|alt.badge.img
  • Acumatica Community Manager
  • November 2, 2022

Hi @Freeman Helmuth - were you able to find a solution? Thank you!


Freeman Helmuth
Semi-Pro I
Forum|alt.badge.img

@Chris Hackett 

No we weren’t able to find a good solution.

We’re going to customize the product configurator to allow modifications after production has started. If a user does this, the customization will skip updating materials on the production order and the user will be warned that they need to manually update the materials.


  • Freshman I
  • November 21, 2023

@Freeman Helmuth how has this solution worked for you?  We implemented in August (we build fuel trailers - base products with varying options that we use configurations to build the order).  I am having what I believe is a similar issue - we build some units for stock, so they are configured and completed to inventory, and may require some slight modifications to sell.  In some cases it is add something, in other cases it is remove something, or both (e.g. remove a 35’ reel and replace w/ a 50’ reel).

 


Freeman Helmuth
Semi-Pro I
Forum|alt.badge.img

Working well. However we are planning to make some more modifications in the near future to completely recalculate the materials for the production order based on the new configuration, and then post that adjustment to inventory. This is still a far future project for us so we have nothing to share at the moment.

The override to allow configuration modifications after production completion is actually very simple.


Forum|alt.badge.img

@Freeman Helmuth Have you completed the planned modifications ? Is it working fine? I am basically looking for a similar modification. Scenario is like this; Sales Order created with configured item along with a unique key. Production order created, released and at least one move operation completed. At this point I would like to modify the configuration and attach the new key to the production order. Is this something that you have already achieved?


Freeman Helmuth
Semi-Pro I
Forum|alt.badge.img

No we haven’t. Just not enough value for us yet.

We have the process to change the config itself but to be clear, the original config is changed. There’s not a new one created.


Freeman Helmuth
Semi-Pro I
Forum|alt.badge.img

...sorry wrong thread-mod please delete


  • Freshman I
  • April 4, 2026

The override to allow configuration modifications after production completion is actually very simple.

 

Do you happen to have a link you can share that describes how to create this override? We often have to make configuration changes after an item is in production. These changes rarely involve a material change but, like you, we need to maintain a single-source of truth for configurations in the Sales Order because we get a lot of re-orders from customers. 

 

Thank you!


Freeman Helmuth
Semi-Pro I
Forum|alt.badge.img

The override to allow configuration modifications after production completion is actually very simple.

 

Do you happen to have a link you can share that describes how to create this override? We often have to make configuration changes after an item is in production. These changes rarely involve a material change but, like you, we need to maintain a single-source of truth for configurations in the Sales Order because we get a lot of re-orders from customers. 

 

Thank you!

Yes we can share that. Will post soon.


MichaelShirk
Captain II
Forum|alt.badge.img+6

@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!