Auto Salvage

Hey All,

First time posting! Hope to get some feedback from you more seasoned folks. We are wanting to automate the receipt to salvage when labor is posted. I have developed a quick Function and BPMs to accomplish this and wanted to know if there were any ‘got yas’ I didnt think of.

From a high level, BPM triggers on Labor.Update

  • Pre-Process gathers initial values and stores in BPM context fields
  • Post-Process pulls in context values and triggers the Function.
  • Function calculates the salvage amount and cost
  • Then runs the PreUpdate and ReceiptSalvage methods.

Pre-Process Code:

bool debug = true;
// Only care about updated rows, A is in here for manual labor adds. MES will create a labordtl record on start production, so all labor activities should be U.
var laborRows = (from ld in ds.LaborDtl
                 where ld.RowMod == “U” || ld.RowMod == “A”
                 select ld).ToList();
if (!laborRows.Any())
{
    return;
}
// Assume one LaborDtl per call (MES).
var tt = laborRows.FirstOrDefault();
string company = tt.Company;
string jobNum  = tt.JobNum;
int asmSeq     = tt.AssemblySeq;
int oprSeq     = tt.OprSeq;
// NEW values from dataset
decimal newLaborQty   = tt.LaborQty;
decimal newScrapQty   = tt.ScrapQty;
decimal newDiscrepQty = tt.DiscrepQty;
// OLD values from DB
var existing = (from x in Db.LaborDtl
                where x.Company     == tt.Company
                   && x.LaborHedSeq == tt.LaborHedSeq
                   && x.LaborDtlSeq == tt.LaborDtlSeq
                select x).FirstOrDefault();
decimal oldLaborQty   = existing != null ? existing.LaborQty   : 0m;
decimal oldScrapQty   = existing != null ? existing.ScrapQty   : 0m;
decimal oldDiscrepQty = existing != null ? existing.DiscrepQty : 0m;
// DELTAS
decimal deltaLaborQty   = newLaborQty   - oldLaborQty;
decimal deltaScrapQty   = newScrapQty   - oldScrapQty;
decimal deltaDiscrepQty = newDiscrepQty - oldDiscrepQty;
// Total delta driving salvage
decimal operDelta = deltaLaborQty + deltaScrapQty + deltaDiscrepQty;
// If nothing increased, no new salvage should be created
if (operDelta == 0m)
{
    // Clear BPM data just in case
    this.callContextBpmData.Number01    = 0m;
    this.callContextBpmData.Number02    = 0m;
    this.callContextBpmData.Number03    = 0m;
    this.callContextBpmData.Character01 = string.Empty;
    this.callContextBpmData.Character02 = string.Empty;
    this.callContextBpmData.Character03 = string.Empty;
    this.callContextBpmData.Character04 = string.Empty;
    return;
}
// Store deltas & keys in callContextBpmData so Post-Process can see them
this.callContextBpmData.Number01 = deltaLaborQty;
this.callContextBpmData.Number02 = deltaScrapQty;
this.callContextBpmData.Number03 = deltaDiscrepQty;
// Save key fields too so Post has them without re-deriving
this.callContextBpmData.Character01 = company;
this.callContextBpmData.Character02 = jobNum;
this.callContextBpmData.Character03 = asmSeq.ToString();
this.callContextBpmData.Character04 = oprSeq.ToString();
// Optional debug
if (debug)
{
    this.PublishInfoMessage(
        $“Delta for salvage: Labor={deltaLaborQty}, Scrap={deltaScrapQty}, Discrep={deltaDiscrepQty}, Total={operDelta}”,
        Ice.Common.BusinessObjectMessageType.Information,
        Ice.Bpm.InfoMessageDisplayMode.Individual,
        “”,
        “”
    );
}

Post-Process Labor.Update

Function Code:

// Parameters: Company, JobNum, AssemblySeq, OprSeq, LaborQty, ScrapQty, DiscrepQty
// Dataset variable: rfmDS (Erp.Tablesets.ReceiptsFromMfgTableset)

// Ensure the dataset exists
if (rfmDS == null)
{
    rfmDS = new Erp.Tablesets.ReceiptsFromMfgTableset();
}

// Total pieces processed at this operation = good + scrap + discrep
decimal operQty = LaborQty + ScrapQty + DiscrepQty;

// Basic guards
if (string.IsNullOrEmpty(JobNum))
{
    return;
}

// If you do NOT want salvage for net-zero ops, you can do:
   if (operQty == 0m) return;

// --- Validate Job ---
var jh = (from j in Db.JobHead
          where j.Company == Company
             && j.JobNum == JobNum
          select j).FirstOrDefault();

if (jh == null || !jh.JobFirm || jh.JobClosed || !jh.JobReleased)
{
    return;
}

// --- Find JobMtl salvage candidates (ZZ material with SalvagePartNum) ---
var jobMtls = (from m in Db.JobMtl
               where m.Company == Company
                  && m.JobNum == JobNum
                  && m.PartNum.StartsWith("ZZ")
                  && m.SalvagePartNum != ""
               // optional: only materials tied to this operation
                && (m.RelatedOperation == OprSeq) // || m.RelatedOperation == 0
               select m).ToList();

if (!jobMtls.Any())
{
    return;
}

foreach (var jm in jobMtls)
{
    decimal salvagePer = (decimal)jm.SalvageQtyPer;
    decimal mltQtyPer  = (decimal)jm.QtyPer;

    if (salvagePer == 0m || mltQtyPer == 0m)
        continue;

    // Salvage for this op = (good + scrap + discrep) × material qty per FG × salvage qty per material
    decimal salvageThisOperation = operQty * mltQtyPer * salvagePer;

    decimal salvageQty = salvageThisOperation;

    if (salvageQty == 0m)
        continue;


    // Get salvage part (for UOM and basic validation)
    var salvagePart = (from sp in Db.Part
                       where sp.Company == Company
                          && sp.PartNum == jm.SalvagePartNum
                       select sp).FirstOrDefault();

    if (salvagePart == null)
        continue;

string salvageUM        = salvagePart.IUM;
string salvageWarehouse = "Default";                                             // TODO: your salvage WH
string salvageBin       = "Salv";                                                // TODO: your salvage bin

// ----------------------------------------------------------------------
// Compute salvageUnitCost from ICommCode + PartUOM (no _UD join)
// ----------------------------------------------------------------------
decimal salvageUnitCost = 0m;

// Get the ZZ material part on the job (jm.PartNum)
var zzMatPart = (from p in Db.Part
                 where p.Company == Company
                    && p.PartNum == jm.PartNum
                 select p).FirstOrDefault();

if (zzMatPart != null)
{
    // Commodity for that material - Custom Table
    var comm = (from c in Db.ICommCode
                where c.Company == Company
                   && c.CommodityCode == zzMatPart.CommodityCode
                select c).FirstOrDefault();

    if (comm != null)
    {
        
        decimal salvagePricePerPound = comm.SalvagePricePerPound_c;
        decimal buyingRebatePerPound = comm.BuyingRebatePerPound_c;
        decimal sellingRebatePerPound = comm.SellingRebatePerPound_c;

        var uom = (from u in Db.PartUOM
                   where u.Company == Company
                      && u.PartNum == jm.PartNum
                   select u).FirstOrDefault();

        decimal convFactor = 1m;
        if (uom != null)
            convFactor = (decimal)uom.ConvFactor;

        decimal salvageUnitCredit = (salvagePricePerPound + sellingRebatePerPound + buyingRebatePerPound) * convFactor;

        salvageUnitCost = salvageUnitCredit;
    }
}

// If you only want salvage when you have a positive credit, you can optionally do:
// if (salvageUnitCost <= 0m)
//     continue;

    // --- Create a PartTran row in rfmDS ---
    var pt = rfmDS.PartTran.NewRow();

    var now = DateTime.Now;
    int sysTime = now.Hour * 3600 + now.Minute * 60 + now.Second;

    pt["Company"]        = Company;
    pt["Plant"]          = jh.Plant;
    pt["Plant2"]         = jh.Plant;
    pt["TranClass"]      = "R";
    pt["TranType"]       = "SVG-STK";
    pt["InventoryTrans"] = true;
    pt["TranDate"]       = now.Date;
    pt["SysDate"]        = now.Date;
    pt["SysTime"]        = sysTime;

    pt["JobNum"]      = jm.JobNum;
    pt["AssemblySeq"] = jm.AssemblySeq;
    pt["JobSeqType"]  = "M";
    pt["JobSeq"]      = jm.RelatedOperation != 0 ? jm.RelatedOperation : OprSeq;

    pt["PartNum"]     = jm.SalvagePartNum;
    pt["TranQty"]     = salvageQty;
    pt["ActTranQty"]  = salvageQty;
    pt["UM"]          = salvageUM;
    pt["ActTransUOM"] = salvageUM;
    pt["DimConvFactor"] = 1m;

    pt["WareHouseCode"] = salvageWarehouse;
    pt["BinNum"]        = salvageBin;

    pt["MtlUnitCost"]    = salvageUnitCost;
    pt["MtlMtlUnitCost"] = salvageUnitCost;
    pt["BegMtlUnitCost"] = salvageUnitCost;
    pt["ExtCost"]        = Math.Round(salvageQty * salvageUnitCost, 4);

    pt["GLTrans"]  = true;
    pt["RowMod"]   = "A";

    // Add the row to PartTran (use Add, not Rows.Add)
    rfmDS.PartTran.Add(pt);
}
 


So there are several things that you can do throughout your code to 1. make it more efficient, and 2. make it less wordy

  1. include all your if statements into your queries. Currently you retreive a record, then apply an if statement to check something, and “continue” if the check fails. If you were to include the conditions directly in your select, you would skip this extra check and save time.
  2. I prefer to use the alternate formatting for the selections (see example below.
  3. I also “hate” to put returns in the middle of any code. I also dislike Continue type logic… I would rather put a big if statement into the logic and have it skip a section of code rather than have a bunch of continues. Sometimes this is required, but most of the time, you can avoid it. (this is all personal choice, and doesn’t really affect efficiency but CAN cause confusion.

Here is one way to make things much more efficient. Since you are only need a one field (the Plant field), only return that instead of the entire record. Note that you can return as many or few fields as necessary This will make it much faster. Here is an example:

var jh = db.JobHead.Where(j =>  j.Company == Company &&
                                    j.JobNum == JobNum
                                    j.JobFirm &&
                                    !j.JobClosed &&
                                    j.JobReleased).select(jh=>jh.Plant).FirstOrDefault();var jh = db.JobHead.Where(j =>  j.Company == Company &&
                                    j.JobNum == JobNum
                                    j.JobFirm &&
                                    !j.JobClosed &&
                                    j.JobReleased).select(jh=>jh.JobNum,jh.Plant).FirstOrDefault();

Here is an example of how i would do the jobMaterial selection differently. Note all the commented out code, and how there are extra conditions in the select.

// --- Find JobMtl salvage candidates (ZZ material with SalvagePartNum) ---
var jobMtls = Db.JobMtl.Where(m => m.Company == Company &&
                                    m.JobNum == JobNum &&
                                    m.PartNum.StartsWith("ZZ") &&
                                    m.SalvagePartNum != "" &&
                                    m.SalvageQtyPer != 0 &&
                                    m.QtyPer != 0 &&
                                    OperQty * m.QtyPer * m.SalvagePer != 0
                                    m.RelatedOperation == OprSeq).ToList();

foreach (var jm in jobMtls) {
    //decimal salvagePer = (decimal)jm.SalvageQtyPer;
    //decimal mltQtyPer  = (decimal)jm.QtyPer;

    //if (salvagePer == 0m || mltQtyPer == 0m)
    //    continue;

    // Salvage for this op = (good + scrap + discrep) × material qty per FG × salvage qty per material
    //decimal salvageThisOperation = operQty * mltQtyPer * salvagePer;

    //decimal salvageQty = salvageThisOperation;

    //if (salvageQty == 0m)
    //    continue;

    decimal salvageQty = operQty * jm.SalvageQtyPer * jm.QtyPer

1 Like

Thanks Tim,

I will work on making those adaptions.

No red flags on the logic or methods used?