Add A Filter to Fulfillment Workbench

We’d like to filter fulfilment workbench searches to pre-packaged parts that can ship by slapping on a shipping label, and off it goes. What’s the best way to do that? I was thinking of adding a UD Field to the Part’s attribute tab (Part.Slapper_c), but how wouId we filter for just those parts in FWB? Quicksearch or Namedsearch on order lines that have parts where PartSlapper = true? Or can I add that as a filter directly to the FWB search, either in the search page or in the results page (like fulfillment %)

image

I’ve tried creating a quick search or named search, but can’t get them to appear in FWB. Any idea on what columns need to be included that I’m missing? Or is FWB special, and this won’t work?

select
[OrderDtl].[OrderNum] as [OrderDtl_OrderNum],
[OrderDtl].[OrderLine] as [OrderDtl_OrderLine],
[OrderDtl].[PartNum] as [OrderDtl_PartNum],
[OrderDtl].[LineDesc] as [OrderDtl_LineDesc],
[Part].[CommercialBrand] as [Part_CommercialBrand]
from Erp.OrderDtl as OrderDtl
inner join Erp.Part as Part on
OrderDtl.Company = Part.Company
and OrderDtl.PartNum = Part.PartNum
and ( Part.CommercialBrand = ‘SL’ )

where (OrderDtl.OpenLine = true)

image

image

OrderRel.SysRowID

1 Like

SO… fulfillment workbench is “Special”… it doesn’t follow the rules because of the complexity of the data needed.

  1. the data table used is the PartDtl table
  2. the data RETURNED is the PartDtl.SysRowID

So… here is how you do it:

  1. BAQ:
    • Create a BAQ that shows the data you want to show, but also include the related PartDtl record.
  2. QuickSearch:
    • go in to Fulfillment Workbench, and onto the Order Fulfillment tab
    • do the standard search and recall at least one record to the screen
    • right-click on the Order Number field, and choose “Quick Search Entry” so you can define the quicksearch you want to use.
  • create your new quicksearch using your BAQ… make sure that you have at least one new criteria, and you need to return the SysRowID:
  1. TURN ON QUICKSEARCH:
    • There is a menu option in the ACTIONS menu of the fulfillment Workbench called “User Preferences”. Enter the name of your created quicksearch here.

NOW… you can use this new quicksearch for your orders.

1 Like

Thanks @timshuwy and @Doug.C! I was able to get my rough idea to to work. It seems that QuickSearch replaces the base search in FWB, and there’s no option to have both side by side.

Would our users need to add the user pref Quicksearch every time, and remove it every time to get the base search?

Is it possible to get FWB to run automatically on a schedule? (maybe I make a BAQ to get the records? Or somehow set all the FWB filters? And then actions > allocate, and pick the template… And then repeat with slightly different settings to un-allocate and unreserve the ones that don’t meet criteria to go to a job?

I recently did this with Scheduled Epicor Functions but it required a lot of tracing and reverse engineering all the BO Methods. It was a lot of work so I would be very interested to hear if there ends up being an easier way to achieve this.

Is PartDtl.SysRowId still the hook if I am trying to return jobs in the ‘Job Fulfillment’ tab? I am trying to make a quick search that returns based on the Finished Good Part Number and not having success.

I have one currently working in fulfillment workbench. Have you added the column into the BAQ Search tab?
image

I didn’t have that but I did just add it.

I can do my search on the FG Part #, get my expected results, but when I select any # of records and hit OK it doesn’t pull the selected records into the FWB grid.

How long do you give it? Fulfillment workbench is a slow screen.

I don’t think I’m being impatient.

Even if I select a couple records, I see the ‘Retrieving data’ briefly in the lower left, it spins a second, and then goes back to ‘Ready’

Just wanted to double check. Unsure of what might cause records to not show up. Apologies.

@mikelyndersOKCC - Can you share your function? I’m about to go down the same path. We run FWB several times a day, with certain parameters (Shipby set to -2mo to today, exclude others like allocated, picked, etc). Epicor takes about 15 minutes to churn through it wide-open. We’d get a lot of goodwill if we can schedule this as a function, and then allocate it. Then we run it again to un-allocate & un-reserve the ones with issues…

@askulte

I’m not sure where I was in development when I made my comment a few months ago but our strategy has evolved a bit. I will share the allocation function below but it is tailored to our specific use case and might not make sense in a different context. Everything we make passes through a final inspection operation where the operator has access to an HTML5/Javascript/React based application that I created. This application integrates various digital inspection instruments and many features of MES that we have recreated using Epicor Functions. When the operation is completed, one of the last steps is firing this Allocation Function for that Lot only. This is possible because everything we do is made to order, thus a demand exists the moment the product is produced. We have worked hard on implementing an IOT pick-to-light system that directs the operator to split up the manufactured material into bins that correspond to orders in a staging area. You will see both inventory transfer and allocation in the function below. This is handled by a separate Epicor Function called from the HTML. Paired with this is a BAQ search in FWB that grabs all the demands fully allocated and ready to ship (with similar filters to what you described). This is necessary because we haven’t figured out how to print the Material Queue sheets automatically, else this would be a scheduled Epicor Function.

Apologies for poor coding, this project evolved rapidly and needs better comments. Hopefully you can pull something useful out of this.

AllocateLot(lotNum, completedQty)

const int PS_MONTHS = 4; // if DoNotShipBefore <= today() + PS_MONTHS material goes to Pre-Shipping Area


try{
this.CallService<OrderAllocSvcContract>(orderAllocBO => {
  this.CallService<JobPartSvcContract>(jobPartBO => {
    this.CallService<MaterialQueueSvcContract>(materialQueueBO => {
         
      string partNum = this.Db.JobPart.Where(r => r.JobNum == this.lotNum).First().PartNum;
      
      // --------------------  clear material queue of any stk-stk movements to PS zone of this part
	  
      this.debug += "starting material queue delete,";
      MaterialQueueTableset mtlQueue = materialQueueBO.GetRows("TranType = 'STK-STK'", 0, 0, out bool morePages);

      this.debug += "a,";
      var mtlQueueQuery = mtlQueue.MtlQueue.Where(x=>x.PartNum == partNum).Join(
        this.Db.WhseBin,
        mtl => new {mtl.Company, mtl.ToWhse, mtl.ToBinNum},
        whse => new {whse.Company, ToWhse=whse.WarehouseCode, ToBinNum=whse.BinNum},
        (mtl, whse) => new {mtl.Company, mtl.ToWhse, mtl.ToBinNum, whse.ZoneID, mtl.MtlQueueSeq, mtl=mtl}
      )
      .Where(x=>x.Company == "OKCCM" && x.ToWhse == "Main" && x.ZoneID == "PS")
      .Select(x=>x.mtl);
      this.debug += "b,"; 
	  
      foreach (var mtlQueueItem in mtlQueueQuery){
        this.debug += "pre deleting " + mtlQueueItem.MtlQueueSeq + " ,";
        // SelectedForProcessing
        mtlQueueItem.RowMod = "D";
      }
      materialQueueBO.Update(ref mtlQueue);
      this.debug += "end material queue delete,";
      
	  
	  
	  
	  // ------------------ create a table set to store output
      
      orderAllocBO.SetCalcPref(true); // set OrderAlloc to auto calculate fullfillment on search
      
      string cMessageText = "";
      string oErrorText = "";
      string oDemandtype = "";
      bool lReleased = false;
      bool opFWBLimitedRefresh = false;
      
      this.data = new DataSet("data");
      
      DataTable finalAllocationTable = this.data.Tables.Add("allocations");
      finalAllocationTable.Columns.Add("zoneId", typeof(string));
      finalAllocationTable.Columns.Add("location", typeof(string));
      finalAllocationTable.Columns.Add("binNum", typeof(string));
      finalAllocationTable.Columns.Add("orderNum", typeof(Int32));
      finalAllocationTable.Columns.Add("qty", typeof(Int32));
      finalAllocationTable.Columns.Add("isEntireSingleLine", typeof(bool));
      
      DataTable myAllocationTable = new DataTable("allocations");
      myAllocationTable.Columns.Add("zoneId", typeof(string));
      myAllocationTable.Columns.Add("location", typeof(string));
      myAllocationTable.Columns.Add("binNum", typeof(string));
      myAllocationTable.Columns.Add("orderNum", typeof(Int32));
      myAllocationTable.Columns.Add("qty", typeof(Int32));
      
      DataTable psBinContentsTable = this.data.Tables.Add("psBinContents");
      psBinContentsTable.Columns.Add("binNum", typeof(string));
      psBinContentsTable.Columns.Add("orderNum", typeof(Int32));
      psBinContentsTable.Columns.Add("orderLine", typeof(Int32));
      psBinContentsTable.Columns.Add("partNum", typeof(string));
      psBinContentsTable.Columns.Add("qty", typeof(Int32));
	  
	  
	  
	  // ---------------- get order alloc tableset
	  
      
      string stockBinNum = default;
      
      OrderAllocTableset myOrderAllocTableset = this.EfxLib.InventoryAutomationLib.GenerateOrderAllocTablesetByPart(partNum);
      int lineCount = myOrderAllocTableset.OrderAlloc.Count();
      var myOrderAllocRows = myOrderAllocTableset.OrderAlloc.Where(r => r.RemainingToReserve > 0); // filter to demands that need material
	  
	  // for each demand, 
      
      foreach(OrderAllocRow myOrderAllocRow in myOrderAllocRows.OrderBy(x => x.ReqDate).ThenBy(x=>x.NeedByDate).ThenBy(x=>x.OrderNum)) // demands are ordered by ReqDate, NeedByDate, OrderNum
      {
        try{
          this.debug += $"\nOrderNum: {myOrderAllocRow.OrderNum} OrderLine: {myOrderAllocRow.OrderLine} RemainingToReserve {myOrderAllocRow.RemainingToReserve}";
          //;
         
          myOrderAllocRow.SelectedForAction = true;
          myOrderAllocRow.ReleaseCount = 0; // lineCount; //should be the count of records in myOrderAllocTableset.OrderAlloc
          myOrderAllocRow.LineCount = 0; //lineCount;
          myOrderAllocRow.RowMod = "U";
                     
          (myOrderAllocTableset, cMessageText, oErrorText, oDemandtype) = this.EfxLib.InventoryAutomationLib.GetOnHandQtys(myOrderAllocTableset); // get the current on hand quanitites
          
          PartAllocLotRow myPartAllocLot = myOrderAllocTableset.PartAllocLot.Where(r => r.LotNum == this.lotNum && r.AvailableQty > 0).FirstOrDefault(); // find a chunk of unallocated parts
          
          if (myPartAllocLot == null) break;
          this.debug += $"\nLotNum: {myPartAllocLot.LotNum} AvailableQty: {myPartAllocLot.AvailableQty}";
          //stockBinNum = myPartAllocLot.BinNum;
          string fromBinNum = myPartAllocLot.BinNum;
          string fromZoneID = this.Db.WhseBin.Where(x=>x.Company==myPartAllocLot.Company && x.BinNum==myPartAllocLot.BinNum && x.WarehouseCode==myPartAllocLot.WarehouseCode).FirstOrDefault().ZoneID;
          this.unallocatedBinNum = fromBinNum;
          this.unallocatedZoneId = fromZoneID;
          string toBinNum = fromBinNum;
          string toZoneID = fromZoneID;
          string location = "";
          string binSizeID = default;
          decimal estOrderMass = default;
          int boxQty = default;
          int count = default;
          int orderBoxes = default;
          string debug = "";
          this.debug += $"\nFromBinNum: {myPartAllocLot.BinNum}";
          
          int transferQty = (int)Math.Max(0, Math.Min(Math.Min(myOrderAllocRow.RemainingToReserve, this.completedQty), myPartAllocLot.AvailableQty));
          if (transferQty > 0 && !(myOrderAllocRow.DoNotShipBeforeDate > BpmFunc.AddInterval(BpmFunc.Today(), PS_MONTHS, IntervalUnit.Months))) // determine if material should be transferred to pre-shipping area
          {
            this.debug += "\nTransfering to Pre-Shipping Area";
            DataSet AvailibleBins;
            (estOrderMass, binSizeID, toBinNum, toZoneID, AvailibleBins, boxQty, count, orderBoxes, debug) = this.EfxLib.InventoryAutomationLib.GetToBin(myOrderAllocRow.OrderNum, partNum, myOrderAllocRow.ShipOrderComplete, myOrderAllocRow.DoNotShipBeforeDate, myOrderAllocRow.OrderLine, transferQty); //determine the destination bin
            
            /*
            // filter out any locations already used Where(Bin.Locations is not in Locations)
            AvailibleBins.Except(myAllocationTable.Select(x=>x.location))
            myAllocationTable
            
            // loop through the candidates and don't consider 
            */
            
            List<string> usedLocations = myAllocationTable.AsEnumerable().Select(x=>(string)x["Location"]).Distinct().ToList();
            
            var binTable = AvailibleBins.Tables["Bins"];
            var i = 0;
            while (i < binTable.Rows.Count ) 
            {
              var binCandidate = AvailibleBins.Tables["Bins"].Rows[i];
              if(!usedLocations.Contains((string)binCandidate["Location"])){
                // assign toBin
  
                toBinNum = (string)binCandidate["BinNum"];
                toZoneID = (string)binCandidate["ZoneID"];
                location = (string)binCandidate["Location"];
                
                break;
              }
              i++;
            }
            
            if (String.IsNullOrEmpty(toBinNum)) {
              throw new Exception("Invalid Bin");
            }
                      
            this.debug += $"\nToBinNum: {toBinNum}";
  
            this.debug += $"\nTransferQty: {transferQty}";
            this.debug += $"\nRemainingToReserve: {myOrderAllocRow.RemainingToReserve} AvailableQty: {myPartAllocLot.AvailableQty}";
            this.EfxLib.InventoryAutomationLib.InventoryTransfer(partNum, toBinNum, this.lotNum, fromBinNum, this.lotNum, transferQty); // move the inventory
            (myOrderAllocTableset, cMessageText, oErrorText, oDemandtype) = this.EfxLib.InventoryAutomationLib.GetOnHandQtys(myOrderAllocTableset); // refresh on hand qtys to reflect inventory transfer
            this.debug += "\nTransfered Inventory to Pre-Shipping Area";
  
            try
            {
              myPartAllocLot = myOrderAllocTableset.PartAllocLot.Where(r => r.LotNum == this.lotNum && r.AvailableQty >= 0 && r.BinNum == toBinNum).First(); // should find the inventory in a new bin, if not throw error
            }
            catch (InvalidCastException e)
            {
                throw new Exception($"Could not find matching PartAllocLot for BinNum: {toBinNum}, LotNum: {this.lotNum}", e);
            }
      
            int allocQty = (int)Math.Max(0,Math.Min(myPartAllocLot.AvailableQty, Math.Min(myOrderAllocRow.RemainingToReserve, this.completedQty))); // allocate all the transfered inventory
            this.debug += $"\nAllocQty: {allocQty}";
            myPartAllocLot.Allocate = true;
            myPartAllocLot.NewAllocatedQty = allocQty + myPartAllocLot.AllocatedQty;
            //this.AvailibleQty = this.AvailibleQty - allocQty;
                 
            orderAllocBO.AllocateByLotBin(ref myOrderAllocTableset, "", "All", out cMessageText, out lReleased); // perform the allocation
            orderAllocBO.GetFWBLimitedRefresh(out opFWBLimitedRefresh);
            orderAllocBO.GetFWBLimitedRefresh(out opFWBLimitedRefresh);
                   
            var context = Ice.Services.ContextFactory.CreateContext<ErpContext>();
            Erp.Internal.Lib.DeferredUpdate libDeferredUpdate = new Erp.Internal.Lib.DeferredUpdate(context);
            libDeferredUpdate.UpdPQDemand(); // must process the deferred updates
            this.debug += "\nAllocated Inventory";
            
            this.completedQty -= allocQty;
            myOrderAllocRow.SelectedForAction = false;
            myOrderAllocRow.RowMod = "";
            
            DataRow myAllocationRow = myAllocationTable.NewRow();
            myAllocationRow["zoneId"] = toZoneID;
            myAllocationRow["binNum"] = toBinNum;
            myAllocationRow["location"] = location;
            myAllocationRow["orderNum"] = myOrderAllocRow.OrderNum;
            myAllocationRow["qty"] = allocQty;
            // myAllocationRow["isEntireSingleLine"] = allocQty == lines.First().Qty && lines.Count() == 1 ;
            myAllocationTable.Rows.Add(myAllocationRow);
          }
          else this.debug += "\nSkipping Transfer to Pre-Shipping Area, DoNotShipBeforeDate: {myOrderAllocRow.DoNotShipBeforeDate}";
        }
        catch(Exception e){
          this.debug+=e.Message + ", ";
        }
      }
      
      // get is single line
      var orderIsEntireSingleLine = myAllocationTable.AsEnumerable()
      .GroupJoin(
        this.Db.OrderDtl.Where(x=>x.Company=="OKCCM"),
        a => new {Company="OKCCM", OrderNum=(int)a["orderNum"]},
        od => new {od.Company, od.OrderNum},
        (a, ods) => new {Allocation=a, IsEntireSingleLine=(ods.Count() == 1 && ods.FirstOrDefault().SellingQuantity == (int)a["qty"])}
      );

      
      foreach(var row in orderIsEntireSingleLine){
        DataRow finalAllocationRow = finalAllocationTable.NewRow();
        finalAllocationRow["zoneId"] = row.Allocation["zoneId"];
        finalAllocationRow["binNum"] = row.Allocation["binNum"];
        finalAllocationRow["location"] = row.Allocation["location"];
        finalAllocationRow["orderNum"] = row.Allocation["orderNum"];
        finalAllocationRow["qty"] = row.Allocation["qty"];
        finalAllocationRow["isEntireSingleLine"] = row.IsEntireSingleLine;
        finalAllocationTable.Rows.Add(finalAllocationRow);
      }
      
      
      
      // get psBinContents
      try
      {
        var psBinContentsQuery = this.Db.PartAlloc.AsEnumerable()
          .Join(
            myAllocationTable.AsEnumerable(),
            pa => new { binNum=pa.BinNum, orderNum=pa.OrderNum },
            a => new { binNum=(String)a["binNum"], orderNum=(int)a["orderNum"] },
            (pa, a) => new {
              binNum=(String)a["binNum"],
              orderNum=(int)a["orderNum"],
              orderLine=pa.OrderLine,
              partNum=pa.PartNum,
              qty=(pa.AllocatedQty + pa.PickingQty)
            }
          )
          .GroupBy(
            x => new {x.binNum, x.orderNum, x.orderLine, x.partNum},
            (x, xs) => new {x.binNum, x.orderNum, x.orderLine, x.partNum, qty=xs.Sum(y=>y.qty)}
          );
          
        foreach(var psBinContentsRecord in psBinContentsQuery)
        {
          DataRow psBinContentsRow = psBinContentsTable.NewRow();
          psBinContentsRow["binNum"] = psBinContentsRecord.binNum;
          psBinContentsRow["orderNum"] = psBinContentsRecord.orderNum;
          psBinContentsRow["orderLine"] = psBinContentsRecord.orderLine;
          psBinContentsRow["partNum"] = psBinContentsRecord.partNum;
          psBinContentsRow["qty"] = psBinContentsRecord.qty;
          psBinContentsTable.Rows.Add(psBinContentsRow);
        }   
      }
      catch(Exception e){
        this.debug+=e.Message + ", ";
      }
      
	  // need to modify to show original bins
      
      if (this.completedQty > 0){
		PartAllocLotRow myPartAllocLot = myOrderAllocTableset.PartAllocLot.Where(r => r.LotNum == this.lotNum && r.AvailableQty > 0).FirstOrDefault(); // tryto find the pieces
		  
		  
        DataRow finalAllocationRow = finalAllocationTable.NewRow();
        finalAllocationRow["zoneId"] = this.Db.WhseBin.Where(x=>x.Company==myPartAllocLot.Company && x.BinNum==myPartAllocLot.BinNum && x.WarehouseCode==myPartAllocLot.WarehouseCode).FirstOrDefault().ZoneID;
        finalAllocationRow["binNum"] = myPartAllocLot.BinNum;
        finalAllocationRow["orderNum"] = 0;
        finalAllocationRow["qty"] = this.completedQty;
        finalAllocationRow["isEntireSingleLine"] = false;
        finalAllocationTable.Rows.Add(finalAllocationRow);
      }
      this.unallocatedQty = this.completedQty; 
    });
  });
});
}
catch(Exception e){
  this.debug += e.Message;
}

GenerateOrderAllocTablesetByPart(partNum)

this.CallService<OrderAllocSvcContract>(orderAllocBO => {
  this.CallService<PartAllocTemplateSvcContract>(partAllocTemplateBO => {
    OrderAllocListTableset orderAllocListTableset = orderAllocBO.GetListOfOrders("","","",$"OrderRel.PartNum = '{ PartNum }'","","","","","false","OrderHed.ReservePriorityCode , OrderRel.ReqDate","","",0,0, out bool morePages, "");
    this.orderAllocTableset = orderAllocBO.OrderAllocationGetRows(orderAllocListTableset, 0);
    PartAllocTemplateTableset partAllocTemplateTableset = partAllocTemplateBO.GetByID("Automation");
    
    PartAllocTemplateRow partAllocTemplateRow = partAllocTemplateTableset.PartAllocTemplate.First();
  
  
    
    // Update orderAllocTableset.PartAllocTrans with template
    foreach (PartAllocTranRow partAllocTranRow in orderAllocTableset.PartAllocTran)
    {
      partAllocTranRow.AddHoc = false;
      partAllocTranRow.WarehouseCode = partAllocTemplateRow.WarehouseCode;
      partAllocTranRow.AllocTemplateID = partAllocTemplateRow.AllocTemplateID;
      partAllocTranRow.AllocTempDescAllocTemplateDesc = partAllocTemplateRow.AllocTemplateDesc;
      partAllocTranRow.RowMod = "U";
    }
        
  });
});

@mikelyndersOKCC - Thanks so much for sharing! This is definitely helpful to see how it’s been done already. Thank you.

I’m going through a similar effort now and wanted to update this reply for posterity. I can tell you that in lieu of specifying the user preference default quick search for a sales order, you can instead specify context default and base default, which will then give your custom quick search with the “base search” button back to the user to use the base search when they need. I also wanted to note that my quicksearch was mysteriously missing until I first added it as my user preference default, saved and reloaded the nonpersonalized copy of the custom, so perhaps doing so works around some sort of bug. I didn’t investigate any deeper than this.

This is the easier way to do what I was planning to do, which is to make an additional side by side button to launch the custom quick search (simple) and then finding a way to call the order alloc adapter get list and all the base logic invoked data into the form (time consuming).

2 Likes