January 20, 2012
A request for assistance came in from an ERP mailing list. The original poster (OP) is running an unspecified version of Oracle Database 9i, and is in need of a solution to generate new part numbers with prefixed characters that describe the type of part, followed by a sequential number that is unique to the prefix. The prefixes might be PAINT, BAR, BEARING, DRILL, etc. Sample part numbers might include BAR0599, PAINT012, BEARING012345, etc.
When I first saw the request, my first thought was to create sequences for the different prefixes, similar to the following:
CREATE SEQUENCE PART_PAINT_ID START WITH 13 NOCACHE; CREATE SEQUENCE PART_BAR_ID START WITH 600 NOCACHE; CREATE SEQUENCE PART_BEARING_ID START WITH 12346 NOCACHE; CREATE SEQUENCE PART_DRILL_ID START WITH 999 NOCACHE; ...
Once the above sequences are created, we could then find the next part number with a SQL statement similar to the following:
SELECT 'PAINT'||PART_PAINT_ID.NEXTVAL NEXT_PART_ID FROM DUAL; NEXT_PART_ID ------------ PAINT13
I suspected that there was a catch – for some reason the sample part numbers included a 0 before the sequence number, and I assumed that there could be a variable number of 0 digits before that sequence number for the different prefixes. To fix the above, we might try working with the LPAD function to add leading zeros to the sequence number:
SELECT 'PAINT'||LPAD(TO_CHAR(PART_PAINT_ID.NEXTVAL), 3, '0') NEXT_PART_ID FROM DUAL; NEXT_PAR -------- PAINT014
Need more leading zeros? No problem, just adjust the number in the LPAD function:
SELECT 'PAINT'||LPAD(TO_CHAR(PART_PAINT_ID.NEXTVAL), 6, '0') NEXT_PART_ID FROM DUAL; NEXT_PART_I ----------- PAINT000015
Need fewer zeros?:
SELECT 'PAINT'||LPAD(TO_CHAR(PART_PAINT_ID.NEXTVAL), 1, '0') NEXT_PART_ID FROM DUAL; NEXT_P ------ PAINT1
The above shows the first of several potential problems with this approach. What else could go wrong? What if a smart person decides that he does not need this “crutch” solution and creates 30 part numbers using the method of best guess or cheat sheet in the side drawer? Because these are primary key values, the smart person might cause a number of problems that might not be detected for some time… until the Oracle sequence reaches one of the unexpected sequence numbers that were already used.
In addition to the suggestion of Oracle sequences, I offered a couple of other suggestions. The ERP system offers a macro language that is a lot like VBScript. The OP could create a VBScript that builds a HTML web page in real time, or possibly pull the next sequence number from a ASP (or similar) web page. As an example of a HTML web page built in real time, this is an example that I created roughly three years ago:
Dim objIE
Dim objShell
Dim strHTML
Dim intFlag
On Error Resume Next
Set objShell = CreateObject("WScript.Shell")
strHTML = strHTML & "<form name=""Visual"">" & vbCrLf
strHTML = strHTML & "<input type=hidden id=txtOK value="" "">" & vbCrLf
strHTML = strHTML & "<table>" & vbCrLf
strHTML = strHTML & "<tr><td>Component<td><select size=""1"" id=""cboComponent"" name=""cboComponent"" style=""width:150"">" & vbCrLf
strHTML = strHTML & "<option value=""ACTIVATOR"">ACTIVATOR</option>" & vbCrLf
strHTML = strHTML & "<option value=""ACCELERATOR"">ACCELERATOR</option>" & vbCrLf
strHTML = strHTML & "<option value=""CATALYST"">CATALYST</option>" & vbCrLf
strHTML = strHTML & "<option value=""EPOXY PRIMER"">EPOXY PRIMER</option>" & vbCrLf
strHTML = strHTML & "<option value=""PRIMER"">PRIMER</option>" & vbCrLf
strHTML = strHTML & "<option value=""REDUCER"">REDUCER</option>" & vbCrLf
strHTML = strHTML & "<option value=""TOP COAT"">TOP COAT</option>" & vbCrLf
strHTML = strHTML & "</select></td></tr>" & vbCrLf
strHTML = strHTML & "<tr><td>Vendor<td><select size=""1"" id=""cboVendor"" name=""cboVendor"" style=""width:150"">" & vbCrLf
strHTML = strHTML & "<option value=""DUPONT"">DUPONT</option>" & vbCrLf
strHTML = strHTML & "<option value=""LILLY"">LILLY</option>" & vbCrLf
strHTML = strHTML & "<option value=""NILES CHEMICAL"">NILES CHEMICAL</option>" & vbCrLf
strHTML = strHTML & "<option value=""MANITOWOC"">MANITOWOC</option>" & vbCrLf
strHTML = strHTML & "<option value=""MAUTZ"">MAUTZ</option>" & vbCrLf
strHTML = strHTML & "<option value=""PAINTS AND SOLVENTS"">PAINTS AND SOLVENTS</option>" & vbCrLf
strHTML = strHTML & "<option value=""SHEBOYGAN"">SHEBOYGAN</option>" & vbCrLf
strHTML = strHTML & "</select></td></tr>" & vbCrLf
strHTML = strHTML & "<tr><td>Type<td><select size=""1"" id=""cboType"" name=""cboType"" style=""width:150"">" & vbCrLf
strHTML = strHTML & "<option value=""FLAT"">FLAT</option>" & vbCrLf
strHTML = strHTML & "<option value=""GLOSS"">GLOSS</option>" & vbCrLf
strHTML = strHTML & "<option value=""MED. GLOSS"">MED. GLOSS</option>" & vbCrLf
strHTML = strHTML & "</select></td></tr>" & vbCrLf
strHTML = strHTML & "<tr><td>Color<td><select size=""1"" id=""cboColor"" name=""cboColor"" style=""width:150"">" & vbCrLf
strHTML = strHTML & "<option value=""RED"">RED</option>" & vbCrLf
strHTML = strHTML & "<option value=""YELLOW"">YELLOW</option>" & vbCrLf
strHTML = strHTML & "<option value=""GREEN"">GREEN</option>" & vbCrLf
strHTML = strHTML & "<option value=""BLUE"">BLUE</option>" & vbCrLf
strHTML = strHTML & "<option value=""BLACK"">BLACK</option>" & vbCrLf
strHTML = strHTML & "<option value=""WHITE"">WHITE</option>" & vbCrLf
strHTML = strHTML & "<option value=""GRAY"">GRAY</option>" & vbCrLf
strHTML = strHTML & "</select></td></tr>" & vbCrLf
strHTML = strHTML & "</table>" & vbCrLf
strHTML = strHTML & "<p><center><input type=button value=""OK"" id=cmdOK onclick=""document.getElementById('txtOK').value='OK';""></center>" & vbCrLf
strHTML = strHTML & "</form>" & vbCrLf
Set objIE = CreateObject("InternetExplorer.Application")
objIE.Navigate "about:blank"
objIE.Document.Body.InnerHTML = strHTML
objIE.Document.Title="Get Part Info"
objIE.Left = 0
objIE.Top = 0
objIE.Width = 400
objIE.Height = 400
objIE.Statusbar = False
objIE.Menubar = False
objIE.Toolbar = False
objIE.Visible = True
Do While objIE.Busy <> False
objShell.Sleep 200
Loop
intFlag = 0
'loop until the button is clicked
Do While intFlag = 0
If Err <> 0 Then
IntFlag = -1
End If
If objIE is Nothing Then
'User closed ID
intFlag = -1
Else
If objIE.Document.All.txtOK.Value <> " " Then
intFlag = 1
End If
End If
'objShell.Sleep 250 'Throws an error?
Loop
If intFlag = 1 Then
'Copy in the values from the web page
USER_1 = objIE.Document.Body.All.cboComponent.Value
USER_2 = objIE.Document.Body.All.cboVendor.Value
USER_3 = objIE.Document.Body.All.cboType.Value
USER_4 = objIE.Document.Body.All.cboColor.Value
objIE.Quit
End If
Set objIE = Nothing
Set objShell = Nothing
The result of the above VBScript is an interactive interface that appears similar to the following:
Another option that I suggested to the OP is to use Excel to keep track of the last sequence number for each prefix – and use an Excel dialog displayed from a VBScript macro. Roughly three years ago I created a sample macro with the following code:
Dim objExcel
Dim objForm
Dim objShell
On Error Resume Next
Set objExcel = CreateObject("Excel.Application")
'with the help of custom program, set a 1 second delay, then force the window to the top
Set objShell = CreateObject("WScript.Shell")
objShell.Run("C:\BringToTop.exe " & Chr(34) & "Paint Naming" & Chr(34) & " 2")
Set objShell = Nothing
With objExcel
.Workbooks.Open "C:\ExcelMacroDialog.xls"
If .Sheets("CalculateArea").Cells(1, 1).Value <> "" Then
ID = .Sheets("CalculateArea").Cells(1, 1).Value
DESCRIPTION = .Sheets("CalculateArea").Cells(2, 1).Value
PRODUCT_CODE = .Sheets("CalculateArea").Cells(3, 1).Value
COMMODITY_CODE = .Sheets("CalculateArea").Cells(4, 1).Value
USER_5 = .Sheets("CalculateArea").Cells(5, 1).Value
PURCHASED = True
FABRICATED = False
End If
End With
objExcel.DisplayAlerts = False
objExcel.ActiveWorkbook.Saved = True
objExcel.Quit
Set objExcel = Nothing
The above macro is quite short, because most of the work is performed in the Excel spreadsheet. The resulting Excel dialog appeared similar to this:
My first suggestion to the OP, not knowing the full scope of the problem, was to try coding a VBScript macro similar to the following:
Dim strPartID Dim strPartIDNew Dim strNumberOld Dim strNumberNew Dim i strPartID = PART_ID strNumberOld = "" 'strPartID = "PAINT0599" 'Remove this line after testing strPartID = "PAINT0089" 'Remove this line after testing For i = Len(strPartID) to 1 Step -1 If (Mid(strPartID, i, 1) >= "0") And (Mid(strPartID, i, 1) <= "9") Then strNumberOld = Mid(strPartID, i, 1) & strNumberOld Else strNumberNew = cStr(cInt(strNumberOld) + 1) strPartIDNew = Left(strPartID, i) If Len(strNumberOld) > Len(strNumberNew) Then 'Add Padding 0s strPartIDNew = strPartIDNew & String((Len(strNumberOld) - Len(strNumberNew)), "0") End If strPartIDNew = strPartIDNew & strNumberNew Exit For End If Next If strPartIDNew <> "" Then Msgbox "The New Part ID is " & strPartIDNew Else Msgbox "Not a Valid Starting Point" & strPartID End If
The intention of the above macro is to locate the number 89 in the supplied strPartID variable, recognize that a 4 digit serial number is expected, and output:
The New Part ID is PAINT0090
Nice, but that is not what the OP needs. The highest currently sequenced number will not be provided – that value must be looked up in the database. So close…
Let’s try a different approach, starting by creating a sample table with three sequences of part numbers with different prefixes:
CREATE TABLE T1 ( ID VARCHAR2(30), DESCRIPTION VARCHAR2(40), PRIMARY KEY(ID)); INSERT INTO T1 SELECT 'PAINT'||LPAD(TO_CHAR(ROWNUM), 6, '0') ID, 'TESTING ' || TO_CHAR(ROWNUM) DESCRIPTION FROM DUAL CONNECT BY LEVEL<=35000; INSERT INTO T1 SELECT 'BAR'||LPAD(TO_CHAR(ROWNUM), 6, '0') ID, 'TESTING ' || TO_CHAR(ROWNUM) DESCRIPTION FROM DUAL CONNECT BY LEVEL<=45000; INSERT INTO T1 SELECT 'BEARING'||LPAD(TO_CHAR(ROWNUM), 6, '0') ID, 'TESTING ' || TO_CHAR(ROWNUM) DESCRIPTION FROM DUAL CONNECT BY LEVEL<=888; COMMIT;
Let’s find the next sequence number for the BAR prefix:
SELECT MAX(TO_NUMBER(SUBSTR(ID,4))) + 1 NN FROM T1 WHERE ID BETWEEN 'BAR0' AND 'BAR99999999'; NN ---------- 45001
As long as ALL of the characters after the BAR keyword prefix are numbers, the above would tell us that the next number with BAR as the prefix is 45001. On Oracle Database 10.1 and higher it would be a good idea to add an additional predicate to the WHERE clause that uses regular expressions to avoid potential problems where some unrelated ID column values start with the letters BAR, a number character, and then at some position to the right contain a letter character (that condition would cause the above SQL statement to fail).
Building onto the above SQL statement, we could just retrieve the next part number in the sequence from the database, when provided any existing prefixed part number as the starting point:
SELECT 'BAR' || LPAD(TO_CHAR(MAX(TO_NUMBER(SUBSTR(ID,4))) + 1), 6, '0') NEXT_PART_ID FROM T1 WHERE ID BETWEEN 'BAR0' AND 'BAR99999999'; NEXT_PART --------- BAR045001
We are able to take the above SQL statement and incorporate it into a VBScript macro to find the next prefixed sequential number for the primary key column:
Dim strPartID
Dim strPartIDNew
Dim strNumberOld
Dim strNumberNew
Dim i
Dim strUserName
Dim strPassword
Dim strDatabase
Dim strSQL
Dim dbDatabase
Dim snpData
On Error Resume Next
strUsername = "MyUserID"
strPassword = "MyPassword"
strDatabase = "MyDatabase"
Set snpData = CreateObject("ADODB.Recordset")
Set dbDatabase = CreateObject("ADODB.Connection")
dbDatabase.ConnectionString = "Provider=OraOLEDB.Oracle;Data Source=" & strDatabase & ";User ID=" & strUserName & ";Password=" & strPassword & ";"
dbDatabase.Open
strNumberOld = ""
'strPartID = PART_ID
strPartID = "BEARING0599" 'Remove when finished testing
'strPartID = "BAR0089" 'Remove when finished testing
'strPartID = "PAINT0089" 'Remove when finished testing
For i = Len(strPartID) to 1 Step -1
If (Mid(strPartID, i, 1) >= "0") And (Mid(strPartID, i, 1) <= "9") Then
strNumberOld = Mid(strPartID, i, 1) & strNumberOld
Else
strPartIDNew = Left(strPartID, i)
strSQL = "SELECT" & VBCrLf
strSQL = strSQL & " '" & strPartIDNew & "' || LPAD(TO_CHAR(MAX(TO_NUMBER(SUBSTR(ID," & (i+1) &"))) + 1), 6, '0') NEXT_PART_ID" & VBCrLf
strSQL = strSQL & "FROM" & VBCrLf
strSQL = strSQL & " T1" & VBCrLf
strSQL = strSQL & "WHERE" & VBCrLf
strSQL = strSQL & " ID BETWEEN '" & strPartIDNew & "0' AND '" & strPartIDNew & "99999999'"
snpData.Open strSQL, dbDatabase
If snpData.State = 1 Then
If Not(snpData.EOF) Then
strPartIDNew = snpData("next_part_id")
End If
snpData.Close
End If
Exit For
End If
Next
If strPartIDNew <> "" Then
Msgbox "The New Part ID is " & strPartIDNew
Else
Msgbox "Not a Valid Starting Point" & strPartID
End If
dbDatabase.Close
Set snpData = Nothing
Set dbDatabase = Nothing
The OP put together a parallel solution that also used a VBScript macro. The macro sent a SQL statement very similar to the following to the database:
SELECT ID FROM T1 WHERE ID LIKE 'BAR%';
In the macro code the OP parsed each of the returned ID values to determine the highest sequence number, added 1 to that value, padded the new highest sequence number with “0′ digits and output the result. Most likely due to curiosity, the OP asked why I did not simply use his VBScript macro as part of my proposed solution. What reasons do you think that I gave to the OP?



Recent Comments