We have been asked to develop a way to stop employees from clocking into job operations if a prior operation is not marked complete. I was thinking on creating it using a bpm, but am unsure where to start. Any tips would be greatly appreciated.
I am no pro with method directives, but I think you are looking at setting up a method directive. I would start by targeting this method: Erp.Proxy.BO.LaborImpl.DefaultOprSeq()
If you can tap into this method, say in the pre-processing, then I think you could do your check to see if the previous op has been completed. I have a BAQ that looks at the job number and operation that you give it, then it pulls out the previous op and tells you if it has been completed. Take a look and use what you can from it!
Good luck!
FindPrevOp.baq (30.4 KB)
@dgreenEA pre processing on Labor.Update is where I stop operators from doing a start activity. Mine is to make sure you are in the right resource group for the operation, but the same process.
(ttLaborDtl_Row.Added() || ttLaborDtl_Row.Updated()) && ttLaborDtl_Row.ActiveTrans == true && ttLaborDtl_Row.LaborType == "P"))
read JobOper for JobNum, AssemblySeq, equal to ttlaborDtl and OprSeq < ttlaborDtl.OprSeq
OrderBy JobNum, AssemblySeq ascending and OprSeq descending
with FirstOrDefault this will get you the previous Operation.
You only need the OpComplete to come back or you could add OpComplete and use .Any to set a bool.
Hi, maybe not the best help, but it would be a starting point, you can take a glance on this, this allows employee to clock in but does not allow to report greater QTY than has been done on previous operation.
It is built on Labor.Update PreProcessing
int OpBefore = 0; //Operation before dataset
int OpBeforeOp = 0; //OpSeq before dataset
int OpBeforeAsembly = 0;//AssemblySeq before dataset
decimal OpBeforeQty = 0;//QtyCompleted before dataset
string OpBeforeOpCode = ""; //OpCode before dataset
decimal PrevQty = 0; //Previous quantity of current operation before dataset
decimal ReportQty =0;//Current ds qty+laborqty before dataset
decimal TempQty = 0;//dataset quantity sum
foreach(var dsRow in (from dsRows in ds.LaborDtl where dsRows.Company == "XXX" && (dsRows.RowMod == IceRow.ROWSTATE_ADDED || dsRows.RowMod == IceRow.ROWSTATE_UPDATED) && dsRows.LaborType == "P" && dsRows.TimeStatus == "E" && dsRows.ActiveTrans == true select dsRows)) //Loop thrue dataset
{
var List = (from rows in Db.JobOper.With(LockHint.NoLock) where rows.Company == dsRow.Company && rows.JobNum == dsRow.JobNum && rows.AssemblySeq == dsRow.AssemblySeq && rows.OprSeq < dsRow.OprSeq select rows.OprSeq).ToList(); //get previous OprSeq or 0
OpBefore = List.LastOrDefault();
foreach(var row in Db.JobOper.With(LockHint.NoLock).Where(a=>a.Company == dsRow.Company && a.JobNum == dsRow.JobNum && a.OprSeq == OpBefore)) //Get op before data
{
OpBeforeOp = row.OprSeq;
OpBeforeAsembly = row.AssemblySeq;
OpBeforeQty = row.QtyCompleted;
OpBeforeOpCode = row.OpCode;
}
foreach(var row in Db.LaborDtl.With(LockHint.NoLock).Where(r=>r.Company == dsRow.Company && r.JobNum == dsRow.JobNum && r.AssemblySeq == dsRow.AssemblySeq && r.OprSeq == dsRow.OprSeq && r.OpCode == dsRow.OpCode && r.ActiveTrans != true)) //Get current operation sum of LaborQty
{
PrevQty = PrevQty + row.LaborQty;
}
TempQty = TempQty + dsRow.LaborQty; //Get sum of dataset qty's
ReportQty = TempQty + PrevQty; //get current report qty
if(OpBeforeOp != 0 ) //If previous operations exist
{
if(ReportQty > OpBeforeQty )
{
throw new Ice.Common.BusinessObjectException(new Ice.Common.BusinessObjectMessage("(BPM:Op_Qty_Check) " + "You can not report this qty becouse: " + "Previous Op " + OpBeforeOpCode + " reported qty is: " + Convert.ToInt32(OpBeforeQty) + " and total QTY on this Op is: " + Convert.ToInt32(ReportQty))
{
Type = Ice.Common.BusinessObjectMessageType.Error,
});
}
}
}
Hi
A post-processing method directive on Labor.DefaultOprSeq should do it with the following custom code:
Erp.Tables.JobOper JobOper;
// Find active record
foreach (var ttLaborDtl_i in (from ttLaborDtl_Row in ttLaborDtl
select ttLaborDtl_Row))
{
var ttLaborDtlRow = ttLaborDtl_i;
// Select associated Job Operation record
JobOper = (from JobOper_Row in Db.JobOper
where ttLaborDtlRow.Company == JobOper_Row.Company
&& ttLaborDtlRow.JobNum == JobOper_Row.JobNum
&& ttLaborDtlRow.AssemblySeq == JobOper_Row.AssemblySeq
&& ttLaborDtlRow.OprSeq == JobOper_Row.OprSeq
//&& ttLaborDtlRow.OpComplete == JobOper_Row.OpComplete
select JobOper_Row).FirstOrDefault();
var PrevOp = (from JobOper_Row in Db.JobOper
where JobOper_Row.OprSeq
< ttLaborDtlRow.OprSeq && JobOper.OpComplete == false
select JobOper_Row);
if (PrevOp != null)
{
CallContext.Current.ExceptionManager.AddBLException("Previous operation not complete");
}
}
One thing to keep in mind: when you do this on the post-processing of Labor.DefaultOprSeq as Sue points out, you get the message earlier in the process - meaning when the user changes the Operation Sequence and tabs out. If you catch it on the Update method, you’ve allowed the user to fill out the entire screen before you’re giving them the error. I prefer the DefaultOprSeq method - consider it a bit more user friendly.
If you use both Start/End production activity as well as Report Quantity, make sure you test both programs. I think the method directives fire in both, so shouldn’t be an issue. But you definitely don’t want to assume.
Kevin Simon
Adding / improving to Sue’s code.
// Iterate over all labor details
foreach (var ttLaborDtlRow in ds.LaborDtl)
{
// Retrieve all JobOper records related to the current labor detail
var jobOperations = (from jobOper in Db.JobOper
where jobOper.Company == ttLaborDtlRow.Company
&& jobOper.JobNum == ttLaborDtlRow.JobNum
&& jobOper.AssemblySeq == ttLaborDtlRow.AssemblySeq
select jobOper).ToList();
// Find the current operation
var currentOperation = jobOperations
.FirstOrDefault(jobOper => jobOper.OprSeq == ttLaborDtlRow.OprSeq);
if (currentOperation == null)
{
continue; // If the current operation is not found, skip to the next labor detail
}
// Find the incomplete previous operation, if any
var incompletePreviousOperation = jobOperations
.Where(jobOper => jobOper.OprSeq < currentOperation.OprSeq && !jobOper.OpComplete)
.OrderByDescending(jobOper => jobOper.OprSeq)
.FirstOrDefault();
if (incompletePreviousOperation != null)
{
// Construct a detailed error message
string errorMessage = $"You're attempting to start operation {currentOperation.OprSeq} " +
$"but the previous operation {incompletePreviousOperation.OprSeq} " +
$"is not complete.";
CallContext.Current.ExceptionManager.AddBLException(errorMessage);
}
}
- Combined database queries to retrieve all relevant job operations at once, reducing multiple database calls.
- Used
Where
andOrderByDescending
to efficiently identify the most recent incomplete previous operation. - Constructed a detailed error message including operation sequence numbers to provide more context:
"You're attempting to start operation {currentOperation.OprSeq} but the previous operation {incompletePreviousOperation.OprSeq} is not complete."
- Skipped processing for labor details with no matching operation, ensuring the loop continues efficiently.
- Enhanced code readability by using meaningful variable names and adding comments.
Using the same method as Sue: Labor.DefaultOprSeq but it no longer uses tt
but rather ds
now.
Have the managers tell their employees to stop doing it?
There is a shop warning for this you can display to the operator that will warn them when they try and start an operation in MES. You can also have that warning display in Shop Tracker for review, or notify the manager in a different way.
If only.
Employees would have to listen.
Managers would have to have teeth.
Upper management should grow a backbone…
I’m thinking of adding a shock plate to all 3 for negative reinforcement.
I’m sure we could add some positive as well, maybe a donut dispenser.
In any company, if these concepts are actually laughable, Epicor will never be able to solve that problem, no matter how many BPMs you create.
Epicor is a tool we can use to great effect to help people do their jobs. I personally think when we try and use it to make people do their jobs, we are heading in the wrong direction. I believe understanding the distinction is worth thinking about, and when a request comes in we should at least try and ascertain which one of these things we are setting out to do.
I agree with you in principle. However sometimes it’s just not enough, so you take their excuses away as a last resort. Then you can truly throw your hands up and say, I’ve done all that I can.
We actually hijacked this warning in a post-processing BPM on Labor method DefaultOprSeq and made it raise an exception instead. And it plays nicely with Ops set to either Start-to-Start or Finish-to-Finish, where if the preceding operation has not been started then the message the user receives says that a previous operation has not been started, rather that it saying that the preceding operation has not been completed.