Engineered part in quote not creating proper job in order

We are currently trying to set up a generic part which we can then engineer with proper materials and push through the entire process from quote → order → job creation → fulfillment

The issue that we are finding is that our engineered part (from the quote) does not carry the engineered materials/operations through the chain into the order etc. There is probably something very easy that we are missing :slight_smile:

On the Quote side, we have engineered the part and checked the ‘engineered’ box. The revision for this part is properly approved:
image

image

Then, we use ‘Create Sales Order’:
image

Also, our price does not seem to carry over when using the quote order wizard:
image
On the quote:
image

On the actual created sales order - our quantity came across but not price:
image

Using ‘Create Jobs’ on the sales order creates our job with zero materials and zero operations - where did these go? Our quantity seems to be the only remaining proper value.
image

image

Job status:
image
Job completely empty:
image

What are we missing here? How can we take a part that is engineered in the quote and carry all of that through to the order, job creation, etc? This part is type Manufactured

Any help greatly appreciated :slight_smile: thank you all

1 Like

Is this a configured part? There are some quirks with configuring quotes on pricing that can cause issues, and the method rules have to have code for both Job & Quote engineering.

1 Like

This is not a configured part - there are not configurator rules etc.
There is a configurator entry attached but it is completely empty. This part should be able to be engineered with any of our parts and I do not want to have to write a configurator rule individually for 1000s of parts

1 Like

So it is a configurable part but it has no method rules? Am I understanding that correctly?

If so, I would guess that is why the engineering is not copying over. When you run Get Details, are you selecting the From Quote option? I would have assumed that would override a configuration, but I haven’t seen that particular situation before.

1 Like

Ended up figuring it out - I wasn’t creating the sales order correctly from the quote.

Instead of simply hitting the ‘Create’, we have to go through the job wizard screen. This properly pulls our engineered job from the quote. This is still within “Create Sales Order”:
image

Then, since pricing wasn’t carrying over to the order since we didn’t use any configuration rules etc - I created a function on the order entry side that fires if it finds the specific part that we are creating this functionality for. I probably could have done this slightly better but here is the code:

string companyID = "*****"; //redacted for privacy :)
PartFound = false;
FoundPrice = 0m;
foundOrderLine = 0;

// Try to find a line with partnum = "5865000"
var orderLine = Db.OrderDtl
    .FirstOrDefault(d => d.Company == companyID &&
                         d.OrderNum == SalesOrderNum &&
                         d.PartNum == "5865000");

if (orderLine != null)
{
    PartFound = true;
  foundOrderLine = orderLine.OrderLine;

    // Only continue if we have a valid QuoteNum
    if (orderLine.QuoteNum != 0)
    {
        var quoteLine = Db.QuoteDtl
            .FirstOrDefault(qd => qd.Company == companyID &&
                                  qd.QuoteNum == orderLine.QuoteNum &&
                                  qd.QuoteLine == orderLine.QuoteLine);

        if (quoteLine != null)
        {
            FoundPrice = quoteLine.DocExpUnitPrice;
        }
    }
}

After retrieving the price, I simply update the price within the order entry application based on our FoundPrice response param.

If anyone is interested in the overall use-case here, we wanted a generic part for generic kits which can be configured. You are able to enter all the parts ordered into the engineering for that part, then this function will divide all the qty/parent for all materials based off of a given division factor (we are assuming all parts are evenly divisable here… specific to our use case - though you could automatically look for the highest common denominator if anyone does something similar). Then, this function updates the qty of the parent part within the quote.

Basically, it will divide all your materials, update your quote parent part qty, and also calculate the price based on the customer on the quote → customer’s price list → part prices from that specific price list.

// Declare and initialize
var quoteMtlTS = new Erp.Tablesets.QuoteAsmTableset();
List < Erp.Tablesets.QuoteMtlRow > quoteMtlList = new List < Erp.Tablesets.QuoteMtlRow > ();

// Clear outputs
outJson = "";
outMessage = "";
debug = "";
innerdebug = "";
totalPrice = 0;

// Additional structures
var outJsonList = new List < Dictionary < string,
  object >> ();
var mtlLookup = new Dictionary < int,
  Dictionary < string, object >> (); // Using MtlSeq as the key

try {
  this.CallService < Erp.Contracts.QuoteAsmSvcContract > (QuoteAsmSvc => {
    try {
      // Fetch dataset
      QuoteAsmSvc.GetDatasetForTreeByRef(
        ref quoteMtlTS,
        QuoteNum,
        QuoteLine,
        0, // AssemblySeq
        0, // CopyNum
        false // GetLatestRev
      );

      // Confirm rows returned
      debug = "quoteMtlTS.QuoteMtl count: " + quoteMtlTS.QuoteMtl.Count.ToString();
    if (quoteMtlTS.QuoteMtl.Count == 0) {
      innerdebug = "<b>ERROR</b><br><br>Please enter materials";
    }

      // Store rows in local list and populate mtlLookup using MtlSeq as the key
      foreach(var row in quoteMtlTS.QuoteMtl) {
        quoteMtlList.Add(row);

        var dict = new Dictionary < string,
          object > {
            {
              "AddrList",
              row.AddrList
            },
            {
              "AnalysisCdDescription",
              row.AnalysisCdDescription
            },
            {
              "AnalysisCode",
              row.AnalysisCode
            },
            {
              "APSAddResType",
              row.APSAddResType
            },
            {
              "AssemblySeq",
              row.AssemblySeq
            },
            {
              "AssemblySeqDescription",
              row.AssemblySeqDescription
            },
            {
              "AttributeSetID",
              row.AttributeSetID
            },
            {
              "AutoSetChkDirect",
              row.AutoSetChkDirect
            },
            {
              "BasePartNum",
              row.BasePartNum
            },
            {
              "BaseRevisionNum",
              row.BaseRevisionNum
            },
            {
              "BaseUOM",
              row.BaseUOM
            },
            {
              "BitFlag",
              row.BitFlag
            },
            {
              "BuyIt",
              row.BuyIt
            },
            {
              "Class",
              row.Class
            },
            {
              "ClassDescription",
              row.ClassDescription
            },
            {
              "ClassInactive",
              row.ClassInactive
            },
            {
              "Company",
              row.Company
            },
            {
              "ContractID",
              row.ContractID
            },
            {
              "Description",
              row.Description
            },
            {
              "Direct",
              row.Direct
            },
            {
              "DispPlanningNumberOfPieces",
              row.DispPlanningNumberOfPieces
            },
            {
              "DispSalvagePlanningNumberOfPieces",
              row.DispSalvagePlanningNumberOfPieces
            },
            {
              "DynAttrValueSetDescription",
              row.DynAttrValueSetDescription
            },
            {
              "DynAttrValueSetShortDescription",
              row.DynAttrValueSetShortDescription
            },
            {
              "EnableFixedQty",
              row.EnableFixedQty
            },
            {
              "EstMtlBurUnitCost",
              row.EstMtlBurUnitCost
            },
            {
              "EstMtlUnitCost",
              row.EstMtlUnitCost
            },
            {
              "EstScrap",
              row.EstScrap
            },
            {
              "EstScrapType",
              row.EstScrapType
            },
            {
              "EstUnitCost",
              row.EstUnitCost
            },
            {
              "FindNum",
              row.FindNum
            },
            {
              "FixedQty",
              row.FixedQty
            },
            {
              "GlbRFQ",
              row.GlbRFQ
            },
            {
              "GroupSeq",
              row.GroupSeq
            },
            {
              "IsMtlConfigurationOn",
              row.IsMtlConfigurationOn
            },
            {
              "IsMtlConfigureOn",
              row.IsMtlConfigureOn
            },
            {
              "IsMtlExtConfig",
              row.IsMtlExtConfig
            },
            {
              "IsMtlRevisionApproved",
              row.IsMtlRevisionApproved
            },
            {
              "IUM",
              row.IUM
            },
            {
              "LeadTime",
              row.LeadTime
            },
            {
              "LinkToContract",
              row.LinkToContract
            },
            {
              "LocationView",
              row.LocationView
            },
            {
              "MfgComment",
              row.MfgComment
            },
            {
              "MinimumCost",
              row.MinimumCost
            },
            {
              "MiscCharge",
              row.MiscCharge
            },
            {
              "MiscCode",
              row.MiscCode
            },
            {
              "MtlBurRate",
              row.MtlBurRate
            },
            {
              "MtlSeq",
              row.MtlSeq
            }, // Using MtlSeq as the key
            {
              "OrigGroupSeq",
              row.OrigGroupSeq
            },
            {
              "OrigStructTag",
              row.OrigStructTag
            },
            {
              "PartExists",
              row.PartExists
            },
            {
              "PartNum",
              row.PartNum
            },
            {
              "PartNumAttrClassID",
              row.PartNumAttrClassID
            },
            {
              "PartNumIUM",
              row.PartNumIUM
            },
            {
              "PartNumPartDescription",
              row.PartNumPartDescription
            },
            {
              "PartNumPricePerCode",
              row.PartNumPricePerCode
            },
            {
              "PartNumSalesUM",
              row.PartNumSalesUM
            },
            {
              "PartNumSellingFactor",
              row.PartNumSellingFactor
            },
            {
              "PartNumTrackDimension",
              row.PartNumTrackDimension
            },
            {
              "PartNumTrackInventoryAttributes",
              row.PartNumTrackInventoryAttributes
            },
            {
              "PartNumTrackInventoryByRevision",
              row.PartNumTrackInventoryByRevision
            },
            {
              "PartNumTrackLots",
              row.PartNumTrackLots
            },
            {
              "PartNumTrackSerialNum",
              row.PartNumTrackSerialNum
            },
            {
              "PBImage",
              row.PBImage
            },
            {
              "PBrkCost01",
              row.PBrkCost01
            },
            {
              "PBrkCost02",
              row.PBrkCost02
            },
            {
              "PBrkCost03",
              row.PBrkCost03
            },
            {
              "PBrkCost04",
              row.PBrkCost04
            },
            {
              "PBrkCost05",
              row.PBrkCost05
            },
            {
              "PBrkCost06",
              row.PBrkCost06
            },
            {
              "PBrkCost07",
              row.PBrkCost07
            },
            {
              "PBrkCost08",
              row.PBrkCost08
            },
            {
              "PBrkCost09",
              row.PBrkCost09
            },
            {
              "PBrkCost10",
              row.PBrkCost10
            },
            {
              "PBrkQty01",
              row.PBrkQty01
            },
            {
              "PBrkQty02",
              row.PBrkQty02
            },
            {
              "PBrkQty03",
              row.PBrkQty03
            },
            {
              "PBrkQty04",
              row.PBrkQty04
            },
            {
              "PBrkQty05",
              row.PBrkQty05
            },
            {
              "PBrkQty06",
              row.PBrkQty06
            },
            {
              "PBrkQty07",
              row.PBrkQty07
            },
            {
              "PBrkQty08",
              row.PBrkQty08
            },
            {
              "PBrkQty09",
              row.PBrkQty09
            },
            {
              "PBrkQty10",
              row.PBrkQty10
            },
            {
              "PCLinkRemoved",
              row.PCLinkRemoved
            },
            {
              "PlanningNumberOfPieces",
              row.PlanningNumberOfPieces
            },
            {
              "PLMMtlSeq",
              row.PLMMtlSeq
            },
            {
              "PurComment",
              row.PurComment
            },
            {
              "PurMiscDescription",
              row.PurMiscDescription
            },
            {
              "PurPoint",
              row.PurPoint
            },
            {
              "PurPointAddress1",
              row.PurPointAddress1
            },
            {
              "PurPointAddress2",
              row.PurPointAddress2
            },
            {
              "PurPointAddress3",
              row.PurPointAddress3
            },
            {
              "PurPointCity",
              row.PurPointCity
            },
            {
              "PurPointCountry",
              row.PurPointCountry
            },
            {
              "PurPointName",
              row.PurPointName
            },
            {
              "PurPointPrimPCon",
              row.PurPointPrimPCon
            },
            {
              "PurPointState",
              row.PurPointState
            },
            {
              "PurPointZip",
              row.PurPointZip
            },
            {
              "QtyBearing",
              row.QtyBearing
            },
            {
              "QtyPer",
              row.QtyPer
            },
            {
              "QuoteLine",
              row.QuoteLine
            },
            {
              "QuoteLineLineDesc",
              row.QuoteLineLineDesc
            },
            {
              "QuoteNum",
              row.QuoteNum
            },
            {
              "QuoteNumCurrencyCode",
              row.QuoteNumCurrencyCode
            },
            {
              "RDEndNum",
              row.RDEndNum
            },
            {
              "RDPrefix",
              row.RDPrefix
            },
            {
              "RDStartNum",
              row.RDStartNum
            },
            {
              "RDSuffix",
              row.RDSuffix
            },
            {
              "RelatedOperation",
              row.RelatedOperation
            },
            {
              "RelatedOperationDescription",
              row.RelatedOperationDescription
            },
            {
              "ReqRefDes",
              row.ReqRefDes
            },
            {
              "RequiredQty",
              row.RequiredQty
            },
            {
              "RevisionNum",
              row.RevisionNum
            },
            {
              "RFQLine",
              row.RFQLine
            },
            {
              "RFQLineLineDesc",
              row.RFQLineLineDesc
            },
            {
              "RFQNeeded",
              row.RFQNeeded
            },
            {
              "RFQNum",
              row.RFQNum
            },
            {
              "RFQStat",
              row.RFQStat
            },
            {
              "RFQVendQuotes",
              row.RFQVendQuotes
            },
            {
              "RowMod",
              row.RowMod
            },
            {
              "SalvageAttrClassID",
              row.SalvageAttrClassID
            },
            {
              "SalvageAttributeSetID",
              row.SalvageAttributeSetID
            },
            {
              "SalvageBaseUOM",
              row.SalvageBaseUOM
            },
            {
              "SalvageDescription",
              row.SalvageDescription
            },
            {
              "SalvageEstMtlBurUnitCredit",
              row.SalvageEstMtlBurUnitCredit
            },
            {
              "SalvageMtlBurRate",
              row.SalvageMtlBurRate
            },
            {
              "SalvageQtyPer",
              row.SalvageQtyPer
            },
            {
              "SalvageUM",
              row.SalvageUM
            }
          };

        mtlLookup.Add(row.MtlSeq, dict); // Use MtlSeq as the key
      }

      // Modify fields as required
      foreach(var mtl in mtlLookup.Values) {
        decimal qtyPer = Convert.ToDecimal(mtl["QtyPer"]);
        decimal factor = Convert.ToDecimal(divisionFactor);

        // Check if the division is clean (no remainder)
        if (qtyPer % factor == 0) {
          int dividedQty = Convert.ToInt32(qtyPer / factor);

          // Assign divided value
          mtl["QtyPer"] = dividedQty;
          mtl["RDEndNum"] = dividedQty;
          mtl["ReqRefDes"] = dividedQty;
          mtl["RequiredQty"] = dividedQty;

          // Mark for update
          mtl["RowMod"] = "U";

          // Retrieve the customer number from QuoteNum
          int custNum = 0; // Default value for CustNum

          this.CallService < Erp.Contracts.QuoteSvcContract > (QuoteService => {
            try {
              // Retrieve the quote using QuoteNum
              var retrievedQuote = QuoteService.GetByID((int) mtl["QuoteNum"]);

              // Check if QuoteHed is not null and contains elements
              var quoteHed = retrievedQuote?.QuoteHed?.FirstOrDefault(q => q.QuoteNum == (int) mtl["QuoteNum"]);

              if (quoteHed != null) {
                custNum = quoteHed.CustNum;
              }
            } catch (Exception ex) {
              // Handle exception and return default value
              custNum = 0;
            }
          });
          //int custNum = this.EfxLib.LCIQuoteLibrary.GetCustNumFromQuote((int)mtl["QuoteNum"]);

          // Retrieve the customer price list based on the customer number
          string priceList = (string) this.EfxLib.LCIPriceLst.GetCustomerPriceListByCustNum(custNum);

          // Retrieve the part price based on the PartNum and the price list
          decimal partPrice = 0;

          try {
            var partNumTrimmed = ((string) mtl["PartNum"])?.Trim().ToUpper();
            var listCodeTrimmed = priceList?.Trim().ToUpper();

            if (!string.IsNullOrEmpty(partNumTrimmed) && !string.IsNullOrEmpty(listCodeTrimmed)) {
              var priceLstPart = Db.PriceLstParts
                .FirstOrDefault(plp =>
                  plp.PartNum.Trim().ToUpper() == partNumTrimmed &&
                  plp.ListCode.Trim().ToUpper() == listCodeTrimmed);

              if (priceLstPart != null) {
                partPrice = priceLstPart.BasePrice;
              }
            }
          } catch (Exception ex) {
            partPrice = 0;
            innerdebug = "<b>Error</b><br>err retrieving part price - are all parts in the price list?<br>";
          }

          //decimal partPrice = (decimal)this.EfxLib.LCIPriceLst.GetPartPrice((string)mtl["PartNum"], priceList);

          totalPrice += partPrice * dividedQty;

        } else {
          // Round both values to 2 decimal places for the debug message
          decimal roundedQtyPer = decimal.Round(qtyPer, 2);
          decimal roundedFactor = decimal.Round(factor, 2);

          innerdebug = $"<b>Error</b> - <br><br>not clean division: <b>{roundedQtyPer} / {roundedFactor}</b><br><br><b>Please ensure all division is clean without remainders</b>";

          return;
        }
      }

      // Set the modified data back into the QuoteMtl dataset for update
      foreach(var row in quoteMtlTS.QuoteMtl) {
        var updatedRow = mtlLookup.ContainsKey(row.MtlSeq) ? mtlLookup[row.MtlSeq] : null;
        if (updatedRow != null) {
          row.QtyPer = Convert.ToInt32(updatedRow["QtyPer"]);
          row.RDEndNum = Convert.ToInt32(updatedRow["RDEndNum"]);
          row.ReqRefDes = Convert.ToInt32(updatedRow["ReqRefDes"]);
          row.RequiredQty = Convert.ToDecimal(updatedRow["RequiredQty"]);
          row.RowMod = updatedRow["RowMod"].ToString();
        }
      }

      // Update the service with modified data
      QuoteAsmSvc.Update(ref quoteMtlTS);

      // Serialize and order by MtlSeq before sending the result
      outJson = Newtonsoft.Json.JsonConvert.SerializeObject(
        outJsonList.OrderBy(d => (int) d["MtlSeq"]), // Optional: Ensure MtlSeq order
        Newtonsoft.Json.Formatting.Indented
      );
    } catch (Exception ex) {
      outMessage = ex.Message;
      debug = "Error while processing QuoteAsm: " + ex.Message;
    }
  });
} catch (Exception ex) {
  outMessage = ex.Message;
  debug = "Error in service call: " + ex.Message;
}

There are a lot of extra response params not even being used here in the end as they were helpful in debugging while writing this function, and innerdebug turned into an error message that appears on the application if innerdebug != “”. But wanted to share in-case anyone found this useful for any reason :slight_smile: