UOM bugs

I tried to report a bug, and the devs decided that something else we depend on is a bug instead, and threatened to “fix” it. Sorry if your UOM conversions suddenly stop working.

What I noticed
If you add your part-specific UOMs to classes first, then create parts, Epicor creates a PartUOM for every part-specific UOM in the new part’s UOM class. When the conversion is used for that part, Epicor marks the PartUOM used and does not mark the UOMConv used. I believe this is the normal, correct behavior.

If you add a part-specific UOM to the class after adding parts, Epicor does not create PartUOMs for existing parts in the class. This is a known issue that people have proposed various solutions for. If the conversion is used, Epicor still does not create the PartUOM, and marks the UOMConv used instead. This leads to several issues. The worst is that Epicor still lets you edit the conversion factor in the Part screen. Only then does it finally create the missing PartUOM, making it appear that past transactions used the wrong factor.

What I suggested
These four rules would fix the bugs and make the UOM system work consistently. Some of them can be implemented as customizations, but not all of them.

  • If UOMConv.HasBeenUsed is true, it should not be possible to set UOMConv.PartSpecific to true.
  • If UOMConv.PartSpecific is true, the system should never set UOMConv.HasBeenUsed to true. If the PartUOM is missing, it should be created.
  • If PartUOMs exist and any PartUOM.HasBeenUsed is true, it should not be possible to set UOMConv.PartSpecific to false.
  • If PartUOMs exist but all PartUOM.HasBeenUsed are false, then when UOMConv.PartSpecific is changed from true to false, the related PartUOMs should be deleted.

Response
Everything is Working As Intended™. Among the developers’ claims about how the UOM system currently works was that you cannot have a part-specific UOM in a class that is not type Other. When I pointed out that this is false, they said that’s the bug they’re going to fix!

Kevin, a bug to some is food. I have also had an experience like this. Sorry you are in this position.

1 Like

Support still insists that everything is Working As Intended™.

All the bugs I’ve identified can be fixed or prevented with a single, relatively simple in-transaction data directive on UOMConv. Add references to Erp.Contracts.BO.Part and Erp.Contracts.BO.UOMClass.

I just finished testing each of the 9 scenarios identified in the comment, and this DD successfully prevents all of them. Item 8 actually prevents you from setting up a test of item 7. This is a Good Thing! But I had some Parts that were already in an inconsistent state that allowed me to fully test 7. Ideally you’d want to create this DD before creating any UOMs or Parts. But if your system is already corrupted, this should at least keep it from getting any worse.

/*
1. [bug fix] Prevent user from deleting UOMConv referenced by Part.
2. [bug fix] Prevent user from deleting UOMConv if related PartUOM is marked used.
3. [bug fix] Prevent user from unchecking PartSpecific if related PartUOM is marked used.
4. [bug fix] Delete related PartUOMs if deleting UOMConv is allowed.
5. [bug fix] Delete related PartUOMs if unchecking PartSpecific is allowed.
6. [bug fix] Prevent user from checking PartSpecific if UOMConv is marked used.
7. [bug fix] Prevent Epicor from marking part-specific UOMConv used when PartUOM is missing.
8. [best practice] Prevent user from creating a part-specific UOMConv with a non-zero factor.
9. [best practice] Prevent user from creating a part-specific UOMConv in a built-in class.
*/
foreach(var row in ttUOMConv.Where(c => c.Added() || c.Updated()))
{
	/* 6 and 7. Could differentiate error messages, but the result would be the same. */
	if(row.HasBeenUsed && row.PartSpecific)
	{
		throw new Ice.BLException("HasBeenUsed and PartSpecific are mutually exclusive.");
	}
	/* 8 and indirectly 7. */
	if(row.PartSpecific && row.ConvFactor != 0)
	{
		throw new Ice.BLException("PartSpecific requires a ConvFactor of zero.");
	}
}

/* Identify updated rows where PartSpecific changed. */
var psChanged = ttUOMConv.Where(up => up.Updated() && ttUOMConv.Single(un => un.Unchanged() && un.SysRowID == up.SysRowID).PartSpecific != up.PartSpecific);

foreach(var row in ttUOMConv.Where(c => c.Added() && c.PartSpecific).Union(psChanged.Where(o => o.PartSpecific)))
{
	CallService<Erp.Contracts.UOMClassSvcContract>(svc =>
	{
		var ds = svc.GetByID(row.UOMClassID);
		/* 9. */
		if(ds.UOMClass.Single().ClassType != "Other")
		{
			throw new Ice.BLException("PartSpecific only allowed in class type Other.");
		}
	});
}

foreach(var row in ttUOMConv.Where(c => c.Deleted()).Union(psChanged.Where(o => !o.PartSpecific)))
{
	CallService<Erp.Contracts.PartSvcContract>(svc =>
	{
		/* Page through Parts to minimize memory footprint. Requires validation pass and update pass. */
		var pageSize = 10;
		var absolutePage = 0;
		var morePages = false;
		var partUomsExist = false;
		
		do
		{
			var ds = svc.GetRows(
				$"UOMClassID = '{row.UOMClassID}'", /* Part */
				"0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1",
				$"UOMCode = '{row.UOMCode}'", /* PartUOM */
				"0=1", "0=1",
				pageSize,
				++absolutePage,
				out morePages
			);
			/* 1. */
			if(row.Deleted() && ds.Part.Any(o => o.IUM == row.UOMCode || o.PUM == row.UOMCode || o.SalesUM == row.UOMCode))
			{
				throw new Ice.BLException("Cannot delete UOMConv referenced by Part.");
			}
			/* 2 and 3. */
			if(ds.PartUOM.Any(o => o.HasBeenUsed))
			{
				var action = row.Deleted() ? "delete UOMConv" : "uncheck PartSpecific";
				throw new Ice.BLException($"Cannot {action} after related PartUOM HasBeenUsed.");
			}
			partUomsExist |= ds.PartUOM.Any();
		}
		while(morePages);
		
		/* Update pass could race with invalidating changes to PartUOMs. */
		if(partUomsExist)
		{
			absolutePage = 0;
			do
			{
				var ds = svc.GetRows(
					$"UOMClassID = '{row.UOMClassID}'", /* Part */
					"0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1", "0=1",
					$"UOMCode = '{row.UOMCode}'", /* PartUOM */
					"0=1", "0=1",
					pageSize,
					++absolutePage,
					out morePages
				);
				if(ds.PartUOM.Any())
				{
					/* 4 and 5. */
					foreach(var pu in ds.PartUOM)
					{
						pu.RowMod = "D"; 
					}
					svc.Update(ref ds);
				}
			}
			while(morePages);
		}
	});
}