Looking for a product configurator expert who uses it to build a BOM and a price

Another issue with Component Pricing… AFAIK it cannot capture the labor involved in the current job, only the labor that’s already rolled into the prices of the components. This was the main reason we decided it’s really only useful for configured products that are essentially sales kits of existing parts thrown in a box. This is why we ended up focused on the Quote Worksheet as the only way to capture all the costs and come up with a price. I still haven’t solved the problem of how to price subcomponents that are spun off as separate jobs, since those MoMs aren’t generated until MRP runs.

I’m tantalizingly close to getting this to work, but it’s been one wall after another.

After giving up on component pricing, we went down the path of configurators building MoMs on quotes and using the quote worksheet to calculate a price. I figured out how to run Get Details on a revision from another site. But we didn’t like the complexity of the quote screen and anticipated pushback from users. The last nail in that coffin was that we couldn’t figure out how to generate transfer orders, so we couldn’t get COGS to hit the right accounts. So then I figured out how to make configurators create parts for a multi-level BOM. This allows us to start from the sales order, and it should allow for transfer orders. The question is how to price the parts.

This can be broken down into several questions: where to put a BPM, when to trigger it, and how to calculate the price.

I found that ConfigurationRuntime.SavePcValueConfiguration is called once for the parent part and once for each configurable subassembly. It’s called top down, starting with the parent (assembly 0). This is important later. The parts for the parent and all subassemblies already exist in post of the first call, but apparently their MoMs have not all been created until post of the last call. So we can trigger when the StructTag of the PcStruct with the highest StructID is equal to the StructTag of the ValueHead:

var head = pcValueDS.PcValueHead.Single();
var pcStructs = configurationSequenceDS.PcStruct.Where(o => o.Added() || o.Updated()).OrderBy(o => o.StructID);
if(pcStructs.All(o => o.CreatePart) && head.StructTag == pcStructs.Last().StructTag)
{
  ...
}

Now, how to roll up the cost of the part that was just created? The Costing Workbench UI uses a BO called EngWorkBenchSvc. The UI won’t let you do this, but the BO actually lets you call ViewCosts with a blank GroupID and a part that is not checked out! Glory, glory, hallelujah!

I already knew from earlier experiments where I was triggering on the first call to SavePcValueConfiguration that if you use the PartSvc to update the UnitPrice on the created part, the UI will automatically update with the new price. I was just about to declare victory, but my final test failed. It seems that it changes OrderDtl.PartNum and pulls in the unit price after the first call to SavePcValueConfiguration, not the last one. But you can’t compute the cost after the first call because the subassembly MoMs aren’t built yet. Argh!

If only they’d called SavePcValueConfiguration bottom up instead of top down… or save them in any order, but save them all before changing the OrderDtl.PartNum…

I have UD method code which bases pricing on Epicor price lists applied to part number on a specific date honoring customer groups as well as shipto pricing. The only thing I never addressed was price list discounts and price breaks as those for out products are almost never used.

I created/used a UD Table for labor op timings for one of our configurators at our custom division.

I’d like to leverage Epicor’s built-in cost calculation if possible. It’s unfortunate that it’s not more exposed and documented.

There are actually two BOs related to this, CostWorkBenchSvc and EngWorkBenchSvc. Cost Rollup uses EngWorkBenchSvc. I wonder if some of the discrepancies people have reported between Cost Rollup and the BOM Cost Report are because the report still uses CostWorkBenchSvc.

You can use their built in cost calculation, however it only use todays date for pricing calculations. There is no way to set for future pricing increases, AFAIK it will price according to the day the price is fetched from the configurator.

I haven’t experimented with future dates. As-of date is one of the parameters to EngWorkBenchSvc.ViewCosts, but not to CostWorkBenchSvc.ViewCosts. CostWorkBenchSvc methods also don’t take a revision. I guess they all default to “now” and the default revision.

Very nice find there.

Well the UD Function is a Price lookup versus your Cost built in Epicor lookups. My mistake there, I was referring to the PricingLookup BOs in Epicor not costing. I could not find any way to allow for future dated orders to provide PRICING (as requested by the OP) for a given date. A price lookup in the future not possible AFAIK using standard Epicor BOs.

Future pricing would require a magical oracle, since future costs are unknown and the relationship between cost and price is an arbitrary business rule. :grin: But if you’re doing cost-plus according to some formula, maybe you could estimate a future price from current costs and approved revisions with future effective dates. Price lists with future effective dates might also come into play. I wouldn’t want to have to re-write all that logic myself…

Here is my simple labor time Confgurator UD Method: UDMethods.GetOpTime(Trailer,ModelYear,OpId) // Provides labor time for different custom operations below.

var timeRow = (	from row in Db.UD39 
				where row.Company == Context.CompanyID && 
					row.Key1 == Trailer.Substring(2) &&  
					row.Key2 == ModelYear
				select row).FirstOrDefault();

if (timeRow != null) 
	{
	switch (OpId)
		{
		case "1":
			return timeRow.Number01;
			break;
		case "2":
			return timeRow.Number02;
			break;
		case "3":
			return timeRow.Number03;
			break;
		case "4":
			return timeRow.Number04;
			break;
		case "5":
			return timeRow.Number05;
			break;	
		case "6":
			return timeRow.Number06;
			break;
		case "7":
			return timeRow.Number07;
			break;
		case "8":
			return timeRow.Number08;
			break;
		case "9":
			return timeRow.Number09;
			break;
		case "10":
			return timeRow.Number10;
			break;
		case "11":
			return timeRow.Number11;
			break;
		case "12":
			return timeRow.Number12;
			break;
		case "13":
			return timeRow.Number13;
			break;
		case "14":
			return timeRow.Number14;
			break;
		case "15":
			return timeRow.Number15;
			break;
		case "16":
			return timeRow.Number16;
			break;
		case "17":
			return timeRow.Number17;
			break;
		case "18":
			return timeRow.Number18;
			break;
		case "19":
			return timeRow.Number19;
			break;
		case "20":
			return timeRow.Number20;
			break;
		}
	}
return -1m;

Here is sample usage:

string OpId=((Inputs.txtNewTrailer.Value.Substring(Inputs.txtNewTrailer.Value.Length-2)=="01")?"15":"2"); // Alum Jigs : Jigs
decimal OpTime = UDMethods.GetOpTime(Inputs.txtTrailer.Value, Inputs.txtModelYr.Value, OpId);  

Like I said I did not rewrite all the logic mine does not utilize pricebreaks or discounts within pricelists only UNIT pricing for the configured products (PartNum) based on DATED ordered ShipTo PL > Customer PL > Customer Group PL.

Not really a magical oracle. Just as our suppliers notify us of pricing increases, we will notify customers of pricing increases for new model years, raw materials increases or logistical increases.
We must adjust our prices to maintain any semblance of profitability. Sales can adjust product pricing lists and notify their customers of coming pricing and have the lists in the system with effective dates when said pricing for a given part number goes into effect. This way sales can take orders for a future date and have it priced accordingly by the price at the future date.

1 Like

I have an expert on staff.
Jeremy has been building PC solutions for 10 years. Top-Notch.
Call me at 419-651-6704

Successful proof of concept on ConfigurationRuntime.SavePcValueConfiguration post:

/*
SavePcValueConfiguration is called once for the parent part and once for each subassembly.
All subassembly parts exist in post on the first call, but their BOMs don't exist until post on the last call.
So trigger pricing on the last call, identified by the StructTag of the PcStruct with the highest StructID.
*/
var head = pcValueDS.PcValueHead.Single();
var structs = configurationSequenceDS.PcStruct.Where(o => o.Added() || o.Updated()).OrderBy(o => o.StructID);
if(structs.All(o => o.CreatePart) && head.StructTag == structs.Last().StructTag)
{
  var parent = structs.First();
  var pn = parent.NewPartNum;
  var rev = parent.RevisionNum;
  var unitCost = 0M;
  CallService<EngWorkBenchSvcContract>(svc =>
  {
    var costDS = svc.ViewCosts("", pn, rev, "", DateTime.Now, 1, 999, false);
    var costs = costDS.PartRevCosts.Single();
    unitCost = costs.MaterialUnitCost + costs.LaborUnitCost +
      costs.SubcontractUnitCost + costs.BurdenUnitCost + costs.MtlBurdenUnitCost;
  });
  // TODO determine markup the same way quote worksheet does
  var markup = 2M;
  var decimalPlaces = 2;
  var unitPrice = Math.Round(unitCost * markup, decimalPlaces);
  /*
  Epicor sets the PartNum and UnitPrice on the OrderDtl after the first call
  to SavePcValueConfiguration, when we don't have a price yet.
  */
  var context = pcValueDS.PcContextProperties.Single();
  if(context.Entity == "OrderDtl")
  {
    CallService<SalesOrderSvcContract>(svc =>
    {
      var orderDS = svc.GetByID(context.OrderNumber);
      var dtl = orderDS.OrderDtl.Single(o => o.OrderLine == context.OrderDetailNumber);
      dtl.UnitPrice = unitPrice;
      dtl.DocUnitPrice = unitPrice;
      //dtl.LockPrice = true; // ???
      dtl.RowMod = "U";
      svc.Update(ref orderDS);
    });
  }
  /* Optional. */
  //CallService<PartSvcContract>(partSvc =>
  //{
  //  var partDS = partSvc.GetByID(pn);
  //  var part = partDS.Part.Single();
  //  part.UnitPrice = unitPrice;
  //  part.RowMod = "U";
  //  partSvc.Update(ref partDS);
  //});
}