Access VBA function While...Wend or Do ...Loop - ms-access

I am working on a function for my access database that fills in a form field in my task form automatically based on the data entered in products forms.
Function IsProductReceived(varID As Variant) As String
Dim rst As New ADODB.Recordset
Dim strSQL As String
Dim lngTOID As Long
Dim strReceiveDate As Date
Dim bAcceptable As Boolean
On Error GoTo ErrorHandler
If IsNull(varID) Then
IsProductReceived = "TBD"
Else
lngTOID = varID
strSQL = "SELECT tblProduct.TaskID, tblProduct.Received, tblProduct.Acceptable FROM tblProduct WHERE tblProduct.TaskID = " & lngTOID
rst.Open strSQL, CurrentProject.Connection, adOpenDynamic, adLockOptimistic
If rst.BOF And rst.EOF Then
IsProductReceived = "TBD"
Exit Function
Else
While rst.EOF = False
If rst![Received] <> "" Then
strReceiveDate = rst![Received]
bAcceptable = rst![Acceptable]
If IsDate(strReceiveDate) Then
If bAcceptable = False Then
IsProductReceived = "YES/NOT ACCEPTED"
Else
IsProductReceived = "YES/ACCEPTED"
End If
Else
IsProductReceived = "NO"
End If
Else
IsProductReceived = "NO"
End If
rst.MoveNext
Wend
End If
rst.Close
Set rst = Nothing
End If
Exit Function
ErrorHandler:
MsgBox Err.Description
Err.Clear
If rst.State = adStateOpen Then
rst.Close
Set rst = Nothing
End If
End Function
There is often more that one product forms related to the task form and products are received at different times. I want the "IsProductReceived = "no" to remain on the task form until ALL products related to the task are received.
This code seems to be working as long as the first product has not been received. I can seem to figure out how to make it remain "no" until all products are received.
I currently am using a while/wend, I have attempted a Do/loop but am still not having satisfactory results. Any help would be much appreciated

How about:
Function IsProductReceived(TaskID) As String
Dim product As New ADODB.Recordset
Dim sql As String
Dim countAll As Integer
Dim countReceived As Integer
Dim countAccepted As Integer
IsProductReceived = "TBD"
If Not IsNumeric(TaskID) Then Exit Function
sql = "SELECT Received, Acceptable FROM tblProduct WHERE TaskID = " & TaskID
product.Open sql, CurrentProject.Connection, adOpenDynamic, adLockOptimistic
While Not product.EOF
countAll = countAll + 1
If IsDate(product!Received) Then countReceived = countReceived + 1
If product!Acceptable Then countAccepted = countAccepted + 1
product.MoveNext
Wend
product.Close
If countAll = 0 Then
IsProductReceived = "No"
ElseIf countAll = countAccepted Then
IsProductReceived = "YES/ACCEPTED"
ElseIf countAll = countReceived Then
IsProductReceived = "YES/NOT ACCEPTED"
Else
IsProductReceived = "No"
End If
End Function
A few notes:
Indent your code better.
Drop the faux Hungarian notation, use descriptive variable names.
Avoid deep nesting, especially when it comes to determining the return value.
Check parameters and exit early if the check fails. This removes nesting depth from the function.
Avoid Variant parameter types unless the function must deal with different data types. Here an Integer or Long type would probably be a better fit. (Using a typed function parameter removes the need for a type check entirely.)
While x = False is an antipattern. Use While Not x.
No need to save recordset fields in local variables first. Just use them directly.
Avoid building SQL from string concatenation. After an IsNumeric() check the above is probably okay, but you really should use parameterized queries.

The issue I'm seeing with your code is that you're getting a record set from a table, looping through the set and testing "Recieved" and then assigning a return value for your function after each test. Effectively, you're just returning the value of the very last record in the recordset. Perhaps instead of setting the value of isProductRecieved inside the While loop, set a bool value to false whenever you encounter a product that hasn't been recieved and then set the return value of the function after the loop:
Dim receive As Boolean
Dim accept As Boolean
receive = True
accept = False
If rst![Received] <> "" Then
strReceiveDate = rst![Received]
bAcceptable = rst![Acceptable]
If IsDate(strReceiveDate) Then
If bAcceptable = False Then
accept = False
Else
accept = True
End If
Else
receive = False
End If
Else
receive = False
End If
So now, if "receive" makes it all the way to the end of your while loop, you know that each product is received but if any product was not received it would be set to false. You could also build a short circuit in there to make it a tiny bit faster.

Related

How can I get the value from a control using the name of the control source?

I am trying to write some code to audit changes made via a form. I have a function that works to do this:
Function WriteChanges()
Dim f As Form
Dim c As Control
Dim user As String
Dim db As DAO.Database
Dim cnn As ADODB.Connection
Dim MySet As ADODB.Recordset
Dim tbld As TableDef
Dim recsource As String
Set f = Screen.ActiveForm
Set db = CurrentDb
Set cnn = CurrentProject.Connection
Set MySet = New ADODB.Recordset
recsource = f.RecordSource
Set tbld = db.TableDefs(recsource)
pri_key = fFindPrimaryKeyFields(tbld)
Debug.Print "pri_key: "; pri_key
user = Environ("username")
MySet.Open "Audit", cnn, adOpenDynamic, adLockOptimistic, adCmdTable
For Each c In f.Controls
Select Case c.ControlType
Case acTextBox, acComboBox, acListBox, acOptionGroup
If c.Value <> c.OldValue Then
With MySet
.AddNew
![EditDate] = Now
![user] = user
![SourceTable] = f.RecordSource
![SourceField] = c.ControlSource
![BeforeValue] = c.OldValue
![AfterValue] = c.Value
.update
End With
End If
End Select
Next c
MySet.Close
Set MySet = Nothing
Set f = Nothing
Set db = Nothing
End Function
I use this function on the before update property of various forms and it populates the Audit table with the details of the changes to values in each of the controls. I need to also get the value from the primary key field to add to the Audit table. I can use the following code to identify the name of the primary key within the form's record source:
Function fFindPrimaryKeyFields(tdf As TableDef) As String
Dim idx As Index
On Error GoTo HandleIt
For Each idx In tdf.Indexes
If idx.Primary Then
fFindPrimaryKeyFields = Replace(idx.Fields, "+", "")
GoTo OutHere
End If
Next idx
OutHere:
Set idx = Nothing
Exit Function
HandleIt:
Select Case Err
Case 0
Resume Next
Case Else
Beep
MsgBox Err & " " & Err.Description, vbCritical + vbOKOnly, "Error"
fFindPrimaryKeyFields = vbNullString
Resume OutHere
End Select
End Function
How can I use this to get the value from the control (text box) that has the identified primary key as its control source.
Please forgive any silly errors in my code as I'm a relative novice. Thanks in advance for any help.
I'm not 100% sure what you want exactly, but if you have the name of the field, you can use the following:
Dim primaryKeyValue As Variant
Dim primaryKeyColumnName As String
primaryKeyColumnName = fFindPrimaryKeyFields(TableDefs!MyTable)
Dim f as Form
'Get the form somehow
Dim c As Control
On Error GoTo NextC 'Escape errors because lots of controls don't have a control source
For Each c In f.Controls
If c.ControlSource = primaryKeyColumnName Then
primaryKeyValue = c.Value
End If
NextC:
Next c
On Error GoTo 0
If the primary key column is part of the form record source, you can simply read it by:
Debug.Print "PK value: " & f(pri_key)
Every column of the record source is a form property at runtime, independent of whether there is a control with the same name.
Note: your whole construct will stop working if you have a form that is based on a query that joins multiple tables.

Access VBA - rs.findfirst using regex validation

Hi all first time post.
I'm running a database that has to check data once imported in. I already use one findfirst that works fine which is just checking length of a string. Next I wanted to find any that didn't match a certain pattern in the same field.
Record set is simply
strSQL = "SELECT * FROM TblOrder ORDER BY ID"
Set rs = CurrentDb.OpenRecordset(strSQL, dbOpenDynaset)
The first find that works is;
rs.FindFirst ("Len(string) > 13")
If rs.NoMatch Then
MsgBox "All good"
Else
Do While Not rs.NoMatch
MsgBox "String over 13 - " & rs.AbsolutePosition + 1
rs.FindNext ("Len(string) > 13")
Loop
rs.FindNext ("Len(string) > 13")
End If
So that one is no problem but the one below is where I can't get it to work;
rs.FindFirst ("Validation(string, ""123\d{10}$"") = False")
If rs.NoMatch Then
MsgBox "All are correct pattern"
Else
Do While Not rs.NoMatch
rs.FindNext (Validation(string, "123\d{10}$") = False)
MsgBox "Must start with 978 - " & rs.AbsolutePosition
Loop
rs.FindNext (Validation(string, "123\d{10}$") = False)
End If
I've tried without quotes like on the findnext lines and with as per the findfirst. Without finds null value for the string and with gets a data type mismatch. The string is a number like 123456789 but stored in table as text. The validation works fine if I look through the recordset one row at a time like below;
If Validation(rs!string, "978\d{10}$") = False Then
but I want to scan the entire column rather than go line by line. Line by line was taking too long.
The validation function code is below;
Function Validation(ByRef StrInput As String, ByVal strPattern As String) As String
Dim regex As Object
Set regex = CreateObject("VBScript.RegExp")
With regex
.Pattern = strPattern
.IgnoreCase = True
.Global = False
.MultiLine = False
End With
If regex.Test(StrInput) = True Then
Validation = True
Else
Validation = False
End If
End Function
Don't worry about this I've fixed it. False needed to be in speech-marks.

Adding parameter to existing code to export an Access Query to Excel

'Below is the current code that I have and it will export to the excel workbook and worksheet correctly. The only problem is that I need to limit the data that gets exported by a month end date range (example: 1/31/2017 to 4/30/2017) and also by a plant number (example: "4101")
thanks for any help it is greatly appreciated.
Public Function InventoryXport_4100()
Dim appXL As Object
Dim wb As Object
Dim wks As Object
Dim xlf As String
Dim rs As DAO.Recordset
Dim fld As Field
Dim intColCount As Integer
xlf = "Z:\COST ACCOUNTING INFO\Inventory Reports\MyFile.xlsx"
Set rs = CurrentDb.OpenRecordset("(QS)_Inventory")
Set appXL = CreateObject("Excel.Application")
Set wb = appXL.Workbooks.Open(xlf)
Set wks = wb.Sheets("Inventory Xport") 'Sheet name
If rs.EOF = True Then
MsgBox "No data", vbOKOnly
Exit Function
End If
With appXL
.Application.worksheets("Inventory Xport").SELECT
.Application.columns("A:AQ").SELECT
.Application.columns.Clear
End With
intColCount = 1
For Each fld In rs.Fields
wks.Cells(1, intColCount).Value = fld.Name
intColCount = intColCount + 1
Next fld
appXL.displayalerts = False
wks.Range("A2").CopyFromRecordset rs
appXL.Visible = True
With appXL
.Application.worksheets("Inventory Xport").SELECT
.Application.columns("A:AQ").SELECT
.Application.columns.AutoFit
.Application.Range("A2").SELECT
.Application.ActiveWindow.FreezePanes = True
End With
wb.Save
wb.Close
appXL.Quit
Set wb = Nothing
rs.Close
Set rs = Nothing
End Function
You need to put the filter conditions into a where clause on your query:
Set rs = CurrentDb.OpenRecordset("SELECT * FROM [(QS)_Inventory] WHERE ...")
If you need help with the syntax of the where clause you will need to post the structure of your query (i.e. at least give the names and data types of the columns that you want to filter on). Also, if you want to pass these parameters into the function then you'll need to include them in the Function declaration with suitable parameter names, and replacing each ???? with the appropriate data type:
Public Function InventoryXport_4100(prm1 As ????, prm2 As ????, prm3 As ????)
By the way, why is this a Function rather than a Proc - there's no return value or even return data type set?
Edit following comments back and forth:
So try something like
Set rs = CurrentDb.OpenRecordset("SELECT * FROM [(QS)_Inventory] " & _
"WHERE [Plant] = '4101' " & _
"AND [Per] Between #01/31/2017# And #04/30/2017#")

Tracking value changes when editing a record in Access

I am trying to track value changes when editing a record. All fields on the form are unbound text box.
Below is a function that is used to insert a audit tracking record.
Public Function AuditChanges(RecordID As String, UserAction As String)
Dim DB As Database
Dim rst As Recordset
Dim clt As Control
Dim Userlogin As String
Set DB = CurrentDb
Set rst = DB.OpenRecordset("select * from tbl_audittrail", adOpenDynamic)
Userlogin = Environ("username")
Select Case UserAction
Case "Edit"
For Each clt In Screen.ActiveForm.Controls
If (clt.ControlType = acTextBox _
Or clt.ControlType = accombox) Then
If Nz(clt.Value) <> Nz(clt.OldValue) Then
With rst
.AddNew
![DateTime] = Now()
!UserName = Userlogin
!FormName = Screen.ActiveForm.Name
!Action = UserAction
!RecordID = Screen.ActiveForm.Controls(RecordID).Value
!FieldName = clt.ControlSource
!OldValue = clt.OldValue
!Newvalue = clt.Value
.Update
End With
End If
End If
Next clt
End Select
rst.Close
DB.Close
Set rst = Nothing
Set DB = Nothing
End Function
Below is how I use the function:
Private Sub btnUpdate_Click()
Set DB = CurrentDb
Set rs = DB.OpenRecordset("SELECT * FROM ASID", dbOpenDynaset, dbSeeChanges)
rs.Edit
rs!ISIN = Me.ISIN
rs!SECIDTYPE = Me.SECIDTYPE
rs!ALTSECID = Me.ALTSECID
rs.Update
Call AuditChanges("ISIN", "Edit")
End If
End Sub
The problem is when it calls AuditChanges, it goes directly from
If (clt.ControlType = acTextBox _
Or clt.ControlType = accombox)
to End If
All fields on current form are unbound text boxes and you have to press an "Add" command button to actually add a record. I think there must be something wrong with the control type but I am not sure which control type should be used. Any idea?
Start by putting Option Explicit at the top of each module.
It enforces variable declaration and reports undeclared or misspelled variables/constants at compile time.
To have this automatically in new modules, set the Require Variable Declaration option in the VBA Editor.
This is really a must have for VBA development.
Then the compiler will tell you that accombox doesn't exist, it should be acComboBox
Without Option Explicit, accombox is initialized as NULL variant, and causes your entire If condition to be NULL, and therefore never be entered.

Compare two recordset variables gives type mismatch

I have a bound form with several subforms. some of these subforms can 0 or more records, others have 1 or more.
The form is always open in read-only and on it there are an "edit" and a "close" button.
When the user clicks on the edit button I save the content of the current record togehter with all records of the subforms so that when he/she clicks on the close button I can ask wether to save or not and, if not, discard the changes restoring from saved records.
So far this is the code of the edit button (where GclnAllCnts is a global variable of type Dictionary):
Private Sub EditLibroBtn_Click()
On Error GoTo Err_EditLibroBtn_Click
Dim lngID As Long
Dim ctlCnt As Control
Dim rs As Recordset
lngID = Me.ID
Set GclnAllCnts = New Dictionary
GclnAllCnts.Add Me.Name, Me.RecordsetClone
For Each ctlCnt In Me.Controls
If (ctlCnt.ControlType = acSubform) Then
Set rs = ctlCnt.Form.RecordsetClone
If rs.RecordCount > 0 Then
GclnAllCnts.Add ctlCnt.Name, ctlCnt.Form.RecordsetClone
Else
GclnAllCnts.Add ctlCnt.Name, Null
End If
End If
Next
DoCmd.Close acForm, Me.Name
DoCmd.OpenForm GCstMainFrmName, , , "ID = " & lngID, acFormEdit, acDialog
Exit_EditLibroBtn_Click:
Set ctlCnt = Nothing
Set rs = Nothing
Exit Sub
Err_EditLibroBtn_Click:
MsgBox err.Description & vbNewLine & "Error number: " & err.Number, vbCritical, "Errore"
Resume Exit_EditLibroBtn_Click
End Sub
And this is the code of the close button:
Private Sub ChiudiBtn_Click()
On Error GoTo Err_ChiudiBtn_Click
Dim intBoxAwr As Integer
Dim stSQL As String
Dim vKey As Variant
Dim ctlCnt As Control
Dim clnAllCnts As Dictionary
Dim bSaveNeeded As Boolean
bSaveNeeded = False
If (Me.AllowEdits And Me.ID <> "" And Not IsNull(Me.ID)) Then
Set clnAllCnts = New Dictionary
clnAllCnts.Add Me.Name, Me.RecordsetClone
For Each ctlCnt In Me.Controls
If (ctlCnt.ControlType = acSubform) Then
Set rs = ctlCnt.Form.RecordsetClone
If rs.RecordCount > 0 Then
clnAllCnts.Add ctlCnt.Name, ctlCnt.Form.RecordsetClone
Else
clnAllCnts.Add ctlCnt.Name, Null
End If
End If
Next
If clnAllCnts.Count <> GclnAllCnts.Count Then
bSaveNeeded = True
Else
For Each vKey In clnAllCnts.keys()
If Not GclnAllCnts.Exists(vKey) Then
bSaveNeeded = True
Exit For
Else
'*********** Next Gives error **********
If clnAllCnts.Item(vKey) <> GclnAllCnts.Item(vKey) Then
bSaveNeeded = True
Exit For
End If
End If
Next
End If
If bSaveNeeded Then
intBoxAwr = MsgBox("Salvare le modifiche al libro?", vbYesNo + vbQuestion, "Salvare")
If intBoxAwr = vbYes Then
'etc., omitting code
End Sub
The error I get is Type mismatch (nr. 13) and it is given by the <> comparison (I can Debug.print IsNull(clnAllCnts.Item(vKey)) and IsNull(GclnAllCnts.Item(vKey)).
How can I compare the two recordset variables?
Comparing two Recordset objects by simply saying If rst1 <> rst2 could be dicey anyway, because what does that really mean? Such an expression could very well return True every time, if rst1 and rst2 really are different objects (even if they are of the same object Type).
It appears that you are interested in whether the contents of the two Recordsets is the same. In that case, I would be inclined to serialize the recordset data and store the resulting String instead of storing the Recordset object itself.
The following VBA Function may prove helpful in that case. It loops through a recordset object and produces a JSON-like string containing the current recordset data.
(Note that the function may NOT necessarily produce valid JSON. It doesn't escape non-printing characters like vbCr and vbLf. It doesn't escape backslashes (\). It stores all values as either "string" or null. In other words, in its current form it is not designed to produce a string that could later be deserialized.)
Private Function rstSerialize(ByVal rst As DAO.Recordset)
' loop through the recordset and generate a JSON-like string
' NB: This code will NOT necessarily produce valid JSON!
'
Dim s As String, fld As DAO.Field, rowCount As Long, fldCount As Long
s = "{"
If Not (rst.BOF And rst.EOF) Then
rst.MoveFirst
rowCount = 0
Do Until rst.EOF
If rowCount > 0 Then
s = s & ", "
End If
s = s & """row"": {"
fldCount = 0
For Each fld In rst.Fields
If fldCount > 0 Then
s = s & ", "
End If
s = s & """" & fld.Name & """: " & IIf(IsNull(fld.Value), "null", """" & fld.Value & """")
fldCount = fldCount + 1
Next
s = s & "}"
rowCount = rowCount + 1
rst.MoveNext
Loop
End If
s = s & "}"
rstSerialize = s
End Function
Data Example: If the Recordset contained
DonorID Amount
------- ------
1 10
2 20
the function would return the string
{"row": {"DonorID": "1", "Amount": "10"}, "row": {"DonorID": "2", "Amount": "20"}}
Usage Example: On a form that contains a subform, a button on the main form could do the following
Private Sub Command3_Click()
Dim rst As DAO.Recordset, originalState As String
Set rst = Me.MemberDonationsSubform.Form.RecordsetClone
originalState = rstSerialize(rst)
rst.MoveFirst
rst.Edit
rst!Amount = rst!Amount + 1
rst.Update
Debug.Print "(Recordset updated.)"
If rstSerialize(rst) = originalState Then
Debug.Print "Recordset does not appear to have changed."
Else
Debug.Print "Recordset appears to have changed."
End If
End Sub
which would print the following in the VBA Immediate Window
(Recordset updated.)
Recordset appears to have changed.