We have an issue with labor entry, where users accidentally select the wrong operation. This gets corrected later in the day by their manager. I was thinking that we should be able to, for example, set all of our lathe users to only be able to enter time against lathe resource groups. What is the best way to accomplish this? I don’t think our employees are set to resource groups only departments.
Well, there are a gajillion ways to do this badly, mostly because as soon as you start locking down who can perform what operation, somebody needs to come in on Saturday and just get everything done… but they can’t login to the op.
That said, there needs to be SOME way for Kinetic to know who is authorized to perform what operations. Since you can only have one Resource Group on an Employee record, that could well be too limiting if your shop operators (even only some of them) are cross-trained.
I’ve added UD fields to the Employee record, you can use a whole UD table, you could probably use Security Groups (haven’t tried that but it might work)… anything that you could use in a BPM to allow/disallow an activity.
But honestly, I’ve never seen it work well enough that it was used long-term.
To make it flexible I would do this:
- define UD field on resource group table to store the security group ID. Fill it out for the resource groups which need this restriction.
- define the security groups for each resource group (ideally have same IDs as resource group IDs to keep it simple);
- populate the UD field for the resource groups which need this security enabled. The ones where everyone needs access , leave them empty;
- make a data directive for the update to throw an error if :
a. user does not belong the the security group stored on UD field on the resource group and
b. UD field on resource group is not empty
This way if the UD field is empty, everyone will be able to update or user belongs to the specified security group.
Will your users correct it if you just throw them a warning? In a pre-processing Labor.Update BPM, you could pop up a dataform for ttLaborDtl added rows / RowMod == "A"
if the operation resource group doesn’t match the employee resource group. Then give them the option to continue (if they’re doing work outside their primary group), or cancel and select a different operation (if they’ve just selected the wrong op).
These are all good ideas. I think I want to make it a bit more flexible and allow the operator to log time against any operation where they are in the same department as the operator. If they are not in the same department then I want to just pop a warning to the user, but still allow them to continue. I think this would catch the times where they pick the wrong op.
I am starting with a function. I think that would be the easiest way to compare the departments for the operation and the employee. If I setup the function to take in the Employee ID, how can I quickly return the department ID? I think I would also need another function to return the department ID for the specific operation. I think this is based on the operation’s resource group. So I would have to pass in the job, assembly seq, and operation seq to return the department.
Does this seem like a good approach? Is there an easier way I can just make one function to compare the two departments? It always seems easier to me to do it in small steps.
I used this code in my function:
string employeeDept = string.Empty;
string operationDept = string.Empty;
var employeeRecord = (from emp in Db.EmpBasic
where emp.EmpID == EmpID
select emp).FirstOrDefault();
if (employeeRecord != null)
{
employeeDept = employeeRecord.JCDept;
EmpDept = employeeDept;
}
else
{
DeptMatch = false;
}
var operationRecord = (from op in Db.JobOpDtl
where op.JobNum == JobNum
&& op.AssemblySeq == AsmSeq
&& op.OprSeq == OprSeq
select op).FirstOrDefault();
if (operationRecord == null)
{
DeptMatch = false;
}
var capabilityRecord = (from cap in Db.Capability
where cap.CapabilityID == operationRecord.CapabilityID
select cap).FirstOrDefault();
if (capabilityRecord == null)
{
DeptMatch = false;
}
var resourceGroupRecord = (from rg in Db.ResourceGroup
where rg.ResourceGrpID == capabilityRecord.PrimaryResourceGrpID
select rg).FirstOrDefault();
if (resourceGroupRecord != null)
{
operationDept = resourceGroupRecord.JCDept;
OprDept = operationDept;
}
else
{
DeptMatch = false;
}
DeptMatch = employeeDept == operationDept;

This function seems to work well enough. The only problem is that when the departments do not match, the labor entry still proceeds. I think I am missing something in the method directive:
That looks good to me. I like that you’re doing all the checking work in the function. The only thing I’m concerned about is that there’s no “Cancel” option. Your BPM is on the Update method, so if you show the message that it isn’t a match, they can’t go back and correct it, it’s already updating.
I agree! That is what I am trying to figure out now.
I would just replace the “Show Message” with a BPM Data Form. Use the same text from the message and add “Continue anyway?” If they select no, throw an error that just says “Labor Transaction Cancelled” or something like that.
I am trying to tack on a little bit of code to return the department description instead of the department ID, so that I can feed that back out to the user. Instead of saying youre in dept 11 and you tried to log into dept 5. I want to tell them the actual department descriptions. I tried to use this, which should work, but I keep getting the error below:
var actualDeptRecord = (from department in Db.JCDept
where department.JCDept == operationDept
select department).FirstOrDefault();
if (actualDeptRecord != null)
{
OprDept = actualDeptRecord.Description;
}
else
{
OprDept = "Unknown Department"; // Handle cases where the department is not found
}
actualDeptRecord = (from department in Db.JCDept
where department.JCDept == employeeDept
select department).FirstOrDefault();
if (actualDeptRecord != null)
{
EmpDept = actualDeptRecord.Description;
}
else
{
EmpDept = "Unknown Department"; // Handle cases where the department is not found
}
Compile Error:
‘JCDept’ does not contain a definition for ‘JCDept’ and no accessible extension method ‘JCDept’ accepting a first argument of type ‘JCDept’ could be found (are you missing a using directive or an assembly reference?)
I added JCDept to the table list in the function. Why can’t it find JCDept.JCDept? Do I need to reference it in another way? I am using the following usings:
using System.Linq;
using Erp.Tables;
Thanks!
I see it is related to this post: Linq bpm get JCDept.JCDept Description - Epicor ERP 10 - Epicor User Help Forum
Changing to JCDept1 is all it took for a solution! Woo!!!
Here is my working method and function:
string employeeDept = string.Empty;
string operationDept = string.Empty;
// Retrieve employee department
var employeeRecord = (from emp in Db.EmpBasic
where emp.EmpID == EmpID
select emp).FirstOrDefault();
if (employeeRecord != null)
{
employeeDept = employeeRecord.JCDept;
}
else
{
DeptMatch = false;
return; // Exit early if employee is not found
}
// Retrieve operation record
var operationRecord = (from op in Db.JobOpDtl
where op.JobNum == JobNum
&& op.AssemblySeq == AsmSeq
&& op.OprSeq == OprSeq
select op).FirstOrDefault();
if (operationRecord == null)
{
DeptMatch = false;
return; // Exit early if operation is not found
}
// Check if the operation links directly to a ResourceGroup
if (!string.IsNullOrEmpty(operationRecord.ResourceGrpID))
{
// Retrieve department directly from ResourceGroup
var resourceGroupDirect = (from rg in Db.ResourceGroup
where rg.ResourceGrpID == operationRecord.ResourceGrpID
select rg).FirstOrDefault();
if (resourceGroupDirect != null)
{
operationDept = resourceGroupDirect.JCDept;
}
else
{
DeptMatch = false;
return; // Exit early if direct ResourceGroup is not found
}
}
else
{
// Check through Capability and ResourceGroup
var capabilityRecord = (from cap in Db.Capability
where cap.CapabilityID == operationRecord.CapabilityID
select cap).FirstOrDefault();
if (capabilityRecord == null)
{
DeptMatch = false;
return; // Exit early if capability is not found
}
var resourceGroupFromCap = (from rg in Db.ResourceGroup
where rg.ResourceGrpID == capabilityRecord.PrimaryResourceGrpID
select rg).FirstOrDefault();
if (resourceGroupFromCap != null)
{
operationDept = resourceGroupFromCap.JCDept;
}
else
{
DeptMatch = false;
return; // Exit early if ResourceGroup from Capability is not found
}
}
// Compare departments
DeptMatch = employeeDept == operationDept;
var actualDeptRecord = (from department in Db.JCDept
where department.JCDept1 == operationDept
select department).FirstOrDefault();
if (actualDeptRecord != null)
{
OprDept = actualDeptRecord.Description;
}
else
{
OprDept = "Unknown Department"; // Handle cases where the department is not found
}
actualDeptRecord = (from department in Db.JCDept
where department.JCDept1 == employeeDept
select department).FirstOrDefault();
if (actualDeptRecord != null)
{
EmpDept = actualDeptRecord.Description;
}
else
{
EmpDept = "Unknown Department"; // Handle cases where the department is not found
}
This is great! When user submits labor entry, if the departments match, then nothing happens, the labor entry goes in as expected. If the departments don’t match, then user gets a message showing the two departments, then gets a BPM form with OK/Cancel, I would like to find a way to have only one popup.
Can I get my department strings from my function to show inside my BMP data form? Then I could get rid of the extra message box showing the two departments, and just put it all in the BPM form.
Thanks!
Nate
I know there is a way to do it, but I just went to look at the BPM Form Designer and I’m not seeing what I remember. Maybe I haven’t actually done this in Kinetic yet . I’m not even seeing a “test form” action like they had in classic.
Now I’m wondering if I had a customization that replaced the Form Text with callContextBpmData.Character20 or something. I know I’ve done it before, dangit.
In case you ever run into it, Company.Company also needs to be Company.Company1 or you’ll hit the same thing.
I am heading down that road. In My Labor.Update Pre-processing method directive, I added a Set BPM Data Field. I used Char20, and set the value to the string that I want. If I immediately do a Show Message using callcontext char 20, I can see the value is correct. But as soon as I get into my customization on the BPM data form, callcontext seems to be empty. Is there a trick to getting callcontext to carry over form the method divertive into the BPM data form that was launched form the directive?
Thanks again!!!
EDIT: So I can pass in callcontext char 20 to the BPM Data Form, but so far, only by adding a Form Field with the Field = BPMData.Character20. This makes a text box, with the value I had previously set in my method. However once I added this field, my buttons were no longer visible. It is good to see that the data can be accessed via the form, now to just get it in the right spot, with the buttons!
That was the trick I needed! This code in my customization in my BPM works perfectly! Thanks @kve!
private void IP_ConfirmLabor_Load(object sender, EventArgs args)
{
// Add Event Handler Code
myText = (Ice.Lib.Framework.EpiLabel)csm.GetNativeControlReference("c9e52ffc-3bc3-4864-b723-f7bb545d586f");
EpiDataView edvText = (EpiDataView)(oTrans.EpiDataViews["BPMData"]);
string MyText = edvText.dataView[0]["Character20"].ToString();
myText.Text = MyText;
myText.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // Left-align the text
}
Submitted an idea to make this a feature instead of a customization:
Add Options to Validate Labor Entry Against | Epicor Ideas Portal
Well poop… It worked great from Labor Entry, but from MES, the window just disappears when I click Start Production Activity using the wrong operation. How can I keep the start production activity window open if the user clicks cancel to change the operation?
It at least cancels the selection, so no job is actually started, forcing the user to click start activity again and enter it correctly. I guess this is ok, but I would rather keep the window open if possible.
Hey Nate,
As a last bit of polish I’d recommend adding company to the where clause in your db queries.
In the end, I have two method directives. One handles labor entry from the main client application, using Labor.SubmitForApproval pre-processing. The other handles labor entry from the MES side using Labor.DefaultOprSeq post-processing. This seems to work great!