VB Loop for MS Access that adds numbers - sql

I'm new to coding with Access. I'm wanting to create a Do While Until loop that compares the amounts of two tables. One table has a record with customer numbers with the total amount for each customer number and then another table that has all detail line items with amounts for the items for each customer.
Example:
Summary
CustID | Total |
1234 | $20.00 |
2345 | $40.00 |
Detail
CustID | DocNo | Amount | Included |
1234 | 0000 | $15.00 | |
1234 | 1111 | $5.00 | |
1234 | 2222 | $3.00 | |
I wanted to be able to execute the loop and then update the Included column to "Yes" to all applicable line items whose accumulated sum added to the total in the Summary table.
So the example above would update the first and second rows of the Detail table to "Yes" and then move on to the next CustID, because the first two row Amounts totaled to $20.00 - the amount in the Summary table. The last record in the detail table will not be updated to anything, because it does not apply. The good thing about the order of these records is that the records that will be labeled "Yes" will be the first cumulative set of records; not every other record, or random records. There are no fixed number of records for each total, it varies - therefore, I need the loop.
Also, does anyone have a recommendation for how to best learn VB/VBA for Access and Excel? I only understand SQL and I'm trying to enhance my coding skills, so I'm not having to manually subtotal these amounts and then delete the 30,000 non applicable records in an Excel spreadsheet.
Thanks in advance!

I've worked out a first approach, using DAO, your tables are supposed to be Summary and Detail:
Sub sof20295931SummaryDetail()
Dim lCustID0 As Long
Dim dblDetailTotal As Double, dblSummaryTotal As Double
Dim strSQL As String
Dim rst0 As DAO.Recordset, rst As DAO.Recordset
'
' get summary recordset:
'
strSQL = "SELECT CustID, Total" _
& " FROM Summary" _
& " ORDER BY CustID;"
Set rst0 = CurrentDb.OpenRecordset(strSQL)
'
' get detail recordset:
'
strSQL = "SELECT CustID, DocNo, Amount, Included" _
& " FROM Detail" _
& " ORDER BY CustID,DocNo;"
Set rst = CurrentDb.OpenRecordset(strSQL)
lCustID0 = rst0!CustID
dblSummaryTotal = rst0!Total
dblDetailTotal = 0
'
' check the recordset of the Detail:
'
Do While (Not rst.EOF)
'
' change CustID:
'
If (rst!CustID > lCustID0) Then
rst0.MoveNext
lCustID0 = rst0!CustID
dblSummaryTotal = rst0!Total
dblDetailTotal = 0
End If
'
' sum up:
'
dblDetailTotal = dblDetailTotal + rst!amount
'
' modify record:
'
rst.Edit
'
If (dblDetailTotal <= dblSummaryTotal) Then
rst!Included = "Yes"
Else
rst!Included = Null
End If
'
rst.Update
'
' go to the next record:
'
rst.MoveNext
Loop
'
' destruct objects:
'
rst0.Close
Set rst0 = Nothing
'
rst.Close
Set rst = Nothing
End Sub
My Windows Locale used Euro Money format, here is the Summary table:
And Detail table in which I added some extra records:
Detail after modification:

Since no reply on whether a more SQL-centered approach is okay, here it is.
The field Included does not logically belong in Detail, but in Summary. If you want to keep it in Detail, using SQL is a challenge: you can't use the aggregate function Sum() and also update values. But if you move Included to Summary, SQL is the natural solution. I would recommend this approach.
qrySummary (to be nested in second query below)
SELECT CustID, Sum([Total]) AS CustomerTotal
FROM tblDetail GROUP BY CustID;
qryValidate
UPDATE tblSummary LEFT JOIN qrySummary ON tblSummary.CustID = qrySummary.CustID
SET tblSummary.Included =
IIf([qrySummary].[CustomerTotal]=[tblSummary].[Amount],True,False);
To follow through on the spreadsheet, you will need to view Included at the detail level; this will require a query that joins Detail and Summary.

Related

Lookup value in Access table without a unique identifier using VBA

I have a table like this:
+-----------+------+
| TableName | Flag |
+-----------+------+
| TableG | 0 |
| TableE | 0 |
| TableR | 1 |
| TableL | 1 |
| TableN | 0 |
What I'm trying to do is have a function that looks up every TableName where Flag=1 so that I can assign each table where Flag=1 to a button in a form (e.g. assign the first instance to Button1, etc.). The tricky part is that this table cannot be edited, so no unique ID/count can be assigned to easily grab items.
Here's what I've tried so far:
DLookup does not handle having multiple lookup matches (e.g. DLookup("TableName", "MyTable", "Flag=1") would not allow me to access all the values where Flag=1), so I don't think I can use that to store them in an array
I've tried using a RecordSet (see below), but I could not figure out how to have the SQL string automatically add a count so that each table name could be referenced (e.g. TableNameRS(1) to pull the name of the first table with Flag=1)
Public Function TableNameRS(TableID As Integer)
Dim RS As DAO.Recordset
Dim SQL As String
Dim Name As Variant
SQL = "select TableName, COUNT(TableName) from MyTable where Flag=1"
Set RS = CurrentDb.OpenRecordset(SQL)
Name = RS("TableName")
TableNameRS = Name
RS.Close
End Function
I am out of ideas - what are my options here?
If the number of records and buttons are fixed, say 10, and buttons have names like btn1, btn2, etc., consider a procedure like:
Public Sub TableNameRS(TableID As Integer)
Dim RS As DAO.Recordset
Set RS = CurrentDb.OpenRecordset("SELECT TableName FROM MyTable WHERE Flag=1 ORDER BY TableName")
For x = 1 to 10
Me("btn" & x).Caption = RS("TableName")
RS.MoveNext
Next
RS.Close
End Function
Procedure can be called in form Open event. Then code in button Click event can pass its caption as an argument to whatever other procedure needs to take action with TableName value.
Calculating a sequence identifier in Access query is a common topic MS Access Restart Number Sequence.
Assuming TableName values are unique:
SELECT TableName, DCount("*", "table", "TableName<'" & [TableName] & "' AND Flag=1")+1 AS Seq,
FROM table
WHERE Flag=1
ORDER BY TableName;
Can use that query as RecordSource for form or report or execute DLookup() on it.
Be aware any change in filter will impact sequence calculation.

Access Query/VBA to Group from two fields and Concatenate values

I have a challenging problem I'm attempting to solve and could use your expertise in this matter.
I am attempting to replicate some reports in Access 2013 using queries that I otherwise get from the front-end application to a FootPrints Service Core 11.6 Database. I've completed queries and calculations to replicate most fields from the front end reports, except for the assignee information.
(Note: Assignee is the individual or [generally] teams that a ticket is assigned to for work, can be multiple [teams and individuals])
These assignees are listed out separately within an assignees table of FootPrints Database (See attached images). When the front end application generates reports it somehow groups together the individual and team assignee information in a particular way I'm unable to emulate (See Image). This is where I need your help!
I need to combine all the assignees (individual and team assignees) within a single field, grouped by the ticket number (mrID) they associate with.
So, where there is the following in the database
MrID | Assignee | Team
12345 | Bob Smith | Help Desk Tier 1
12345 | Jane Smith | Help Desk Tier 1
12345 | (Null) | Telecom
23456 | (Null) | Help Desk Tier 2
34567 | Chuck Norris | (Null)
45678 | (Null) | Help Desk Tier 1
45678 | (Null) | Help Desk Tier 2
45678 | (Null) | Networking
45678 | (Null) | Access Control
It should appear as 1 field, like this:
MrID | Assignees
12345 | Help Desk Tier 1: Bob Smith, Jane Smith. Telecom:
23456 | Help Desk Tier 2:
34567 | Chuck Norris
45678 | Help Desk Tier 1: . Help Desk Tier 2: . Networking: . Access Control:
As you can see in the above example, each team assignee is followed by a :, multiple team members (individuals) are seperated by ,'s, and multiple teams are separated by .'s
Following this convention Is there a way to mimic this process through the use of a query (or VBA if necessary) in Access?
Sincerely,
Kris
You have not provided enough data to make more tests. Yes, you included screenshots, but data to copy and paste just 3 records, so I worked with that
I replied your table like this (name of my table is Table1):
Then, I have a Query like this:
The SQL code for this query is:
SELECT DISTINCT Table1.MrID, FINAL_ASSIGNEES([mrid]) AS ASSIGNEES FROM Table1;
As you can see, this SQL code invokes an UDF coded in VBA named FINAL_ASSIGNEES. The code of this UDF is:
Public Function FINAL_ASSIGNEES(ByVal vThisMrID As Long) As String
Dim RST As Recordset
Dim SqlStr As String
SqlStr = "SELECT DISTINCT Table1.MrID, CONCATENATE_ASSIGNEE([MrID],[Team]) AS ASSIGNEES FROM Table1 " & _
"WHERE Table1.MrID=" & vThisMrID & ";"
Set RST = Application.CurrentDb.OpenRecordset(SqlStr, 2, 4)
With RST
If .EOF <> True And .BOF <> True Then
.MoveLast
.MoveFirst
Do Until .EOF = True
FINAL_ASSIGNEES = FINAL_ASSIGNEES & .Fields(1).Value & ". "
.MoveNext
Loop
FINAL_ASSIGNEES = Left(FINAL_ASSIGNEES, Len(FINAL_ASSIGNEES) - 2) 'minus 2 to get rid of extra ". "
End If
Set RST = Nothing
End With
End Function
And yes, this VBA code calls a second UDF named CONCATENATE_ASSIGNEE. The code of this second UDF is:
Public Function CONCATENATE_ASSIGNEE(ByVal vMrID As Long, ByVal vTeam As String) As String
Dim MyRST As Recordset
Dim MySQL As String
MySQL = "SELECT Table1.Assignee FROM Table1 " & _
"WHERE (((Table1.MrID)=" & vMrID & ") AND ((Table1.Team)='" & vTeam & "'));"
Set MyRST = Application.CurrentDb.OpenRecordset(MySQL, 2, 4)
DoEvents
With MyRST
If .EOF <> True And .BOF <> True Then
.MoveLast
.MoveFirst
Do Until .EOF = True
If IsNull(.Fields(0)) = True Then
CONCATENATE_ASSIGNEE = CONCATENATE_ASSIGNEE & "Unassigned" & ", "
Else
CONCATENATE_ASSIGNEE = CONCATENATE_ASSIGNEE & .Fields(0).Value & ", "
End If
.MoveNext
DoEvents
Loop
CONCATENATE_ASSIGNEE = vTeam & ": " & Left(CONCATENATE_ASSIGNEE, Len(CONCATENATE_ASSIGNEE) - 2) 'minus 2 to get rid of the extra ", "
End If
Set MyRST = Nothing
End With
End Function
But this gets kind of what you are after. If you are working with big recordsets, probably it will take some time to make calculations. But at least you can adapt this to your needs.

Create Access report that lists information from one table, which is matched on ID from another table

I'm updating this as the previous answer turned out to not be what I really needed.
I have two tables in my Access database. One is called Billings, one is called Bookings. Each table has a column called Booking Number, which is how they are related. The Billings table also has a field called Container. What I want to do is:
Create a one page report for each Booking Number (which can be expanded to multiple pages depending on the number of containers)
In the body of the report I would like to list each Container (along with other information about each container) on separate rows
Here are my tables:
Bookings
----------------
ID | Booking Number
1 | '1234'
2 | '1235'
Billings
----------------
ID | Booking Number | Container
1 | '1234' | '12'
2 | '1234' | '16'
3 | '1235' | '18'
Based on these two tables, there should be a report for Booking #1234, and Booking #1235. Booking #1234's report should list Container's 12 and 16, while Booking #1235's report would only list 18. I'm a PHP/MySQL developer, so I understand SQL queries. However, the way I would write the query for MySQL obviously does not work for Access. At the moment I am using a Module similar to the answer below, but it does not do what I need it to do. This is my current query:
SELECT
b.*,
ListContainers(b.[Booking Number]) AS Containers
FROM
Bookings AS b
WHERE
((b.[Booking Number]) Is Not Null);
This will create a comma separated list of the containers associated with each Booking Number, but I want to create a separate row for each Container, which will also include other information from the Billings table.
Has anyone had any experience with a similar situation, or know any steps I could take to accomplish what I'm looking to do?
For your new requirements you'll want to use a "subreport" to list the containers (and related information). For details, see my recent answer to a similar question here.
[This is my answer to the original question, before the "spec" changed.]
Create a new Module in Access and paste in the following code
Public Function ListContainers(Booking_Number As String) As String
Dim cdb As DAO.Database, rst As DAO.Recordset, rtn As String
Const Separator = ", "
Set cdb = CurrentDb
rtn = ""
Set rst = cdb.OpenRecordset("SELECT Container FROM Billings WHERE [Booking Number]=""" & Booking_Number & """ ORDER BY Container", dbOpenSnapshot)
Do While Not rst.EOF
rtn = rtn & rst!Container & Separator
rst.MoveNext
Loop
rst.Close
Set rst = Nothing
Set cdb = Nothing
If Len(rtn) > 0 Then
rtn = Left(rtn, Len(rtn) - Len(Separator)) '' trim trailing separator
End If
ListContainers = rtn
End Function
You can then use that Function in a query, like this
SELECT [Booking Number], ListContainers([Booking Number]) AS Containers
FROM Billings
That will return
Booking Number Containers
-------------- ----------
1234 12, 16
1235 18

Access Query Match lowest unused number

I have got a query result in MSAccess.
QueryMatch :
InvoiceNumber RegionNumber Group
9448180 73657 A
9448180 74170 A
9448180 74171 A
9448180 78761 A
9448196 73657 A
9448196 74170 A
9448196 74171 A
9448196 78761 A
9448201 73657 A
9448201 74170 A
9448201 74171 A
9448201 78761 A
1234567 12345 B
so on..
Table 2:
RegionNumber InvoiceNumber
73657
74170
74171
78761
The query has a long list , separated by groups.
There can be x + n RegionNumber for x InvoiceNumbers.
n = 0 to 25.
One RegionNumber must be matched with One InvoiceNumber only for each group.
How do we update Table2?
Let us do for smallest RegionNumber to match smallest InvoiceNumber within the Matchresult.
Leaving the last RegionNumber NULL.
Please provide a VBA or can this be done with queries alone ?
Selecting MIN (InvoiceNumber) for each RegionNumber will result in the same InvoiceNumber.
Thanks
Let's consider the following [QueryMatch] sample data
InvoiceNumber RegionNumber Group
123 678 A
234 678 A
345 678 A
123 789 A
We could try to just iterate through the RegionNumber values (ascending) and pick the lowest InvoiceNumber, but that approach will ultimately fail. We would assign InvoiceNumber 123 to RegionNumber 678 and then when it comes time to process RegionNumber 789 the only possible choice would be InvoiceNumber 123 and it has already been taken.
So, we'd better start by getting a list of the RegionNumber values and the number of distinct InvoiceNumbers that each one has. That will let us process the most constrained RegionNumber values first.
SELECT qm.RegionNumber, Count(qm.InvoiceNumber) AS NumDistinctInvoiceNumbers
FROM
(
SELECT DISTINCT RegionNumber, InvoiceNumber FROM QueryMatch
) qm
GROUP BY qm.RegionNumber
ORDER BY 2 ASC
...which returns...
RegionNumber NumDistinctInvoiceNumbers
789 1
678 3
...lets us know that we need to process RegionNumber 789 first, then assign one of the "leftovers" to RegionNumber 678.
Now, to find the lowest unused InvoiceNumber for a given RegionNumber we need to exclude any ones that we have already written to [Table 2]. So, assuming that we have already "given" InvoiceNumber 123 to RegionNumber 789, one way to find a suitable candidate for RegionNumber 678 would be...
DMin("InvoiceNumber", "QueryMatch", "RegionNumber=678 AND InvoiceNumber NOT IN (Select InvoiceNumber FROM [Table 2])")
...which will return the smallest unused InvoiceNumber, or Null if not match is found.
Wrap that up in some VBA code and we get
Public Sub AssignInvoicesToRegions()
Dim cdb As DAO.Database, rstRegion As DAO.Recordset, rst2 As DAO.Recordset
Dim vInvNo As Variant
Set cdb = CurrentDb
Set rst2 = cdb.OpenRecordset("Table 2", dbOpenDynaset)
Set rstRegion = cdb.OpenRecordset( _
"SELECT qm.RegionNumber, Count(qm.InvoiceNumber) AS NumDistinctInvoiceNumbers " & _
"FROM " & _
"( " & _
"SELECT DISTINCT RegionNumber, InvoiceNumber FROM QueryMatch " & _
") qm " & _
"GROUP BY qm.RegionNumber " & _
"ORDER BY 2 ASC", _
dbOpenSnapshot)
Do While Not rstRegion.EOF
Debug.Print rstRegion!RegionNumber
vInvNo = DMin("InvoiceNumber", "QueryMatch", "RegionNumber=" & rstRegion!RegionNumber & " " & _
"AND InvoiceNumber NOT IN (Select Nz(InvoiceNumber, 0) AS InvNo FROM [Table 2])")
If IsNull(vInvNo) Then
MsgBox "No available InvoiceNumber for RegionNumber=" & rstRegion!RegionNumber, _
vbCritical, "Lookup Failed"
Else
rst2.FindFirst "RegionNumber=" & rstRegion!RegionNumber
rst2.Edit
rst2!InvoiceNumber = vInvNo
rst2.Update
End If
rstRegion.MoveNext
Loop
Debug.Print "Done."
rstRegion.Close
Set rstRegion = Nothing
rst2.Close
Set rst2 = Nothing
Set cdb = Nothing
End Sub
Note that in its current form this algorithm is not guaranteed to find a match for every RegionNumber. Depending on the order in which the RegionNumber values are processed some regions may find that all of their candidates have been taken (hence the IsNull() check in the code). In that case you may have to tweak the algorithm to give those regions "first shot" at an InvoiceNumber, possibly by manually assigning a higher priority to those "difficult" regions.

How do I limit multi-select options for one field based on data entered into another field when they both reference the IDs of another table?

I have two tables, we'll call them T1 and T2. T1 currently has nearly 600 records in it, one of which contains the ID number and another of which contains a title, so ID and TITLE:
T1
ID | TITLE
-----|----------
1 | Title ABC
... | ...
201 | Title XYZ
... | ...
411 | Title 123
T2 has an ID field, a Titles field, an Accepted Titles field, and a Rejected Titles field, so ID, TITLES, ACCEPTED TITLES, and REJECTED TITLES. The access form uses a multiple select ListBox to select one or more TITLES from T1, however many are required, but usually no more than ten. Once entries are made into the TITLES field of T2, which is Numeric for the record IDs corresponding to the titles selected from T1, I want a combo box for each of the ACCEPTED TITLES and REJECTED TITLES in T2 to be limited to showing only those titles that correspond to IDs entered into the TITLES field. So, if I have in the TITLES field of T2,
T2
ID | TITLES | ACCEPTED TITLES | REJECTED TITLES
---|---------------|------------------|----------------
1 | 1, 201, 411 | |
I want the dropdown for the ComboBox to show only the titles corresponding to those IDs entered into the TITLES field. So, taking the ACCEPTED TITLES field, it might look like this:
T2
ID | TITLES | ACCEPTED TITLES | REJECTED TITLES
---|---------------|--------------------|---------------
1 | 1, 201, 411 | | [ ] Title ABC \/|
| [ ] Title XYZ |
| [ ] Title 123 |
I'm thinking I should be able to build a SELECT WHERE IN (...) statement that I can use in the "Row Source" properties of ACCEPTED TITLES and REJECTED TITLES. Then the list would be as short as the items selected for TITLES rather than 600+ records long. This also completely eliminates the potential for erroneous input under ACCEPTED TITLES or REJECTED TITLES since those titles can only be selected from those entered under TITLES. But, I don't yet know how to build such a SELECT statement.
Any assistance will be appreciated. Thanks for your time.
I propose a slightly different design for T2 (TitleStatus).
Create Table [TitleStatus] ([TitleID] Number, [StatusID] Number);
Create Index TitleIDindex ON [TitleStatus] (TitleID) WITH PRIMARY;
Then another table to house the statuses. Something like
Create Table [Statuses] ([StatusID] Number, [StatusText] String);
Create Index StatusIDindex ON [Statuses] (StatusID) WITH PRIMARY;
(this table could be organized with one column, but either way you want to prevent statuses like 'Accepted' and 'Accepted Title' from creeping in. Then you'd have two records which are the same thing but you don't naturally know to look for both)
Then T2 (TitleStatus) will look like
TitleStatus
TITLEID | STATUSID
----------|----------
1 | 1
... | ...
201 | 1
... | ...
411 | 1
500 | 2
Where Statuses Looks like
StatusID | StatusText
1 | Accepted
2 | Rejected
You're inserts should be fairly straight forward form there.
You can get all the accepted titles like this
Select T1.Title, Status.StatusText
From T1
Inner join TitleStatus TS on TS.TitleID = T1.ID
Inner Join Statuses S on S.StatusID = TS.StatusID
Where S.StatusText = 'Accepted'
I found the solution. It's actually much simpler than I realized:
Public Function GetTitleIDs() As String
Dim IDData As ADODB.Recordset
Dim SubLookup As Variant
Dim SelectSubmissions As String
' Should pull the same records and in the same order as '
' those found in originating Listbox '
Set IDData = CurrentProject.Connection.Execute("SELECT [Poems].[ID], [Poems].[Title] FROM Poems ORDER BY [Title];")
SubLookup = IDData.GetRows
' Submissions is the name of my originating Listbox. The rest remains unaltered. '
Dim listrow As Integer
For listrow = 0 To Me.Submissions.ListCount - 1
If Me.Submissions.Selected(listrow) = True Then
SelectSubmissions = ", " & SubLookup(0, listrow) & SelectSubmissions
End If
Next
If Len(SelectSubmissions) > 0 Then
SelectSubmissions = Right(SelectSubmissions, Len(SelectSubmissions) - 1)
End If
GetTitleIDs = SelectSubmissions
End Function
Private Sub Form_Current()
' Needed to update existing Listbox entry or entries when record is loaded '
' in form. If record is new, Listbox(es) will simply contain no records. This '
' can source all records for the fields you want in order to show existing '
' Listbox entries without fail. '
Me.Declined.RowSource = "SELECT [Poems].[ID], [Poems].[Title], [Poems].[Year Completed], [Poems].[Blog Location] FROM Poems ORDER BY [Title];"
Me.Accepted.RowSource = "SELECT [Poems].[ID], [Poems].[Title], [Poems].[Year Completed], [Poems].[Blog Location] FROM Poems ORDER BY [Title];"
End Sub
Private Sub Declined_GotFocus()
' Presumably your subset selection list gets focus after '
' checking list entries in originating Listbox. '
Me.Declined.RowSource = "SELECT [Poems].[ID], [Poems].[Title], [Poems].[Year Completed], [Poems].[Blog Location] FROM Poems WHERE [ID] IN (" & GetTitleIDs & ") ORDER BY [Title];"
End Sub
Private Sub Accepted_GotFocus()
Me.Accepted.RowSource = "SELECT [Poems].[ID], [Poems].[Title], [Poems].[Year Completed], [Poems].[Blog Location] FROM Poems WHERE [ID] IN (" & GetTitleIDs & ") ORDER BY [Title];"
End Sub
This will show only options in Declined and Accepted that are checked under Submissions, limiting selection options to only those values of interest for these fields. It's perfect.
So long as the table and ordering is the same for both the records assigned to IDData and the ListBox you're checking against, the IDs will always line up correctly.