From main form, run VBA to validate total in subform - ms-access

I have a form in MS Access 2002 with the following snippet of code behind a command button.
'Requery subform to make sure total is calculated
Me.fsubUpdateShipments.Requery
DoEvents
'Confirm quantities have been entered
If Form_fsubUpdateShipments.txtTotalShipmentQty.Value <= 0 Then
MsgBox "Cannot create shipment, no quantities have been entered", vbCritical
Exit Sub
End If
For months this has been working fine but today a colleague came to me and explained that the error message was showing even when they had entered quantities.
After doing a bit of digging I determined that the .Value <= 0 expression was being evaluated before the textbox in question had finished calculating its value: =Sum([QtyToShip]). This seems to only occur when the subform has a non-trivial number of records (around 10 or more) and is obviously a serious problem.
Adding a breakpoint on the line containing the If statement allows the code to run correctly but I obviously cannot use this method permanently.
Is there some way I can force the code to pause until the subform controls have finished recalculating their values?

I would skip using the user interface altogether and look at the tables. If this is run on a main form command button, the subform has lost focus and saved its values. A requery is unnecessary. Just re-create the Parent-Child relationship in the criterion (third parameter) of a DSum:
If DSum("QtyToShip", "ShipmentDetails", "ShipmentID = " & Me!ShipmentID) <= 0 Then
MsgBox "Cannot create shipment, no quantities have been entered", vbCritical
Exit Sub
End If

There are two options, both already identified by others, but as their suggestions are either incomplete or untidy (slow), I'm making this post.
A. You could put in an enforced wait to delay the IF test:
Put this in a module:
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
...
Public Sub GoSleep(millisecondDelay as long)
Sleep (millisecondDelay)
End Sub
And in your form:
'Requery subform to make sure total is calculated
Me.fsubUpdateShipments.Requery
DoEvents
GoSleep 1000 '1 second delay. Or 2000, whatever is required really
'Confirm quantities have been entered
If Form_fsubUpdateShipments.txtTotalShipmentQty.Value <= 0 Then
MsgBox "Cannot create shipment, no quantities have been entered", vbCritical
Exit Sub
End If
B. As another answer says, you can recalculate it yourself, and test on the basis of this result.
However rather than using DSum as pteranodon suggests, I would suggest something faster than DSum is appropriate as DSum (DCount etc) is very slow.
I typically use ADO for data operations, though my code can be adapted for DAO readily enough. To use this code you'll need a reference to Microsoft activeX 2.8 if you don't have one already:
In a module:
Public Function GetDBValue(qry as string) as variant
dim rst as new adodb.recordset
rst.open qry, currentproject.connection, adOpenKeyset, adLockReadOnly
if rst.eof then
GetValue = null
else
GetValue = rst.fields(0)
end if
end function
public Function IsNullSQL(basevalue as variant, replacementvalue as variant) as variant
isNullSQL = iif(isnull(basevalue), replacementvalue, basevalue)
end function
in your form:
''Requery subform to make sure total is calculated
'Me.fsubUpdateShipments.Requery
'DoEvents
'Confirm quantities have been entered
If IsNullSQL(GetValue("SELECT Sum(QtyToShip) FROM tbl WHERE ..."), -1) < 0 Then
MsgBox "Cannot create shipment, no quantities have been entered.", vbCritical, "No Quantities to Ship"
Exit Sub
End If
'If Form_fsubUpdateShipments.txtTotalShipmentQty.Value <= 0 Then
' MsgBox "Cannot create shipment, no quantities have been entered", vbCritical
' Exit Sub
'End If

Related

DoCmd.DeleteObject acTable - The record ... specified on this form or report does not exist

Let me start by saying I am not at all familiar with Access or VBA. However, I am the IT guy and at some point someone created an MS Access database that does a thing and now I have to support it.
We have a database that upon opening deletes any old data and re-runs the external query that makes this application work. Occasionally whatever state the program exited out in that table already does not exist. This causes MS Access to hang on the delete line and I have to run the debugger, comment out the DoCmd.DeleteObject line, re run, and then un-comment the line to let the user continue with their day.
I want to add in some sort of conditional statement, but anything I've been able to google in terms of If...Then statements or 'TableExist' type functions always causes an error to be thrown that I haven't defined the function. My best guess is I'm nesting this incorrectly or I'm not calling some sort of external function correctly, but as I said my VBA knowledge is extremely limited.
This code executes on startup:
Public Function Startup() As Integer
DoCmd.Close acForm, "soLookup"
DoCmd.DeleteObject acTable, "sales_order"
DoCmd.RunSavedImportExport "Import-sales_order"
DoCmd.OpenForm "soLookup"
End Function
Its the
DoCmd.DeleteObject acTable, "sales_order"
Line that causes things to fail.
I've attempted to restructure this several times based on several examples I had found, but I'll only bother with one below
Public Function Startup() As Integer
DoCmd.Close acForm, "soLookup"
If TableExists("sales_orders") Then
DoCmd.DeleteObject acTable, "sales_orders"
Else
'Do nothing
End If
DoCmd.RunSavedImportExport "Import-sales_order"
DoCmd.OpenForm "soLookup"
End Function
Nothing I seem to try seems to give me any result other than an error of some sort. All I want to do is add a conditional statement to this 'Startup' bit that checks if the "sales_order" table even if exists, and if it doesn't, then to just move on to the next comment and forget the DoCmd.DeleteObject. How can I achieve this functionality?! Thanks!
The TableExists function is not a standard function in Access. You need to define it yourself.
There are two main ways to define such a function, by trying and error trapping, or by iterating through all tables and checking names.
Way 1 (error trapping):
Public Function TableExists(TableName As String) As Boolean
On Error Resume Next
TableExists = CurrentDb.TableDefs(TableName).Name = TableName
End Function
Way 2 (iterating collection):
Public Function TableExists(TableName As String) As Boolean
Dim td As DAO.TableDef
For Each td In CurrentDb.TableDefs
If td.Name = TableName Then
TableExists = True
Exit Function
End If
Next
End Function
Define either of these functions in a public module, and your last approach should work
I will instead modify the query to import the source data into a new table (sales_orders), which will overwrite existing data when the query runs and so I don't have to delete the table and have additional check for TableExists

How to update a subform after performing a DELETE query?

I have a form, myForm, that includes a subform, mySubform. The records in mySubform have a many to one relationship with the record source of the myForm, as expected, and There is a combobox in mySubform, myCombo, whose values is linked to one of the columns of the record source of mySubform.
I have been having difficulty to delete a record in mySubform, by erasing the current value in myCombo. I have put the code below under the OnChange event of myCombo (after trying many different alternatives).
Please not that I have simplified the delete query and the actual one works fine in the back-end (SQL Server). However, after performing the delete operation, I get this error when the execution reaches Me.Requery:
Error 3162 You tried to assign a Null value to a variable that is not a Variant data type.
Also, after having the degugger skip the Me.Requery and going back to the form, the deleted record shows "#DELETED" in every combobox and textbox for that particular record, and mySubform is not any good beyond that point.
I have looked up very many resources and tried different statements instead of Me.Requery, but none has worked so far. Help is much appreciated.
Private Sub myCombo_Change()
If IsNull(Me.myCombo.Text) Or Me.myCombo.Text = vbNullString Then
Dim strSql As String
strSql = "DELETE FROM mySubformRecordSource WHERE PrimaryKeyColumn= " & theCurrentPrimaryKeyValueForTheValueIn_MyCombo
CurrentDb.Execute strSql
Me.Requery
End If
End Sub
I solved the problem by putting Me.Dirty = False after reading this discussion:
Editing Record issues in Access / SQL (Write Conflict)
Private Sub myCombo_Change()
If IsNull(Me.myCombo.Text) Or Me.myCombo.Text = vbNullString Then
Me.Dirty = False
Dim strSql As String
strSql = "DELETE FROM mySubformRecordSource WHERE PrimaryKeyColumn= " & theCurrentPrimaryKeyValueForTheValueIn_MyCombo
CurrentDb.Execute strSql
Me.Requery
End If
End Sub
The form and subform work flawlessly now, but I still dont quite understand the complete logic behind the solution. It has something to do with Me.Dirty=False saving the changes to the content of a non-nullable (or nullable, both work fine) column before programmatically delete the entire record. How exactly though, I dont know and will appreciate input.
Not entering on the choices of used cursors etc I think that the solution could be simply "requery" the parent form called by the code of the subform.
But i also have found similar situation in past projects that i made (sorry not beeing exaustive, i'm not using VBA anymore) and the requery command was done on subform but from the main form code, something like this:
If local_procedures.deleteContratto(anagrafica_id_par, servizio_id_par) = OK Then
Me.subfrm_contratti_frm_anagrafica.Requery
...
In this case you could delegate parent form to call delete procedure and update logic (.Requery) of the child form, by mean of an event raised from subForm listen on parent for example.
For the sake of completeness i started hinting about cursors because i think it's the underlying reason for these kind of sync failures between recordsets, the parent form one and the child form one.
There are many advices about using dbSeeChanges with db.Execute on SQL Server backend and the resulting default cursors.
UPDATE
Here's a code snippet where a custom event is raised from child to be caught from parent that is delegated to execute code regarding the child.
This pattern doesn't break the parent-child/one-to-many logic, assuming parent has more decision power than child.
' in mySubform
' assuming theCurrentPrimaryKeyValueForTheValueIn_MyCombo is a Long ID
Public Event DeleteRequest(record_ID As Long)
Private Sub myCombo_Change()
If IsNull(Me.myCombo.Text) Or Me.myCombo.Text = vbNullString Then
RaiseEvent DeleteRequest(theCurrentPrimaryKeyValueForTheValueIn_MyCombo)
End If
End Sub
' in myForm
Private WithEvents frmMySubform As Form__mySubform
' you could have more instances of mySubform concerning different issues or subrecords and manage each one correctly ...
Private WithEvents frmMySubform2nd As Form__mySubform
Private Sub frmMySubform_DeleteRequest(record_ID As Long)
Dim strSql As String
strSql = "DELETE FROM mySubformRecordSource WHERE PrimaryKeyColumn= " & record_ID
CurrentDb.Execute strSql
With Me.frmMySubform.Form
.Requery
' ...
end With
End Sub
Private Sub frmMySubform2nd_DeleteRequest(record_ID As Long)
Dim strSql As String
strSql = "DELETE FROM my2ndSubformRecordSource WHERE PrimaryKeyColumn= " & record_ID
CurrentDb.Execute strSql
With Me.frmMySubform2nd.Form
.Requery
' ...
end With
End Sub

VBA Executing CODE from a ComboBox

I have a very complex process that involves downloading a number of files from different shares, concatenating those files into working tables, manipulating and calculating related information, and then exporting specific fields (and calculations) as reports into a number of Excel workbooks.
I have this process coded so that I can click one button and the entire process will execute end to end. I have a series of text boxes that function as 'indicators' (red - this part failed, green - this part succeeded). As you can imagine, the code for the entire process is HUGE (32 pages when copied into MSWord) and difficult to weed through when I have a problem.
I got it into my mind that I wanted to put the code into a table so that it was much more modular and easier to deal with. I have setup a combo box with the action that I want to take and a second combo box with the report/file/object that I want to work with (ie Delete - Table 2, Acquire - File 1, Export - Report 4). I have been successful at creating the SQL statement to do simple things like del * from tbl_test and execute that from the combo boxes without any issue.
What I need to know is if there is a way to put what is essentially a code snippet into the table (memo field) and then have that vba code execute when I select the matching combos.
IE the code for 'Acquire - File1' is completely VBA code; it maps a network drive, locates the file, downloads the file, and moves it to a directory.
IE the code for 'Scrub - tblMain_Part1' is a combination of vba and sql code; it checks for the existence of a file (vba), if it finds it, it deletes a portion of the main table (sql) and appends the contents of the file it finds (sql), then it updates the monitor to indicate that it is completed (vba). If the file is not found, it changes the monitor box to red and updates a command button caption (vba)
I am NOT a genius with vba, but I hold my own. The thought process I had was that if I can essentially get the code broken into managable chunks in the table, I could call the code smippets in order if I want to run the entire process, or I could just re-execute portions of the code as needed by selecting the action and report/file/object combination.
Any thoughts/ideas are appreciated.
I think it would be best to split the code into Subs. The table you loop through would have a Sub-Name field and a blnSuccess field. Your code would loop though the table running each sub and then updating blnSuccess based on any errors you receive. This would give you queryable result set when you try to see what happened.
Consider using macros. You shouldn't need a table. Also, consider moving your hard-coded SQL to queries.
I think that you shouldn't use a table, just create a module with different subs for each operation. On your button event, after the combo selections, I would do a case statement.
dim strOperation as string
strOperation = me!selectionOne
Select Case strOperation
Case "delete": deleteTable(me!selectionTwo)
Case "export": export(me!selectionTwo)
case "acquire": acquire(me!selectionTwo)
End Select
Of course, you'd have your acquire, delete, and export methods written in a module and have whatever parameters you need for each operation there.
This is just one idea of many that you could use to approach this.
I was going to edit the original answer but this seems to be off on a different tack....
I think it would be best to split the code into functions that return a string if there is an error. The table you loop through would have a strFunction,strError and strObject fields. Your code would loop though the table running each function based on the case statement while passing the strObject as a string and then updating strError based on any errors you receive. You could query the table after this process to see which records have errors in them.
If the button is called cmdRunAll here is the code for it.
Private Sub cmdRunAll_Click()
On Error GoTo ErrHandler
Dim rst As DAO.Recordset
Set rst = CurrentDb.OpenRecordset("tblCode", dbOpenDynaset, dbSeeChanges)
If Not rst.EOF Then
With rst
.MoveFirst
Do While Not .EOF
.Edit
Select Case !strFunction
Case "fExport"
!strError = fExport(!strObject)
End Select
.Update
.MoveNext
Loop
End With
End If
rst.Close
Set rst = Nothing
MsgBox "Processes complete"
Exit Sub
ErrHandler:
Debug.Print Err.Description & " cmdRunAll_Click " & Me.Name
Resume Next
End Sub
Here is a simple sample function
Public Function fExport(strTable As String) As String
On Error GoTo ErrHandler
Dim strError As String
strError = ""
DoCmd.TransferText acExportDelim, , strTable, "C:\users\IusedMyUserNameHere\" & strTable & ".txt"
fExport = strError
Exit Function
ErrHandler:
strError = Err.Description
Resume Next
End Function

Issue with Ms access Reports

I am trying to preview a report of the current tracking number which is on the main form(main form name is:frmReviewReleaseLogWrapper) by using a command button.
Each tracking number has different part numbers, so each tracking number has many records in the DB table (Depending upon the number of partnumbers, that is each partnumber has a record)
I wrote the following code and it is not working, It is just displaying report name and column names and not the records.
Code is as follows:
Private Sub Command225_Click()
On Error GoTo Err_Command225_Click
Dim stDocName As String
DoCmd.OpenReport "rptEventLog", acPreview, , "[TrackingNumber] = Forms!frmReviewReleaseLogWrapper"
Exit_Command225_Click:
Exit Sub
Err_Command225_Click:
MsgBox Err.Description
Resume Exit_Command225_Click
End Sub
What's the name of the control on the frmReviewReleaseLogWrapper form that contains the tracking number? You need to reference it in your OpenReport statement, like so:
DoCmd.OpenReport "rptEventLog", acPreview, , _
"[TrackingNumber] = Forms!frmReviewReleaseLogWrapper!txtField"
Incidentally, you might do better to give your command button a name that reflects its actual function, so it'll be easier to find later.

In MS Access form, how to color background of selected record?

I have a somewhat complicated looking Access Form with a continuous display (meaning multiple records are shown at once). I'd like to change the background color of the selected record only so the end-user can easily tell which record they are on.
I'm thinking of perhaps a conditional format or maybe something like this:
Private Sub Detail_HasFocus()
Detail.BackColor(me.color)=vbBlue
End Sub
and something similar for when that row loses focus. This code snippet obviously won't work, but it's the kind of code I'd like to achieve.
Here is a complete solution that correctly treats newly-edited records, as well as handles Access UI quirks (i.e. failed re-paints, inconsistent behavior depending on how a record is selected--via mouse or keyboard or record selector, etc.). I include verbose comments, because Access requires thorough explanations due to its many inconsistencies and/or bugs. I have attempted leaner solutions, but inevitably it does not behave well without the tricks to force Access to repaint the form, or complicated ways for determining the current record within the Detail_Paint() event handler.
The code is for an Access form with a textbox bound to an ID Autonumber field. The form also has a Rectangle control named boxCurrent which is updated to highlight the currently-selected record (it has a wide brightly-colored border). I find a Rectangle control offers more visual options than setting Detail.BackColor, although such details are openly configurable using the overall pattern. Developed and tested with Access 2013 and 2016.
'* Set this value in From_Current event handler
Private vCurrentAutonumber As Variant
Private Sub Detail_Paint()
'* Delcare static variables to make often repeated calls more efficient.
Static iActive As Integer
Static vThisValue As Variant, vOldValue As Variant
On Error Resume Next
iActive = 0 '* Default to False/hidden value
vThisValue = Me.ID.Value
If Err.Number = 0 Then
If Not IsNull(vCurrentAutonumber) Then
If vThisValue = vCurrentAutonumber Then iActive = 1
ElseIf Me.NewRecord Then
'* Form currently set to "New Record", but may or may not be in edit mode.
'* When in EDIT MODE, AutonumberControl.Value will HAVE A VALUE
' AND AutonumberControl.OldValue will be null
' When NOT in edit mode, AutonumberControl.Value will be null
' AND AutonumberControl.OldValue will also be null
'*** That is the only way I have found to determine for sure
' if the currently-edited-new-record is the available record for
' this particular call of Detail_Paint().
' Other properties like CurrentRecord, NewRecord, etc. remain
' unchanged during repeated calls to Detail_Paint()
' and access has no other convenient way to determine the
' newly-added autonumber value, so it must be deduced using
' this trick.
If IsNull(vThisValue) Then
If Not Me.Dirty Then
'Record selector on *(New Record) row, but not edited yet.
If Err.Number = 0 Then iActive = 1
End If
Else
vOldValue = Me.ID.OldValue
If Err.Number = 0 Then
If IsNull(vOldValue) Then
'* Newly-edited record with fresh autonumber value is selected.
iActive = 1
'Else if vOldValue is not null, it is an existing record.
'* Not the current record since it can't be both existing and new.
End If
End If
End If
End If
End If
'* Set these values on EACH CALL, since their values will be retained
'* on subsequent calls.
With boxCurrent
.BackStyle = 0 'iActive
.BorderStyle = iActive
End With
End Sub
Private Sub Form_AfterDelConfirm(Status As Integer)
Me.Repaint
End Sub
Private Sub Form_AfterInsert()
If IsNull(vCurrentAutonumber) Then
'* If a new record is saved while staying on that record,
'* the Form_Current() handler is not called and so the
'* vCurrentAutonumber would not be updated with the newly
'* saved value. But now Me.NewRecord is false, so the
'* currently record would not be updated properly unless
'* vCurrentAutonumber is explicitly updated here.
On Error Resume Next
vCurrentAutonumber = Me.ID.Value
'* Force repaint (see comment in Form_Current)
boxCurrent.BackColor = vbBlue
End If
End Sub
'Private Sub Form_BeforeInsert(Cancel As Integer)
'* Attempted to set some variable or property in this event handler
'* --something to indicate to Detail_Paint() which record is the
'* new record being edited. But no matter what I set here, the
'* change is present and identical for each call of Detail_Paint(),
'* so for the most part this technique was not useful.
'* The only alternative is to set one of the data fields, because
'* those DO change for each each to Detail_Paint().
'* IF THE PRIMARY KEY IS NOT AN AUTONUMBER FIELD (OR IF ANOTHER
'* DATA FIELD IS AVAILABLE TO MANIPULATE), ONE COULD FLAG A NEWLY
'* EDITED RECORD BY SETTING SUCH A FIELD HERE AND INSPECTING
'* it in Detail_Paint(). Personally, I avoid dummy fields just for
'* making Access work well and my primary key is Autonumber so it cannot
'* bet set to a known new value.
'End Sub
Private Sub Form_Current()
On Error Resume Next
vCurrentAutonumber = Me.ID.Value
If Err.Number <> 0 Then vCurrentAutonumber = Null
On Error GoTo 0
'*** FORCE REPAINT of record detail section
'* If not forced, records are not necessarily repainted for every type of
'* UI event. For instance, changing records using the record selectors
'* has different behavior than clicking inside a record, but either way
'* the current record has changed and so should be repainted.
'* But calling Me.Repaint is not sufficient to actually repaint the form.
'* Even if the Detail_Paint event is called, the actual visible elements
'* are not always repainted (bug?). It seems that only changing some
'* visible feature/control of the form will force an actual repaint.
boxCurrent.BackColor = vbBlue
End Sub
Private Sub Form_Load()
vCurrentAutonumber = Null
End Sub
OP here. Cade pointed out that the original solution's link points to a '97 db which may not be openable anymore. Also, unfortunately, my original code is long since gone.
However, I recently did something like this using the conditional formatting method which requires no VBA. This is in Access 2016 on a Continuous Form:
In your dataset, add a yes/no field. Let's call it Rcd_Selected.
Make a text box. Also set the Control Source to Rcd_Selected.
Change the Fore Color to #FFFFFF (This will be the non-selected color)
Change Format to 'True/False'
Set Enabled=Yes,Locked=No
In the Ribbon, go to Format->Conditional Formatting and make a new rule: Where Field Value =True, set the fore color and back color to your Selected color and click on Enabled.
Clean up by stretching the text box over the entire Detail section and moving it to the back.
Make a check box field. Sent the Control Source to Rcd_Selected.
Stretch the checkbox over the entire Detail section and move it to the front.
Whenever you click on the area, the checkbox will turn on/off triggering the conditional formatting of the textbox in the background to change color.
One limitation of this is that it makes the entire record read-only. I've never had speed issues and it works when multiple records are selected and un-selected.
One more way for continuous forms...
In the Form's Form_Current event set a TempVar to equal the value of the current record ID e.g. `
TempVars!CurrentRecordID = Me.ID_xxx.value
Me.ControlName.Refresh 'This must one of the conditionally formatted controls per Step 2 below
NB. the second line of code above is necessary for the conditional formatting to be triggered. You only need to refresh one of the conditionally formatted controls.
Conditional Formatting rule expression is: [ID_xxx]=[TempVars]![CurrentRecordID] and set the desired formatting e.g. BackColor
Apply step 2 to any control that you want conditionally formatted when the record is selected.
Only the controls on the current record are highlighted
Private Sub Form_Current()
10 On Error GoTo Form_Current_Error
'=============================================================
20 TempVars!CurrentRecordID = Me.MatterID.Value
30 Me.fldDateEngagedEnquiry.Requery
'================================================================
MyExit:
40 On Error GoTo 0
50 Application.Screen.MousePointer = 0 'reset to default mouse pointer
60 Exit Sub
Form_Current_Error:
70 MsgBox "Code Line Number: " & Erl & vbCrLf & vbCrLf & "Error " & Err.Number & vbCrLf & vbCrLf & " (" & Err.Description & ") in procedure " & vbCrLf & vbCrLf & " Form_Current of Sub Form_frmMyMatters_SVR"
80 GoTo MyExit
End Sub