How to add operation to MRP created job selectively based on criteria

,

I’m brainstorming how to accomplish something. I’ve posted similar questions on the past. Never got the correct fit solution though. Open to all ideas:

We want to back up material for certain jobs that are required to begin other jobs (but not materials within those other jobs). Scenario: we have a sales order with 5 lines. 1 of the lines is to do a factory installation using the other 4 lines. That factory installation might take 3 weeks. So the other lines need to plan so that they are completed/ready 5 weeks (3 weeks install + 2 weeks to stage material) before they are due to ship (ship by date). We use the Ship By date very extensively to measure what’s due to ship in any given time period so it is not open to move the Ship By date to be 5 weeks earlier than actual ship by date. We backwards schedule. We are multi-site.

Receive time only works by part/site level, not per job. We also have a need that the same part will need extra padding for 1 or 2 installs as the kinks are worked out and then brought down to a consistent number. We also use the same parts sometimes for truck upfitting and other times they are used in kits that would not require this extra buffer. So I am looking at something other than receive time.

Adjusting the JobReqDate is also a possibility but it requires me to firm the job in order to change it. We run MRP nightly so without firming it, it reverts back to the original date when MRP wipes it out and re-creates it. We don’t like firming jobs more than a few weeks out so that it allows MRP to keep all materials up to date with the methods of manufacture. So I don’t think this works for us either.

Queue time or move time was also considered but they are applied at the resource level but the times required will vary between jobs. So, it’s not practical to use queue or move time (unless maybe they can be controlled on the job op detail? I don’t remember seeing this but it might be worth checking in to).

My solution currently is to add an operation (opr seq 999) that is a “planning buffer” and is tied to an infinte resource that has 0 cost. Then I can add hours into this operation that will force all previous operations to be completed and thus started sooner. This appears to give me the best granularity but also requires a fairly complex BPM to achieve this. I only want it to be added to jobs that have a make to order demand link that also have at least 1 line on their sales order with a certain product group. I believe this is do-able. But I wanted to solicit input to see if there’s an easier way or if not, any tips/advice for programmatically adding an operation to a job. (Things like method vs data directive, which field changing to indicate it’s done… thinking JobEngineered?, and any other gotchas or ideas you might’ve worked through already)

I don’t really see an alternative to your solution. This will require some custom coding to work, and jobs can be finicky to update, especially if you don’t allow changes on engineered jobs.

Good news, is that I’ve already done the hard part of figuring out the basics of job edits for you. This post is intended for use in a mass-edit function. The specific snippet will wipe a job’s MOM/BOM and re-pull latest rev (never actually used it since it was easier just to tell people to stop firming jobs months ahead of when we actually needed to start work), but you can clip that part out and add a method call for adding your op.

For your use, you may not need to go down the mass edit route. You could trigger it based on changes to the demand link. Without the loop requirement, you could probably even use the widgets if you’re so inclined.

1 Like

Thanks, John. That’s really helpful. We do allow changes on engineered jobs, thankfully. I think that simplifies the process quite a bit. What I ended up doing was using JobEngineered changes to true on a data directive. Then I check for the right kind of sales order, make sure the job is unfirm (I will leave firm jobs alone just like MRP will), and then check to see if my planning buffer operation already exists. If all of this happens and it is still needed, I was successfully able to add the operation there in the data directive in custom code and I have it in a foreach so that it could handle mass job updates but I have not tried to test that. I was assuming it would be doing them one at a time when MRP runs. But I’m guessing there is a mass job update to unengineer/re-engineer… I don’t know that we even use that though because our jobs are engineered when created. I still have more testing to do but so far it passed the order job wizard test.

Well, the plot thickens. Order Job Wizard creates the demand link right after the job is created… so when it’s engineered, I have that information available. However, MRP does not. It seems to engineer the job before the demand link exists so MRP is not creating my planning buffer operation. I may have to go the demand link route as you suggested above.

It’s not that bad…it’s like 20 extra lines of boilerplate-ish code to un/re-engineer a job.

I do not believe it is batch-engineering jobs after creation. It is running though jobs sequentially, and each job is touched once.

I vaguely recall that MRP just creates a fully engineered JobHead record and skips the unengineered step entirely. It might even create the “engineered” JobHead row a moment before actually pulling details. Which means you can be left with an unfirm, engineered job with no details. It’s been a while since I’ve broken MRP badly enough to actually see this, so my memory may be faulty.

In other words, if you’re triggering on a change to JobHead.JobEngineered, it might fail. The field doesn’t change because MRP cheats.

1 Like

this works during MRP run to create Job Ops. DataDirective JobHead In Transaction.

{
    var ttJobHead_xRow = ttJobHead.FirstOrDefault();
    if (ttJobHead_xRow != null &&
        (ttJobHead_xRow.CommentText.StartsWith("WS") || 
        ttJobHead_xRow.CommentText.StartsWith("PA") || 
        ttJobHead_xRow.CommentText.StartsWith("CA") || 
        ttJobHead_xRow.CommentText.StartsWith("BB") || 
        ttJobHead_xRow.CommentText.StartsWith("VA") || 
        ttJobHead_xRow.CommentText.StartsWith("PS") || 
        ttJobHead_xRow.CommentText.StartsWith("FS") || 
        ttJobHead_xRow.CommentText.StartsWith("CS") || 
        ttJobHead_xRow.CommentText.StartsWith("MS") || 
        ttJobHead_xRow.CommentText.StartsWith("PC")))
    {

        //Only run this BPM and insert operations if existing rev doesn't have operations, cuz MRP will cause an error
        var ttPartOpr = Db.PartOpr.Where(x => x.Company == ttJobHead_xRow.Company && 
                                                x.PartNum == ttJobHead_xRow.PartNum && 
                                                x.RevisionNum == ttJobHead_xRow.RevisionNum).FirstOrDefault();
        if (ttPartOpr == null)
        {
            //delete existing operations first 
            foreach (var ttJobOper in Db.JobOper.Where(x => x.Company == ttJobHead_xRow.Company &&  x.JobNum == ttJobHead_xRow.JobNum))
            {
                if (ttJobOper != null)
                {
                Db.JobOper.Delete(ttJobOper);
                }
            }

            //delete existing JobOpDtl first 
            foreach (var ttJobOpDtl in Db.JobOpDtl.Where(x => x.Company == ttJobHead_xRow.Company && x.JobNum == ttJobHead_xRow.JobNum))
            {
                if (ttJobOpDtl != null)
                {
                Db.JobOpDtl.Delete(ttJobOpDtl);
                }
            }
    
            if (ttJobHead_xRow.CommentText.StartsWith("MS"))
            {
                
                JobOper newRow = new JobOper();
                Db.JobOper.Insert(newRow);
                newRow.Company = Session.CompanyID;
                newRow.JobNum = ttJobHead_xRow.JobNum;
                newRow.AssemblySeq = 0;
                newRow.OpCode = "MSPROG";
                newRow.OpDesc = "Programming (MS)";
                newRow.OprSeq = 10;
                newRow.ProdStandard = 0;
                newRow.EstProdHours = 0;
                newRow.PrimaryProdOpDtl = 10;
                newRow.PrimarySetupOpDtl = 10;
                newRow.StdFormat = "HR";
                newRow.SetupGroup = "DEFAULT";
                newRow.StdBasis = "E";
                newRow.IUM = "EA";
                newRow.RunQty = ttJobHead_xRow.ProdQty;
                
                JobOper newRow2 = new JobOper();
                Db.JobOper.Insert(newRow2);
                newRow2.Company = Session.CompanyID;
                newRow2.JobNum = ttJobHead_xRow.JobNum;
                newRow2.AssemblySeq = 0;
                newRow2.OpCode = "MSCUT";
                newRow2.OpDesc = "Cutting (MS)";
                newRow2.OprSeq = 20;
                newRow2.ProdStandard = 0;
                newRow2.EstProdHours = 0;
                newRow2.PrimaryProdOpDtl = 10;
                newRow2.PrimarySetupOpDtl = 10;
                newRow2.StdFormat = "HR";
                newRow2.SetupGroup = "DEFAULT";
                newRow2.StdBasis = "E";
                newRow2.IUM = "EA";
                newRow2.RunQty = ttJobHead_xRow.ProdQty;
                
                JobOper newRow3 = new JobOper();
                Db.JobOper.Insert(newRow3);
                newRow3.Company = Session.CompanyID;
                newRow3.JobNum = ttJobHead_xRow.JobNum;
                newRow3.AssemblySeq = 0;
                newRow3.OpCode = "MACH";
                newRow3.OpDesc = "Machining / Deburring";
                newRow3.OprSeq = 30;
                newRow3.ProdStandard = 120;
                newRow3.EstProdHours = 120;
                newRow3.PrimaryProdOpDtl = 10;
                newRow3.PrimarySetupOpDtl = 10;
                newRow3.StdFormat = "HR";
                newRow3.SetupGroup = "DEFAULT";
                newRow3.StdBasis = "E";
                newRow3.IUM = "EA";
                newRow3.RunQty = ttJobHead_xRow.ProdQty;
                
                JobOper newRow4 = new JobOper();
                Db.JobOper.Insert(newRow4);
                newRow4.Company = Session.CompanyID;
                newRow4.JobNum = ttJobHead_xRow.JobNum;
                newRow4.AssemblySeq = 0;
                newRow4.OpCode = "INVREC";
                newRow4.OpDesc = "Inventory Receipt";
                newRow4.OprSeq = 50;
                newRow4.ProdStandard = 0;
                newRow4.EstProdHours = 0;
                newRow4.PrimaryProdOpDtl = 10;
                newRow4.PrimarySetupOpDtl = 10;
                newRow4.StdFormat = "HR";
                newRow4.SetupGroup = "DEFAULT";
                newRow4.StdBasis = "E";
                newRow4.IUM = "EA";
                newRow4.RunQty = ttJobHead_xRow.ProdQty;
                
                JobOpDtl JobOpDtl_row = new JobOpDtl();
                Db.JobOpDtl.Insert(JobOpDtl_row);
                JobOpDtl_row.Company = Session.CompanyID;
                JobOpDtl_row.JobNum = ttJobHead_xRow.JobNum;
                JobOpDtl_row.AssemblySeq = 0;
                JobOpDtl_row.OpDtlSeq = 10;
                JobOpDtl_row.OprSeq = 10;
                JobOpDtl_row.ResourceGrpID = "MACHP";
                JobOpDtl_row.OpDtlDesc = "Machine Shop Programmers";
                JobOpDtl_row.SetupOrProd = "B";
                
                JobOpDtl JobOpDtl_row2 = new JobOpDtl();
                Db.JobOpDtl.Insert(JobOpDtl_row2);
                JobOpDtl_row2.Company = Session.CompanyID;
                JobOpDtl_row2.JobNum = ttJobHead_xRow.JobNum;
                JobOpDtl_row2.AssemblySeq = 0;
                JobOpDtl_row2.OpDtlSeq = 10;
                JobOpDtl_row2.OprSeq = 20;
                JobOpDtl_row2.ResourceGrpID = "MACHO";
                JobOpDtl_row2.OpDtlDesc = "Machine Shop Operators";
                JobOpDtl_row2.SetupOrProd = "B";
                
                JobOpDtl JobOpDtl_row3 = new JobOpDtl();
                Db.JobOpDtl.Insert(JobOpDtl_row3);
                JobOpDtl_row3.Company = Session.CompanyID;
                JobOpDtl_row3.JobNum = ttJobHead_xRow.JobNum;
                JobOpDtl_row3.AssemblySeq = 0;
                JobOpDtl_row3.OpDtlSeq = 10;
                JobOpDtl_row3.OprSeq = 30;
                JobOpDtl_row3.ResourceGrpID = "MACHO";
                JobOpDtl_row3.OpDtlDesc = "Machine Shop Operators";
                JobOpDtl_row3.SetupOrProd = "B";
                
                JobOpDtl JobOpDtl_row4 = new JobOpDtl();
                Db.JobOpDtl.Insert(JobOpDtl_row4);
                JobOpDtl_row4.Company = Session.CompanyID;
                JobOpDtl_row4.JobNum = ttJobHead_xRow.JobNum;
                JobOpDtl_row4.AssemblySeq = 0;
                JobOpDtl_row4.OpDtlSeq = 10;
                JobOpDtl_row4.OprSeq = 50;
                JobOpDtl_row4.ResourceGrpID = "INV";
                JobOpDtl_row4.OpDtlDesc = "Inventory Receipt";
                JobOpDtl_row4.SetupOrProd = "B";
                
            }
        }
    }

    Db.Validate();
    txScope.Complete();
}```
1 Like

your approach is what I would have suggested several years ago. The only other option would be to trigger your BPM on FIRMING the job.
But, one other option to consider is using the new Part Revision option that was added in the past few releases. you can now stock parts at various revisions. So you could have a revision A, and a revision A999. with this second A999 you could have the BOM be exactly the same but with the extra operation. When ordering the parts on the sales order, you would specify the A999 revision, and this would auto include the extra operation.

This same approach could also be possibly accomplished by creating an ALTERNATE revision which could be triggered with a BPM on get details. But this might be tricky with MRP.

1 Like

What is the reason you avoided using the BOs/methods when structuring your operations? It all makes sense logically… and I thank you for sharing.

Alas, we’re still on 2021.2. I did think about using an alternate method for the same revision… I wonder if that might be easier to use the BPM to force an alternate method than to try to add in an operation during a very specific step of the job creation (before scheduling but after the details are gotten).

The previous sys admin wrote that during the initial rollout of MRP years ago, and then promptly left. When I took over I had my hands full of fixing everything else. It’s one of those “If it’s not totally broken don’t fix it” type of things.
This is on my list of things to update because it’s too rigid. I want to use a MOM and the system like it’s supposed to be used (crazy I know). I’ve been avoiding it because I don’t enjoy pain. But it works for now.

1 Like

So I want to be up front with this. I don’t like the idea of directly editing SQL to add my operation. But I tested it and it seemed like it was working OK. But then, I started testing by running MRP and something is definitely not working right. Even though I have a condition that checks to make sure the Op Detail doesn’t already exist, it is still somehow trying to create it more than once resulting in an SQL exception error, which causes the job to fail during MRP. OK, here’s the weird part. It succeeds and works properly on the first job for a given part number. So I thought about it and I figure it is because MRP goes part by part and generates all the jobs it needs for each part number before moving on to the next part number. OK - but that doesn’t explain why the first job would succeed and the subsequent jobs after that would fail. This totally supports my desire not to edit SQL directly. However, I’ve not found any combination of BPMs/functions that allow me to place the operation onto the job before it gets scheduled. In fact, it really doesn’t like me doing anything because the job is unfirm! I even messed with some logic to firm the job, add the op, then unfirm the job… it seems to allow you to do this in UpdateExt. But, alas, I couldn’t get any of those to work. The method @cpilinko showed in here does appear to work except for the goofy business that I started this post with. Naturally, when I was initially proving out the concept, I was laser focused on one job and was very pleased to find that the job was reliably adding in my special planning op and op detail BEFORE the job was sent to the scheduling engine - which led to an appropriate backed up start date.

So now I need help. I will paste my logic in the next post so you can see what I’m doing. I don’t have a ton of comments in it but hopefully you get the gist.

I am running an In-Transaction DD on JobProd.
It’s all contained in one custom code widget. Here it is:

foreach (var eachProd in (from jp in ttJobProd
                          where jp.RowMod == "A"
                          select jp))
{
  using(var txScope = IceContext.CreateDefaultTransactionScope()) 
  {
  
    string company = eachProd.Company;
    string jobNum = eachProd.JobNum;
    int orderNum = eachProd.OrderNum;
    
    var jobHeadInfo = (from jh in Db.JobHead
                        where jh.Company == company
                        && jh.JobNum == jobNum
                        select jh).FirstOrDefault();
                        
    if (jobHeadInfo != null)
    {
      bool jobFirm = jobHeadInfo.JobFirm;
      decimal runQty = jobHeadInfo.ProdQty;
                 
      if (!jobFirm)
      {
        if (orderNum > 0)
        {
          bool truckUpfitOrder = (from od in Db.OrderDtl
                                  where od.Company == company
                                  && od.OrderNum == orderNum
                                  && (od.ProdCode == "FI" || od.ProdCode == "BODEQUIP")
                                  select od).Any();
          
          if (truckUpfitOrder)
          {
            bool bufferOpExists = (from jo in Db.JobOper
                                    where jo.Company == company
                                    && jo.JobNum == jobNum
                                    && jo.AssemblySeq == 0
                                    && jo.OpCode == "PLANBUFF"
                                    select jo).Any();
                        
            if (!bufferOpExists)
            {
              JobOper newJobOper = new JobOper();
              newJobOper.Company = company;
              newJobOper.JobNum = jobNum;
              newJobOper.AssemblySeq = 0;
              newJobOper.OpCode = "PLANBUFF";
              newJobOper.OpDesc = "Planning Buffer";
              newJobOper.OprSeq = 999;
              newJobOper.ProdStandard = 120;
              newJobOper.EstProdHours = 120;
              newJobOper.PrimaryProdOpDtl = 10;
              newJobOper.PrimarySetupOpDtl = 10;
              newJobOper.StdFormat = "HP";
              newJobOper.SetupGroup = "DEFAULT";
              newJobOper.StdBasis = "E";
              newJobOper.IUM = "EA";
              newJobOper.RunQty = runQty;
              Db.JobOper.Insert(newJobOper);
              
              bool bufferOpDtlExists = (from jo in Db.JobOpDtl
                                    where jo.Company == company
                                    && jo.JobNum == jobNum
                                    && jo.AssemblySeq == 0
                                    && jo.OprSeq == 999
                                    && jo.OpDtlSeq == 10
                                    select jo).Any();
                                    
              if (!bufferOpDtlExists)
              {
                JobOpDtl newJobOpDtl = new JobOpDtl();
                newJobOpDtl.Company = company;
                newJobOpDtl.JobNum = jobNum;
                newJobOpDtl.AssemblySeq = 0;
                newJobOpDtl.OpDtlSeq = 10;
                newJobOpDtl.OprSeq = 999;
                newJobOpDtl.ResourceGrpID = "PLANBUFF";
                newJobOpDtl.OpDtlDesc = "Planning Buffer";
                newJobOpDtl.SetupOrProd = "B";
                Db.JobOpDtl.Insert(newJobOpDtl);
              }

              Db.Validate();
              txScope.Complete();
            }
          }
        }
      }
    }
  }
}

I am only looking for added JobProd rows. I check to make sure the job is unfirm. Then I check to make sure it’s make to order because the next check requires me to review the lines on the sales order and only move forward if the sales order has 1 of 2 very specific product groups on it. After all of that, I check to make sure the operation doesn’t already exist before inseritng a new row to the JobOper table. I then do the same check on the JobOpDtl table and there-in lies my problem. I am getting an SQLException error that looks like this:

System.Data.SqlClient.SqlException (0x80131904): Cannot insert duplicate key row in object ‘Erp.JobOpDtl’ with unique index ‘IX_JobOpDtl’. The duplicate key value is (VANAIR, 3851628-2-1, 0, 999, 10).

Breadcrumbs keep coming. I should’ve mentioned this in the above. The job fails but the one thing it does do is add the Opr 999 without the OpDtl. But that’s all my job has. And it doesn’t schedule itself. So I commented out the entire part where I add the JobOpDtl because I thought maybe there was some logic picking up the operation and automatically setting up the JobOpDtl before I was inserting the row… but now I’m getting the same SQL Exception but for JobOper instead and it appears like I’m trying to add a duplicate JobOper. But, as you can see in my code above, I have a check in place to make sure the JobOper table does not have an operation already before I add it. So there must be some sorcery that MRP uses to create this stuff and it’s not behaving like I’d expect as far as the TTs and BOs are concerned and the Data Directives must be doing something goofy. Again, the whole point is to add the operation to an already existing unfirm job but before it hits the scheduling engine (which appears to happen as soon as it gets the details).

Your biggest issue is trying to insert everything via the DB. That’s bad practice unto itself. It’s also doing things the hard way, since the built-in BO’s should handle a fair amount of the work for you.

1 Like

100% agree. I’m pivoting to another direction. Currently trying to set the ReqDueDate on JobHead since I can more easily do that within the same DD and not involve any other tables other than a lookup to JobProd to get my order.

The issue is trying to do everything in the Data Directive.

DD should be a stub that triggers a function. The function is where you should be doing the heavy lifting. The function side has access to whatever tables and BO’s you need.

3 Likes