BPM on JobOper

Hi everyone, I am trying to write a simple standard data directive on the JobOper table. I want to be able to trap jobs that are created with either a zero qtyPer or a zero prodStd. The exceptions being if the operations are ‘Issue’, ‘SubDes’ or ‘Despatch’, as I expect these operations to have zero in the prodStd. The BPM then sends an email with the jobs that need correcting. My problem is that the BPM is not ignoring the three operations I want it to and emails me every job created. What am I doing wrong here?
Thanks for your help.
Adrian.

E10-Help-1

if i were you, i will create a pre-process method BPM on releasing Jobs, this way you will capture every job before hitting the shop floor, i have done many BPM’s on this BO method as well as EWB, the reason for that is that Epicor create jobs in a massive scale when running the MRP process in my environment, so i decided to capture these jobs at this point/transaction to allow only jobs with times, approved Supplier ID, sub.con. days out values to go to the shop floor, i can share any with you and you can alter to suit your use if you want

Thanks for your reply Al, we have not been Live for a year yet. I used to spend all my time writing the configurators. I am now trying to iron out some of the problems in production. We have quite a few! I’m ok but far from experienced with BPMs so any help / advice you can give would be much appreciated.

2019-02-13_0753

The HasNoStdCost? is a Custom Condition which returns true / false

/* Check JobAsmbl and JobMtl MINUS REFD */
var ttJobHead_Row = ttJobHead.FirstOrDefault();

/* JobAsmbls with No PartCost StdCost and Class not REFD */
bool hasJobAsmblsWithNoStdCost =
	(from ja in Db.JobAsmbl.With(LockHint.NoLock)

		/* Join on PartNum to get ClassID */
		join p in Db.Part.With(LockHint.NoLock)
				on new { ja.Company, ja.PartNum } equals new { p.Company, p.PartNum }

		join pl in Db.Plant.With(LockHint.NoLock)
				on new { ja.Company, PL = ja.Plant } equals new { pl.Company, PL = pl.Plant1 }

		join pc in Db.PartCost.With(LockHint.NoLock)
				on new { pl.Company, CID = pl.PlantCostID, PartNum = ja.PartNum } equals new { pc.Company, CID = pc.CostID, PartNum = pc.PartNum }

		where
			(( ja.Company == ttJobHead_Row.Company && ja.JobNum == ttJobHead_Row.JobNum )) &&
			(p.ClassID != "REFD") && 
			(pc.StdLaborCost + pc.StdBurdenCost + pc.StdMaterialCost + pc.StdSubContCost + pc.StdMtlBurCost) == 0

		select ja
		).Any();


/* JobMtls with No PartCost StdCost and Class not REFD */
bool hasJobMtlsWithNoStdCost =
	(from jm in Db.JobMtl.With(LockHint.NoLock)

		/* Join on PartNum to get ClassID */
		join p in Db.Part.With(LockHint.NoLock)
				on new { jm.Company, jm.PartNum } equals new { p.Company, p.PartNum }

		join pl in Db.Plant.With(LockHint.NoLock)
				on new { jm.Company, PL = jm.Plant } equals new { pl.Company, PL = pl.Plant1 }

		join pc in Db.PartCost.With(LockHint.NoLock)
				on new { pl.Company, CID = pl.PlantCostID, PartNum = jm.PartNum } equals new { pc.Company, CID = pc.CostID, PartNum = pc.PartNum }

		where
			(( jm.Company == ttJobHead_Row.Company && jm.JobNum == ttJobHead_Row.JobNum )) &&
			(p.ClassID != "REFD") && 
			(pc.StdLaborCost + pc.StdBurdenCost + pc.StdMaterialCost + pc.StdSubContCost + pc.StdMtlBurCost) == 0

		select jm
		).Any();


return hasJobAsmblsWithNoStdCost || hasJobMtlsWithNoStdCost;
1 Like

no problem mate, this is my BPM to check setup and production time for each operation on JobOper except our MATCHECK, the BPM will display a message to worn user then set JobReleased check box back to false, i have designed it this way to avoid cater for Firm, Engineered/ and UnEng transactions then run the update method, you can alter it to send you an Email instead if you want,

the code as text:

Erp.Tables.JobHead JobHead;
Erp.Tables.JobOper JobOper;
string sAllOpFlag = string.Empty;

var ttJobHead_xRow = (from ttJobHead_Row in ttJobHead
								where ttJobHead_Row.Company == Session.CompanyID
								&& ttJobHead_Row.RowMod == "U"
select ttJobHead_Row).FirstOrDefault();
if (ttJobHead_xRow != null)
{
	foreach (var JobOper_iterator in (from JobOper_Row in Db.JobOper
					where JobOper_Row.Company == Session.CompanyID
					&& JobOper_Row.JobNum == ttJobHead_xRow.JobNum
          && JobOper_Row.SubContract == false
					&& JobOper_Row.EstSetHours == 0.00m
					&& JobOper_Row.EstProdHours == 0.00m
					&& JobOper_Row.OpCode != "MATCHECK"
	select JobOper_Row))
	 	
	if (JobOper_iterator != null)
	{
	sAllOpFlag ="Op. Seq. No."+JobOper_iterator.OprSeq+" : "+JobOper_iterator.OpCode+"_"+sAllOpFlag; 
	
	}
}
if (sAllOpFlag != "")
{
		PublishInfoMessage("The following Operations: " + sAllOpFlag + " have no Estimated Setup nor Production Time", Ice.Common.BusinessObjectMessageType.Warning, Ice.Bpm.InfoMessageDisplayMode.Individual, "JobEntry","ChangeJobHeadJobReleased");
}
2 Likes

@A.Baeisa modify your post and add this around your code block

```cs
yourcode
```

many thanks @hkeric.wci

1 Like

Looks like million dollar code now :money_mouth_face::moneybag::money_with_wings:

:joy::ok_hand:

Thank you Haso and Al, It’s such a relief to find that there are people here who have done pretty much the same as I am trying to do and are willing to help. I will have a go at adjusting your code and let you know how I get on.
Thanks again guys
Adrian.

1 Like

Mine sents a custom Email with the Entire Job with the ones that have issues in Red in a HTML Table which is Mobile Friendly to a point.

2 Likes

Now that is very cool indeed Haso

Given that I used a Template provided by the Community, I guess it should not be a problem to re-share it.

// Zero Cost Job Part Email
//
// 12/06/17 HK: Initial Implementation, since this is considered a temporary, lets keep it simple
//

// Initialize Actions
Func<string, string> GetCompanyAddressAction = (CompanyID) => {

	var Company_Row =
		(from sc in Db.SysCompany.With(LockHint.NoLock)
		where sc.Company == CompanyID
		select new { sc.EmailFromAddr, sc.EmailFromLabel }).FirstOrDefault();

	if (Company_Row != null) {
		return string.Format(@"""{0}"" <{1}>", Company_Row.EmailFromLabel.Trim(), Company_Row.EmailFromAddr.Trim());
	}

	return string.Empty;
};


// Initialize Variables
decimal TotalStdCost = 0;

// Initialize Mail Variables
string EmailTO = "chrissconnwhereareyou@aol.com";
//string EmailTO = "youcanAddMultiples@cc.me; orccMe@whitehouse.org";
string EmailCC = "";
string EmailBCC = ""; // If blank itll be just ignored on bottom
string EmailSubject = "";
string EmailBody = "";

// Get JobHead
var ttJobHead_Row = ttJobHead.FirstOrDefault();

// Get Demand Link Details
var JobProd_Row =
	(from jp in Db.JobProd.With(LockHint.NoLock)

		join jh in Db.JobHead.With(LockHint.NoLock)
				on new { jp.Company, JN = jp.TargetJobNum } equals new { jh.Company, JN = jh.JobNum }

		where
			jp.Company == ttJobHead_Row.Company && jp.JobNum == ttJobHead_Row.JobNum && jp.PartNum == ttJobHead_Row.PartNum

		select new {
			jp.Company, jp.Plant, RecordType = "JobProd", jp.TargetJobNum, jp.TargetAssemblySeq, jp.TargetMtlSeq,
			DemandPartNum = jh.PartNum
		}
	).FirstOrDefault();

if (ttJobHead_Row != null)
{

	// Set Subject
	EmailSubject = "[ Alert ] Missing Std Cost - Job #: " + ttJobHead_Row.JobNum + " Released in Company " + ttJobHead_Row.Company;

	// Start Email Body
	EmailBody += string.Format("Job # <b>{0}</b> was released in Company <b>{1}</b> on <b>{2}</b> by <b>{3}</b>:",
								ttJobHead_Row.JobNum, ttJobHead_Row.Company, DateTime.Today.ToString("d"), callContextClient.CurrentUserId);

  	EmailBody += "<BR><BR>";


	EmailBody +=
		@"<table width='100%' align='center' border='1' cellspacing='1' cellpadding='1' style='font-family: Arial; font-size: 10pt;' bordercolor='#CCCCCC'>"
		+ "<tr>"
		+ "<th>Demand Job / Part</th>"
		+ "<th>Job / Asm / Mtl</th>"
		+ "<th>Part Type</th>"
		+ "<th>Part</th>"
		+ "<th>Prod Group</th>"
		+ "<th>Class</th>"
		+ "<th>Qty</th>"
		+ "<th>Std Cost</th>"
		+ "</tr>";


	// Union JobAsmbl and JobMtl since they have common columns needed for Details Query
	var JobParts = (
		from ja in Db.JobAsmbl.With(LockHint.NoLock)
		where ja.Company == ttJobHead_Row.Company && ja.JobNum == ttJobHead_Row.JobNum
		select new {
			ja.Company, ja.Plant, RecordType = "JobAsmbl", Seq = ja.AssemblySeq, ParentSeq = ja.Parent, ja.JobNum, ja.PartNum, ja.Description, ja.IUM, ja.RequiredQty
		}
	).Union(
		from jm in Db.JobMtl.With(LockHint.NoLock)
		where jm.Company == ttJobHead_Row.Company && jm.JobNum == ttJobHead_Row.JobNum
		select new {
			jm.Company, jm.Plant, RecordType = "JobMtl", Seq = jm.MtlSeq, ParentSeq = jm.AssemblySeq, jm.JobNum, jm.PartNum, jm.Description, jm.IUM, jm.RequiredQty
		}
	);


	// Get The Rest of Details for JobParts
	var JobParts_Rows =
		(from jp_rows in JobParts

		join pl in Db.Plant.With(LockHint.NoLock)
				on new { jp_rows.Company, PL = jp_rows.Plant } equals new { pl.Company, PL = pl.Plant1 }

		join pc in Db.PartCost.With(LockHint.NoLock)
				on new { pl.Company, CID = pl.PlantCostID, PartNum = jp_rows.PartNum } equals new { pc.Company, CID = pc.CostID, PartNum = pc.PartNum }

		/* Join on PartNum to get ClassID */
		join p in Db.Part.With(LockHint.NoLock)
				on new { jp_rows.Company, jp_rows.PartNum } equals new { p.Company, p.PartNum }

		/* Left Join on PartClass, just incase it does not exist - To Get Description */
		join pcl in Db.PartClass.With(LockHint.NoLock)
					on new { p.Company, p.ClassID } equals new { pcl.Company, pcl.ClassID }
			into pclj
		from pcl in pclj.DefaultIfEmpty()

		/* Left Join on ProdGrup, just incase it does not exist */
		join prdc in Db.ProdGrup.With(LockHint.NoLock)
				on new { p.Company, p.ProdCode } equals new { prdc.Company, prdc.ProdCode }
			into prdcj
		from prdc in prdcj.DefaultIfEmpty()


		select new {
			jp_rows.Company, jp_rows.Plant, jp_rows.RecordType, jp_rows.Seq, jp_rows.ParentSeq, jp_rows.JobNum, jp_rows.PartNum, jp_rows.Description, jp_rows.IUM, jp_rows.RequiredQty,
			p.ClassID, ClassDescription = pcl != null ? pcl.Description : "",
			p.ProdCode, ProdGroupDesc = prdc != null ? prdc.Description : "",
			TotalStdCost = (pc.StdLaborCost + pc.StdBurdenCost + pc.StdMaterialCost + pc.StdSubContCost + pc.StdMtlBurCost)
		}).ToList();

	Ice.Diagnostics.Log.WriteEntry("[ ZeroCostJobPart Notification ] - Building Lines...");

	foreach (var JobPart_Row in JobParts_Rows)
	{
		TotalStdCost = Math.Round(JobPart_Row.TotalStdCost, 3);

		EmailBody +=
		"<tr style='" + (TotalStdCost == 0 && JobPart_Row.ClassID != "REFD" ? "color: red;" : "") + "'>"
		+ @"<td align='center'>" + (JobProd_Row == null ? " - " : JobProd_Row.TargetJobNum + " / " + JobProd_Row.DemandPartNum) + "</td>"
		+ @"<td align='center'>" + JobPart_Row.JobNum + " / " + JobPart_Row.ParentSeq + " / " + JobPart_Row.Seq + "</td>"
		+ @"<td align='center'>" + JobPart_Row.RecordType + "</td>"
		+ @"<td align='center'><b>" + JobPart_Row.PartNum + @"</b><br><span style='font-size: 8pt'>" + JobPart_Row.Description + "</span></td>"
		+ @"<td align='center'><b>" + JobPart_Row.ProdCode + @"</b><br><span style='font-size: 8pt'>" + JobPart_Row.ProdGroupDesc + "</span></td>"
		+ @"<td align='center'><b>" + JobPart_Row.ClassID + @"</b><br><span style='font-size: 8pt'>" + JobPart_Row.ClassDescription + "</span></td>"
		+ @"<td align='center'>" + Math.Round(JobPart_Row.RequiredQty, 2) + " " + JobPart_Row.IUM + "</td>"
		+ @"<td align='center'>" + Math.Round(JobPart_Row.TotalStdCost, 2) + "</td>"
		+ "</tr>";
	}

	EmailBody += "</table>";

	Ice.Diagnostics.Log.WriteEntry("[ ZeroCostJobPart Notification ] - Sending Email...");


	// Send Email
	var mailer = this.GetMailer(async:true);
	var message = new Ice.Mail.SmtpMail();
	message.SetFrom( GetCompanyAddressAction(ttJobHead_Row.Company) );
	message.SetTo(EmailTO);
	message.SetCC(EmailCC);
	message.SetBcc(EmailBCC);
	message.SetSubject(EmailSubject);
	message.SetBody(EmailBody);
	message.IsBodyHtml = true;
	mailer.Send(message);

	Ice.Diagnostics.Log.WriteEntry("[ ZeroCostJobPart Notification ] - Email Sent!");
}
2 Likes

i think that you need to start teaching your fancy stuff and revealing your secrets in coding, i have been looking for such LINQ techniques for a long time, if you do i will be your first student mate.:weight_lifting_man:‍♂

:slight_smile:

I will join you Al

so it does have C#LINQ (Database and UI) forms functions and techniques ?

1 Like

Yes it has a lets solve this problem via C#… then it has a … Now lets solve it via LINQ Only. It has a decent amount of problems to show you the power of LINQ and a diff approach to problem solving. It comes with LINQPad Examples you can embed into LINQPad and tinker w/.

you are a star
image

I got my copy on order :smile: