Pages

Showing posts with label lot genealogy. Show all posts
Showing posts with label lot genealogy. Show all posts

Tuesday, April 16, 2013

Lot Genealogy, Part 3: Debugging Lots and Batches

As you might have noticed I've just updated the LOT_GENEALOGY source code for this project to a new version following the discovery of a bug where the product of a batch is two different item numbers. This is a rare occurrence in our system, which is why it went undetected for so long, but it might not be in yours.

Rather than dwell on that I thought I'd talk about exactly what the process was that led to us discovering the fixing the error.

The first report came from the users; a lot which they knew had two item numbers was only showing one. Other lots didn't seem to be affected. This was then traced back to a single batch, 73591, running this SQL gave us the transactions that affected the cache for that batch. To publish this data I'm obfuscating the data (see here);

SELECT LGG.TRANSACTION_ID,
       HIDEDATA.Hide('LGG.LN', LGG.LOT_NUMBER) Lot_Number,
       LGG.TRANSACTION_QUANTITY,
       LGG.TRANSACTION_TYPE_NAME,
       LGG.Item_Number,
       HIDEDATA.Hide('LGG.ID', LGG.Item_Description) Item_Description,
       LGG.ITEM_TYPE
  FROM LOT_GENEALOGY_GETTRANSACTIONS LGG
 WHERE 1 = 1
   AND LGG.Batch_No = '73591';

The result of this is the following transactions;

LG Transactions For A Single Batch
As you can see the first thing we have is three "WIP Completion" transactions. Each of these represents the completion of the batch and the generation of an output. In this case Item Numbers 073081 and 07440. However without knowing what information was put *into* the batch in the first place it's not possible to usefully use this information in the Genealogy - It has to wait until the "WIP Issue" transactions are processed (each of which represents an ingredient).

The next stage for debugging the cache was to (on a test system!) change the LOT_GENEALOGY_GETTRANSACTIONS view so that rather than looking at *everything* it only looks at the records for a single batch - this is simply done by adding the following where clause to the view SQL;

-- Restrict transactions to a single batch (for debugging)
AND GBH.Batch_No = '73591'

Now we're restricted the input records to just those affecting that batch we can just re-build the whole cache - it will take longer to do the delete than the insert. The script to do it is;

begin
  -- Call the procedure
  lot_genealogy.refreshwholecache;
end;


Once that has been completed the entire content of the cache is the single batch we're tracking. The SQL to show it is;

SELECT HIDEDATA.Hide('LGG.LN', LGW.Master_Lot_Number) Master_Lot_Number,
       HIDEDATA.Hide('LGG.LN', LGW.Ingred_Lot_Number) Ingred_Lot_Number,
       LGW.Ingred_Item_Number,
       HIDEDATA.Hide('LGG.ID', LGW.Ingred_Item_Description) Ingred_Item_Description,
       LGW.Ingred_Item_Type_Code,
       LGW.Batch_Number,
       HIDEDATA.Hide('LGG.LN', LGW.PRODUCT_LOT_NUMBER) PRODUCT_LOT_NUMBER,
       LGW.PRODUCT_ITEM_NUMBER,
       HIDEDATA.Hide('LGG.ID', LGW.Product_Item_Description) Product_Item_Description,
       LGW.Product_Item_Type_Code
  FROM LOT_GENEALOGY_WHEREUSED LGW
 WHERE 1 = 1


The result of this is;

Lot Genealogy Cache: Result for a Single Batch
As you can see the only product for Ingredient Lot LGG.LN 37 is item number 073002, if we look at the transactions earlier we can see that it should be reporting Item Number 07440 as well - it's not which means something is going wrong in the "WIP Issue" part of the cache processing.

If we look at the source code (available here - via Google Drive) you'll see that the final part of the WIP Issue is creating the records in LOT_GENEALOGY_BATCH_PRODUCT so the next stage to check is to see if these records are being created correctly. Here is the SQL;

SELECT LGBP.batch_number,
       HIDEDATA.Hide('LGG.LN', LGBP.PRODUCT_LOT_NUMBER) PRODUCT_LOT_NUMBER,
       LGBP.product_item_number,
       HIDEDATA.Hide('LGG.ID', LGBP.Product_Item_Description) Product_Item_Description,
       LGBP.product_item_type_code
  FROM LOT_GENEALOGY_BATCH_PRODUCT LGBP
 WHERE 1 = 1
   AND LGBP.Batch_Number = '73591'


This gives us the result;

Lot Genealogy Batch Products
This shows us that the correct batch products are being recorded - this is important as we now have both ends of the transaction; the correct transactions are going in and the correct products are coming out. However we also know that the cache isn't being updated correctly therefore the culprits must be the two pieces of SQL that are actually doing the inserts.

If you look at the first one;

INSERT INTO NOETIX_SYS.Lot_Genealogy_WhereUsed
    SELECT v_Transaction.Lot_Number, -- Master_Lot_Number
           v_Transaction.Lot_Number, -- Ingred_Lot_Number
           v_Transaction.Item$item, -- INGRED_ITEM$ITEM
           v_Transaction.Item_Description, -- INGRED_ITEM_DESCRIPTION
           v_Transaction.Item_Type_Code, -- INGRED_ITEM_TYPE_CODE
           v_Transaction.Batch_Number, -- Batch_Number
           v_Product.Product_Lot_Number, -- Product_Lot_Number
           v_Product.PRODUCT_ITEM$ITEM, -- PRODUCT_ITEM$ITEM
           v_Product.PRODUCT_ITEM_DESCRIPTION, -- PRODUCT_ITEM_DESCRIPTION
           v_Product.PRODUCT_ITEM_TYPE_CODE -- PRODUCT_ITEM_TYPE_CODE
      FROM DUAL
     WHERE NOT EXISTS
     (SELECT 1
              FROM NOETIX_SYS.Lot_Genealogy_WhereUsed LGI
             WHERE LGI.MASTER_LOT_NUMBER = v_Transaction.Lot_Number
               AND LGI.Ingred_LOT_NUMBER = v_Transaction.Lot_Number
               AND LGI.Batch_Number = v_Transaction.Batch_Number
               AND (v_Product.Product_Lot_Number IS NULL AND
                   LGI.PRODUCT_LOT_NUMBER IS NULL OR
                   v_Product.Product_Lot_Number = LGI.PRODUCT_LOT_NUMBER));


You can see that this is actually just a select from DUAL so the fact that it's only working on the first pass (i.e. for the first product) means that the offending part of the code must be the NOT EXISTS.

Looking at the WHERE clause in the sub-select reveals that it's not using the Product_Item_Number field. Not using this field means that after it's inserted the first product it mistakes the second one for a duplicate and skips over it.

Altering the final AND statement to;

AND (v_Product.Product_Lot_Number IS NULL AND LGI.PRODUCT_LOT_NUMBER IS NULL OR (v_Product.Product_Lot_Number = LGI.PRODUCT_LOT_NUMBER AND v_Product.Product_Item_Number = LGI.PRODUCT_ITEM_NUMBER))

For BOTH pieces of SQL (the one updating  all the existing records as well as this one which is creating a new master record) addresses the issue logically, if you rebuild the cache from scratch (using the script above) and re-run the SQL to get the content of the cache you will now see;

Lot Genealogy Cache - Complete
The first thing you notice is there are a lot more records. The new records are for Item Number 07440 which is the one we were missing earlier.

If you do a source comparison between the new and old versions of the package you'll notice that this wasn't the only change I made - I've added a great deal more logging to the WIP Issue transaction so it's possible to see what is going on and what "SQL%ROWCOUNT = 0" actually means! I also added a warning to WIP Completion if no records are updated.

I hope this is of some interest ... Debugging an application, even one you've written yourself, is a skill that's very difficult to transfer. Hopefully there is enough information here for people to at least make a start for future issues (which will, by their very nature, be completely different I'm sure!).

Tuesday, April 9, 2013

Lot Genealogy (Part 2): Automating Testing of a Genealogy

If you are going to roll out any caching of your lot genealogy data the one clear thing you have to get right is testing. If, like us, you are working in a highly regulated environment it's necessary to ensure that you've got your testing right.

To this end I've created a new table called LOT_GENEALOGY_WHEREUSED_TESTS;

Lot_Genealogy_WhereUsed_Tests Table Description
The purpose of this new table is to hold copies of the lot genealogy data from Lot_Genealogy_WhereUsed. By holding these "snapshots" we can check after each rebuild of the cache that the data that was there and we had previously validated as correct is still there.

To create tests the following SQL will insert an existing Lot Genealogy into the testing table;

INSERT INTO LOT_GENEALOGY_WHEREUSED_TESTS
  SELECT 'ATR002', -- test_Ref
         'Product lot (166449) consists of two items (038003 and 038001)', -- test_description
         -- data from Lot Genealogy
         MASTER_LOT_NUMBER,
         INGRED_LOT_NUMBER,
         INGRED_ITEM_NUMBER,
         INGRED_ITEM_DESCRIPTION,
         INGRED_ITEM_TYPE_CODE,
         BATCH_NUMBER,
         PRODUCT_LOT_NUMBER,
         PRODUCT_ITEM_NUMBER,
         PRODUCT_ITEM_DESCRIPTION,
         PRODUCT_ITEM_TYPE_CODE
    FROM LOT_GENEALOGY_WHEREUSED LGW
   WHERE 1 = 1
     AND LGW.MASTER_LOT_NUMBER = '0490/0002';


This SQL is taking the lot genealogy for Master Lot Number 0490/0002 and copying it into the testing table, adding a test reference (ATR002) and a test description so we know what the test is supposed to be checking for.

That, as they say, was the easy bit. Now we need to create some SQL that is capable of running a test and returning a result. Our reporting tool (in case you can't tell from my other blog posts!) is SQL Server Reporting Services (SSRS) hence I'm going to split the SQL into two pieces, one to give me a list of all the tests and the other to run an individual test - SSRS will allow me to embed the latter as a sub-report into a report driven by the former.

List of Tests SQL

This was by far the easiest of the two;

SELECT DISTINCT LGWT.TEST_REF VALUE,
                LGWT.Test_Ref || ' (Lot ' || LGWT.MASTER_LOT_NUMBER || ')' LABEL
  FROM LOT_GENEALOGY_WHEREUSED_TESTS LGWT
 ORDER BY 1, 2


This returns something similar to;

List of Tests Generated Using SQL
I've added the bit of additional detail (for the Label) just so that in addition for being able to use this in the main report to get a list of tests I can also use it in the sub-report as a source for a picker on the "Test_Ref" parameter.

Run A Test SQL

This is slightly larger but here's the code then I'll try and explain it;

SELECT :Test_Ref "Test Ref",
       (SELECT TEST_DESCRIPTION
          FROM LOT_GENEALOGY_WHEREUSED_TESTS LGWT1
         WHERE LGWT1.TEST_REF = :Test_Ref
           AND ROWNUM = 1) "Test Description",
       TR.Test_Row_Count "Test Row Count",
       TR.Cache_Row_Count "Cache Row Count",
       TR.Union_Row_Count "Union Row Count",
       CASE
         WHEN TR.Test_Row_Count = TR.Cache_Row_Count AND
              TR.Cache_Row_Count = TR.Union_Row_Count THEN
          'PASS'
         ELSE
          'FAIL'
       END "Test Result"
  FROM (SELECT (SELECT COUNT(*)
                  FROM LOT_GENEALOGY_WHEREUSED_TESTS LGWT1
                 WHERE LGWT1.TEST_REF = :Test_Ref) Test_Row_Count,
               (SELECT COUNT(*)
                  FROM LOT_GENEALOGY_WHEREUSED LGW
                 WHERE LGW.MASTER_LOT_NUMBER =
                       (SELECT MASTER_LOT_NUMBER
                          FROM LOT_GENEALOGY_WHEREUSED_TESTS
                         WHERE TEST_REF = :Test_Ref
                           AND ROWNUM = 1)) Cache_Row_Count,
               (SELECT COUNT(*)
                  FROM (SELECT MASTER_LOT_NUMBER,
                               INGRED_LOT_NUMBER,
                               INGRED_ITEM_NUMBER,
                               INGRED_ITEM_DESCRIPTION,
                               INGRED_ITEM_TYPE_CODE,
                               BATCH_NUMBER,
                               PRODUCT_LOT_NUMBER,
                               PRODUCT_ITEM_NUMBER,
                               PRODUCT_ITEM_DESCRIPTION,
                               PRODUCT_ITEM_TYPE_CODE
                          FROM LOT_GENEALOGY_WHEREUSED_TESTS LGWT1
                         WHERE LGWT1.TEST_REF = :Test_Ref
                        UNION
                        SELECT MASTER_LOT_NUMBER,
                               INGRED_LOT_NUMBER,
                               INGRED_ITEM_NUMBER,
                               INGRED_ITEM_DESCRIPTION,
                               INGRED_ITEM_TYPE_CODE,
                               BATCH_NUMBER,
                               PRODUCT_LOT_NUMBER,
                               PRODUCT_ITEM_NUMBER,
                               PRODUCT_ITEM_DESCRIPTION,
                               PRODUCT_ITEM_TYPE_CODE
                          FROM LOT_GENEALOGY_WHEREUSED LGW
                         WHERE LGW.MASTER_LOT_NUMBER =
                               (SELECT MASTER_LOT_NUMBER
                                  FROM LOT_GENEALOGY_WHEREUSED_TESTS
                                 WHERE TEST_REF = :Test_Ref
                                   AND ROWNUM = 1))) Union_Row_Count
          FROM DUAL) TR


As you can see it takes a single parameter, the Test Reference Number. How it works is it counts the number of records in the cache, counts the number of records in the test table, and then does a select of all the records in the test table and, using a straight UNION, all the records in the cache. Because of the way UNIONs work (stripping out duplicates) the COUNT of the number of records returned by the UNION should be the same as the number of records in each of the other two queries. If they are all the same the TEST_RESULT is 'PASS' otherwise it's 'FAIL'.

NOTE: I'm sure this could be done a lot more efficiently but to be honest given the relative sizes of the tables I don't think you'll be sitting round too long for a result. On our system it takes less then .02 of a second. Your mileage will vary, but probably not by much!

Now that I've got the SQL I've setup a simple SSRS report to display the result for a single test back to me;

SSRS Report Showing A Single Test Result
You'll also notice that the test result includes a listing of all the records included in the test. This is a simple SELECT * ... WHERE TEST_REF = ... so I'm not going to give you the SQL for it.

The master report looks like this (in Report Builder 3);

SSRS Master Reporting Showing All Tests
I've included text boxes (on each page as part of the footer) to record who the Tester and Checker are and the Date. For our internal quality purposes we need this level of detail, you might not but what's the harm?

When executed the report appears like this;

SSRS Testing Report - Final Result
Hopefully you will find this useful.

Friday, March 8, 2013

Lot Genealogy (Part 1): Building a Cache From R12 Data

UPDATE: 15-APR-2013 fixed a bug with WIP Issue transactions, added in some more logging (see source code).

If you have had any opportunities to work using Oracle Process Manufacturing one of features that Oracle have removed in R12 is reporting on Lot Genealogy, specifically the ability to look at the entire genealogy for a lot.

I've tackled this issue previously using Noetix (see here) and this is the first part of a series in which I'll attempt to provide a means for generating a cache for the lot genealogy information that you can report on (rather than having to build the genealogy at the time you run the report) without using Noetix.

So here's the plan;

Lot Genealogy Cache: Process Overview
This plan is based on the established Lot and Batch cycle;

PO > Lot > Batch Cycle
It's the iterative nature of lots > batches and batches > lots that causes a lot of problems when you're writing reports. The key things the process above includes that the simplified diagram doesn't is the ability to reverse a the creation of batches from a lot and the consumption of batches into lots.

Looking at the process overview the first part we need to worry about is the second box down (Get Next Transaction Data). What we need to do in this process is get a sequential list of all transactions since the last time we ran the process. The SQL below will do this;

SELECT MMT.TRANSACTION_ID,
       GBH.Batch_No,
       MTLN.LOT_NUMBER,
       MTLN.TRANSACTION_QUANTITY,
       MTT.TRANSACTION_TYPE_NAME,
       MSIB.SEGMENT1             Item_Number,
       MSIT.DESCRIPTION          Item_Description,
       MSIB.ITEM_TYPE            Item_Type
  FROM INV.MTL_TRANSACTION_LOT_NUMBERS MTLN,
       INV.MTL_MATERIAL_TRANSACTIONS   MMT,
       INV.MTL_SYSTEM_ITEMS_B          MSIB,
       INV.MTL_SYSTEM_ITEMS_TL         MSIT,
       INV.MTL_LOT_NUMBERS             MLN, -- Used to identify lots that have been disabled
       GME.GME_BATCH_HEADER            GBH,
       INV.MTL_TRANSACTION_TYPES       MTT
 WHERE 1 = 1
   AND MTLN.TRANSACTION_ID = MMT.TRANSACTION_ID
   AND MTLN.INVENTORY_ITEM_ID = MMT.INVENTORY_ITEM_ID
   AND MTLN.ORGANIZATION_ID = MMT.ORGANIZATION_ID
   AND MTLN.TRANSACTION_SOURCE_TYPE_ID = 5 /* Job Or Schedule */
   AND MMT.INVENTORY_ITEM_ID = MSIB.INVENTORY_ITEM_ID
   AND MMT.Organization_Id = MSIB.Organization_Id
   AND MSIT.INVENTORY_ITEM_ID(+) = MSIB.INVENTORY_ITEM_ID
   AND MSIT.ORGANIZATION_ID(+) = MSIB.ORGANIZATION_ID
   AND MMT.Transaction_Source_ID = GBH.Batch_Id(+)
   AND MMT.TRANSACTION_TYPE_ID = MTT.TRANSACTION_TYPE_ID(+)
   AND MTLN.Lot_Number = MLN.Lot_Number
   AND MTLN.Inventory_Item_Id = MLN.Inventory_Item_Id
   AND MTLN.Organization_Id = MLN.Organization_Id
   AND MLN.Disable_Flag IS NULL;


Rather than embedding this into our process I think a good idea would be to create an external view (which I'm going to call LOT_GENEALOGY_GETTRANSACTIONS). The source code file for this is available here (via Google Drive).

The next thing we need to consider is how we're going to store the information in the cache itself. The following are source files for each required object;
Assuming you've built the view and the tables above the main package can then be built. The source is here.

Once that's been built you need to configure the settings table;

insert into LOT_GENEALOGY_SETTINGS (setting_name, value_number, value_text, value_date)
values ('MAX_TRANS_NO', -1, null, null);
insert into LOT_GENEALOGY_SETTINGS (setting_name, value_number, value_text, value_date)
values ('LOGGING', null, 'YES', null);
commit;

And then you can run the stored procedure;

begin
  -- Call the procedure
  lot_genealogy.refreshwholecache;
end;


That will populate the cache for the first time, you then need to schedule;

begin
  -- Call the procedure
  lot_genealogy.doupdate;
end;


Which will incrementally update the cache based on new transactions. We schedule this to run every 30 minutes but it will depend on your volume of transactions as to how often you want it to run!


Friday, August 31, 2012

Oracle R12 Lot Genealogy and Noetix

If, like me, you work in a process manufacturing* company one of the key things you need to worry about is lot genealogy. Put simply this is the process that a manufacturer uses to determine where the raw ingredients went. When you read about a product recall in the press this is the process the company will have gone through to trace where every product to be recalled went.

Here's a simple diagram of the process;

Simple Lot Genealogy

As is made (hopefully!) obvious in the diagram the main issue with reporting on Lot Genealogy is recursion. In Release 11 Oracle used to provide a lot genealogy report, it seems they have removed this report in Release 12 although the same information is available via the standard Oracle Forms (although if you have to file reports with the FDA or HMRC it's questionable how well "we've got it on the screen" will go down).

Each step in the above process is logged as an item transaction (amongst a lot of other events). There are four steps to the process;
  1. Which lots were created as a result of purchase order W?
  2. Which batches did lot X go into? 
  3. Which lots did batch Y go into?
  4. Which lots fulfilled sales order Z?
Working through these in turn and using the Noetix View INVG0_Lot_Transactions the SQL to see which lots were created by a receipt into inventory from a Purchase Order is;

SELECT ilt.lot_number
  FROM invg0_lot_transactions ilt
 WHERE 1 = 1
   AND ilt.PO_Number = 'XXX'
   AND ilt.transaction_type = 'PO Receipt';

Now the first thing you'll notice if you're familiar with the view is that I'm using a "PO Number" column - a column that doesn't exist in the basic view as provided by Noetix. In order to simplify not just Lot Genealogy but reporting in general we have added a number of customisations to this view. These are available as a single file here (via Google Docs). Strictly speaking it's not necessary to add these customisations as we could do it all in the script I'll show you below, but it does make it a great deal easier (for example do you prefer "DECODE( MATTR.TRANSACTION_SOURCE_TYPE_ID, 5, MATTR.Transaction_Source_ID, null)" or just Batch_Id?).

The second point, which batches did lot X go into, is a little more complicated as we are also at this stage looking to exclude batches where the lot was issues incorrectly and then fully returned (using the "WIP Issue" and "WIP Return" transaction types). See below for the SQL;

SELECT ilt.Batch_Number, SUM(ilt.TRANSACTION_QUANTITY)
  FROM invg0_lot_transactions ilt
 WHERE 1=1
   AND ilt.lot_number = 'XXX'
   AND ilt.Transaction_Source_Type = 'Job or Schedule'
   AND (ilt.transaction_type = 'WIP Issue' OR ilt.Transaction_Type = 'WIP Return') 
 GROUP BY ilt.batch_number
HAVING SUM(ilt.TRANSACTION_QUANTITY) < 0

The third point, which lots did batch X produce, goes back to the simple-style of the first

SELECT itd.LOT_NUMBER
  FROM invg0_lot_transactions itd
 WHERE 1=1
   AND itd.transaction_type = 'WIP Completion'
   AND itd.Transaction_Source_Type = 'Job or Schedule'
   AND itd.Batch_Number = 'XXX'

The key is "WIP Completion". Now this is where it gets a little interesting. With the SQL above we will pick up "Work in Progress" batches. These are where we have a multi-stage manufacturing process - such as granulation and then coating - and need to put the intermediary product on the system. So when the result of granulation is transported for coating it is stored on the system as a new batch and then the batch is emptied and input into the coating process - we could have filtered these out using the "WIP Completion Return" type in the same way we did above with WIP Issue/ WIP Return but the view I took when developing this is that correcting a mistake (as is the case with WIP Issue/ WIP Return) is different from materials passing through a batch as part of a process (WIP Completion). Hope that's clear?

The final part, which lots fulfil a sales order, is probably the simplest piece of SQL yet;

SELECT itd.LOT_NUMBER
  FROM invg0_lot_transactions itd
 WHERE 1=1
   AND itd.Sales_Order_Number = 'XXX'

Now we have the four parts of the story we need to work out lot genealogy it seems like it's just a simple matter of adding in a recursive lookup on batches to lots and we're there. And yes - you could do it that way but in my experience what you end up with is the recursion being done in different ways in different reports (for example if we are wanting to know where a raw material went in a report we might want to exclude packaging lots). You then find yourself having to "debug" a problem with a report and spending hours working out what this specific version of log genealogy is doing.

For this reason I have implemented a solution that processes the transactions as they are done and builds a cache of lot genealogy information (with a few bits of useful reporting information to prevent some of the more obvious lookups).

The first step is to build the tables. The three tables I'm using are;
  • Lot_Genealogy_Settings (this just holds a setting to record the last transaction processed),
  • Lot_Genealogy_WhereUsed (this holds the genealogy information from a "where used" perspective), and
  • Lot_Genealogy_Batch_Product (this holds a list of the products for each batch)
Starting with the simplest table the columns for Lot_Genealogy_Settings are;

Table Description: Lot_Genealogy_Settings

The creation script is available here (via Google Docs).

NOTE: You'll need to insert a single record to be picked up as the minimum transaction number (use -1) see the code for the package below to determine the values!

The columns for the Lot_Genealogy_Batch_Product are;

Table Description: Lot_Genealogy_Batch_Product
The creation script is available here (via Google Docs).

You'll notice when you look at the SQL that this table is indexed in order to speed up the retrieval of data. The time taken will depend on the amount of data you have but with the indexs (on this and the following table) the time taken to process eight months of data for us was about 3 minutes (that's approximately 571,000 lot transactions).

The final table (and the most important one) is Lot_Genealogy_WhereUsed, it's columns are;

Table Description: Lot_Genealogy_WhereUsed
The creation script for this table is available here (via Google Docs).

You'll notice that I'm creating all the tables in the NOETIX_SYS schema. The reason for this is fairly simple; as this package (and process) is dependant on Noetix and the Noetix Views will be rebuilt periodically I think it's better to have the creation script for the *package* (below) to be part of Noetix Regenerate; this way it gets automatically tested each time we do a build.

Now here's the script to create the package header;

create or replace package lot_genealogy is
  procedure doUpdate;
end lot_genealogy;

That's a little simple, here's the body;

create or replace package body lot_genealogy is

  procedure doUpdate as
    v_Exists               BOOLEAN;
    v_MaxTransactionNumber NUMBER;
  BEGIN
    -- SECTION 0100: Remove Any Existing Records From The Cache
    /*DELETE FROM NOETIX_SYS.Lot_Genealogy_WhereUsed;
    DELETE FROM NOETIX_SYS.LOT_GENEALOGY_BATCH_PRODUCT;
    COMMIT;
   
    UPDATE NOETIX_SYS.Lot_Genealogy_Settings LGS
       SET LGS.VALUE_NUMBER = -1
     WHERE LGS.Setting_Name = 'MAX_TRANS_NO';*/
 
    FOR v_Check IN (SELECT VALUE_NUMBER
                      FROM NOETIX_SYS.Lot_Genealogy_Settings LGS
                     WHERE LGS.Setting_Name = 'MAX_TRANS_NO') LOOP
      v_MaxTransactionNumber := v_Check.Value_Number;
    END LOOP;
 
    -- SECTION 1000: Process The Lot Transaction Records From INVG0_Lot_Transactions
    FOR v_Transaction IN (SELECT ilt.Transaction_Number,
                                 ilt.batch_number,
                                 ilt.transaction_quantity,
                                 ilt.lot_number,
                                 ilt.transaction_type,
                                 ilt.ITEM$Item,
                                 ilt.Item_Description,
                                 ilt.Item_Type_Code
                            FROM INVG0_LOT_TRANSACTIONS ilt
                           WHERE ilt.Transaction_Source_Type =
                                 'Job or Schedule'
                             AND (ilt.transaction_type = 'WIP Issue' OR
                                 --ilt.Transaction_Type = 'WIP Return' OR
                                 ilt.Transaction_Type = 'WIP Completion')
                             AND ilt.Transaction_Number >
                                 v_MaxTransactionNumber
                          --AND ilt.lot_number IN ('166130', '035598', '166127')
                          /*AND ilt.lot_number IN
                          ('029996',
                           '165507WIP',
                           '165507C1',
                           '165507',
                           '165583',
                           '167866WIP',
                           '167866',
                           '167951')*/
                          --AND ilt.transaction_number <= 2053180 -- For testing, about 6,000 records
                           ORDER BY ilt.Transaction_Number) LOOP
   
      -- SECTION 1100: Process WIP Completion Transaction
      -- NOTE: This section answers the question "What lots did batch X produce?"
      IF (v_Transaction.Transaction_Type = 'WIP Completion') THEN
        -- If we have already created records for this batch then we can update them with the new
        --   product UNLESS we already have a product for this batch
        UPDATE NOETIX_SYS.Lot_Genealogy_WhereUsed LGI
           SET LGI.PRODUCT_LOT_NUMBER       = v_Transaction.Lot_Number,
               LGI.Product_Item$item        = v_Transaction.Item$item,
               LGI.Product_Item_Description = v_Transaction.Item_Description,
               LGI.Product_Item_Type_Code   = v_Transaction.Item_Type_Code
         WHERE LGI.BATCH_NUMBER = v_Transaction.Batch_Number
           AND (LGI.PRODUCT_LOT_NUMBER IS NULL OR -- ... we haven't updated the product lot before
                LGI.PRODUCT_LOT_NUMBER = v_Transaction.Lot_Number); -- ... we have but this is the same
        IF SQL%ROWCOUNT = 0 THEN
          -- Nothing's been updated. New record?
          -- Insert the new record for each batch number where it already exists in the LG table (providing
          --  it doesn't already exist)
          INSERT INTO NOETIX_SYS.Lot_Genealogy_WhereUsed
            SELECT DISTINCT LGI.Master_Lot_Number, -- Mater_Lot_Number
                            LGI.INGRED_LOT_NUMBER, -- Ingred_Lot_Number
                            LGI.INGRED_ITEM$ITEM, -- INGRED_ITEM$ITEM
                            LGI.INGRED_ITEM_DESCRIPTION, -- INGRED_ITEM_DESCRIPTION
                            LGI.INGRED_ITEM_TYPE_CODE, -- INGRED_ITEM_TYPE_CODE
                            LGI.BATCH_NUMBER,
                            v_Transaction.Lot_Number, -- Product_Lot_Number
                            v_Transaction.ITEM$ITEM, -- PRODUCT_ITEM$ITEM
                            v_Transaction.ITEM_DESCRIPTION, -- PRODUCT_ITEM_DESCRIPTION
                            v_Transaction.ITEM_TYPE_CODE -- PRODUCT_ITEM_TYPE_CODE
              FROM NOETIX_SYS.Lot_Genealogy_WhereUsed LGI
             WHERE LGI.BATCH_NUMBER = v_Transaction.Batch_Number
               AND NOT EXISTS
             (SELECT 1
                      FROM NOETIX_SYS.Lot_Genealogy_WhereUsed LGI1
                     WHERE LGI1.MASTER_LOT_NUMBER = LGI.Master_Lot_Number
                       AND LGI1.Ingred_LOT_NUMBER = LGI.ingred_Lot_Number
                       AND LGI1.BATCH_NUMBER = LGI.Batch_Number
                       AND LGI1.PRODUCT_LOT_NUMBER =
                           v_Transaction.Lot_Number);
        END IF;
     
        -- In case this batch appears in another transaction later let's record
        --    what the completed products were
        INSERT INTO NOETIX_SYS.LOT_GENEALOGY_BATCH_PRODUCT
          SELECT v_Transaction.Batch_Number, -- Batch_Number,
                 v_Transaction.Lot_Number, -- Product_Lot_Number
                 v_Transaction.ITEM$Item, -- PRODUCT_ITEM$ITEM
                 v_Transaction.Item_Description, -- Product_Item_Description
                 v_Transaction.Item_Type_Code -- Product_Item_Type_Code
            FROM DUAL
           WHERE NOT EXISTS
           (SELECT 1
                    FROM NOETIX_SYS.     LGBP
                   WHERE LGBP.BATCH_NUMBER = v_Transaction.Batch_Number
                     AND LGBP.Product_Lot_Number = v_Transaction.Lot_Number);
      ELSIF (v_Transaction.Transaction_Type = 'WIP Issue') THEN
        v_Exists := False;
        -- Was this issue to the Batch reversed?
        FOR v_Check IN (SELECT 'X'
                          FROM INVG0_LOT_TRANSACTIONS ilt
                         WHERE ilt.Transaction_Source_Type =
                               'Job or Schedule'
                           AND ilt.Transaction_Type = 'WIP Return'
                           AND ilt.batch_number = v_Transaction.Batch_Number
                           and ilt.TRANSACTION_QUANTITY =
                               (v_Transaction.Transaction_Quantity * -1)
                           and ilt.LOT_NUMBER = v_Transaction.Lot_Number) LOOP
          v_Exists := True; -- Yes it was (WIP Issued in error)
        END LOOP;
     
        -- If the WIP Issue wasn't reversed ...
        IF (NOT v_Exists) THEN
          -- Get the batch product(s) (if we can) otherwise return NULL
          FOR v_Product IN (SELECT LGBP.PRODUCT_LOT_NUMBER,
                                   LGBP.PRODUCT_ITEM$ITEM,
                                   LGBP.PRODUCT_ITEM_DESCRIPTION,
                                   LGBP.PRODUCT_ITEM_TYPE_CODE
                              FROM NOETIX_SYS.LOT_GENEALOGY_BATCH_PRODUCT LGBP
                             WHERE LGBP.BATCH_NUMBER =
                                   v_Transaction.Batch_Number
                            UNION
                            SELECT NULL, NULL, NULL, NULL
                              FROM DUAL
                             WHERE NOT EXISTS
                             (SELECT 1
                                      FROM NOETIX_SYS.LOT_GENEALOGY_BATCH_PRODUCT LGBP
                                     WHERE LGBP.BATCH_NUMBER =
                                           v_Transaction.Batch_Number)) LOOP
            -- For each batch product create a LG record (if it doesn't already exist)
            INSERT INTO NOETIX_SYS.Lot_Genealogy_WhereUsed
              SELECT v_Transaction.Lot_Number, -- Master_Lot_Number
                     v_Transaction.Lot_Number, -- Ingred_Lot_Number
                     v_Transaction.Item$item, -- INGRED_ITEM$ITEM
                     v_Transaction.Item_Description, -- INGRED_ITEM_DESCRIPTION
                     v_Transaction.Item_Type_Code, -- INGRED_ITEM_TYPE_CODE
                     v_Transaction.Batch_Number, -- Batch_Number
                     v_Product.Product_Lot_Number, -- Product_Lot_Number
                     v_Product.PRODUCT_ITEM$ITEM, -- PRODUCT_ITEM$ITEM
                     v_Product.PRODUCT_ITEM_DESCRIPTION, -- PRODUCT_ITEM_DESCRIPTION
                     v_Product.PRODUCT_ITEM_TYPE_CODE -- PRODUCT_ITEM_TYPE_CODE
                FROM DUAL
               WHERE NOT EXISTS
               (SELECT 1
                        FROM NOETIX_SYS.Lot_Genealogy_WhereUsed LGI
                       WHERE LGI.MASTER_LOT_NUMBER =
                             v_Transaction.Lot_Number
                         AND LGI.Ingred_LOT_NUMBER =
                             v_Transaction.Lot_Number
                         AND LGI.Batch_Number = v_Transaction.Batch_Number
                         AND (v_Product.Product_Lot_Number IS NULL AND
                             LGI.PRODUCT_LOT_NUMBER IS NULL OR
                             v_Product.Product_Lot_Number =
                             LGI.PRODUCT_LOT_NUMBER));
            -- ... and add to existing master lot numbers with the same product lot
            INSERT INTO NOETIX_SYS.Lot_Genealogy_WhereUsed
              SELECT DISTINCT LGI.MASTER_LOT_NUMBER, -- Master_Lot_Number
                              v_Transaction.Lot_Number, -- Ingred_Lot_Number
                              v_Transaction.Item$item, -- INGRED_ITEM$ITEM
                              v_Transaction.Item_Description, -- INGRED_ITEM_DESCRIPTION
                              v_Transaction.Item_Type_Code, -- INGRED_ITEM_TYPE_CODE
                              v_Transaction.Batch_Number, -- Batch_Number
                              v_Product.Product_Lot_Number, -- Product_Lot_Number
                              v_Product.PRODUCT_ITEM$ITEM, -- PRODUCT_ITEM$ITEM
                              v_Product.PRODUCT_ITEM_DESCRIPTION, -- PRODUCT_ITEM_DESCRIPTION
                              v_Product.PRODUCT_ITEM_TYPE_CODE -- PRODUCT_ITEM_TYPE_CODE
                FROM NOETIX_SYS.Lot_Genealogy_WhereUsed LGI
               WHERE LGI.Product_Lot_Number = v_Transaction.Lot_Number
                 AND NOT EXISTS
               (SELECT 1
                        FROM NOETIX_SYS.Lot_Genealogy_WhereUsed LGI1
                       WHERE LGI1.MASTER_LOT_NUMBER = LGI.MASTER_LOT_NUMBER
                         AND LGI1.Ingred_LOT_NUMBER =
                             v_Transaction.Lot_Number
                         AND LGI1.Batch_Number = v_Transaction.Batch_Number
                         AND (v_Product.Product_Lot_Number IS NULL AND
                             LGI1.PRODUCT_LOT_NUMBER IS NULL OR
                             v_Product.Product_Lot_Number =
                             LGI1.PRODUCT_LOT_NUMBER));
          END LOOP;
        END IF;
      END IF;
      -- Record the transaction number we've just processed so we don't process it again
      v_MaxTransactionNumber := v_Transaction.Transaction_Number;
    END LOOP;
 
    -- Record the last transaction number we processed so we can go from there next time
    UPDATE NOETIX_SYS.Lot_Genealogy_Settings LGS
       SET LGS.VALUE_NUMBER = v_MaxTransactionNumber
     WHERE LGS.Setting_Name = 'MAX_TRANS_NO';
 
    COMMIT;
  end doUpdate;

end lot_genealogy;

Now you'll see that I've included a fair amount of comments on what the code is doing and also I've left in (but commented out) some of the debug lines I've been using. you can also tell my clear preference for using FOR ... LOOP instead of SELECT ... INTO statements (mainly so I don't have a temporary variable lying around or end up with a variable called v_Temp).

I hope this is fairly self-explanatory what it's doing. Let me know in the comments below if you have any questions or if anything is not clear.

*- Process Manufacturing means you take ingredients and you feed them into a process and make something as opposed to Discrete Manufacturing where you take parts and assemble them into something. The key difference is that you don't know how much (exactly) you will make in Process Manufacturing whereas for Discrete if you have all the parts of a car you will make precisely one car. Clear?