Restrict Labor Entry to Specific Resource Groups

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.

1 Like

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.

2 Likes

To make it flexible I would do this:

  1. define UD field on resource group table to store the security group ID. Fill it out for the resource groups which need this restriction.
  2. define the security groups for each resource group (ideally have same IDs as resource group IDs to keep it simple);
  3. populate the UD field for the resource groups which need this security enabled. The ones where everyone needs access , leave them empty;
  4. 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.

1 Like

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).

1 Like

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.

1 Like

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;



Capturef3ff3ss

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:


2 Likes

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.

2 Likes

I agree! That is what I am trying to figure out now.

1 Like

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.

2 Likes

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

2 Likes

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 :upside_down_face:. 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.

1 Like

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

1 Like

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.

1 Like

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!