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

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.

Related

How to filter an Access datasheet form based on fields of datasheet and subdatasheet

I have an Access DB, along with tables and forms.
In one table I have customers and in a second table as a subdatasheet on the first table I have tags.
I have created a form which shows the customer list along with the subdatasheet which displays all the tags that a customer has.
I would like to filter based on a field of the main datasheet, and on the subdatasheet for a tag.
E.g. where customer_name = "Peter" and tag ="Neighbor"
I saw this code but when trying to use it fails.
Filter on Subdatasheet
When trying Set mainDS = Me.Controls(dataSheetName).Form where do I find the datasheetName?
I tried entering Form Name, Table Name, I searched in form properties but didn't manage to find a solution.
Based on your question it sounds like you are new to Access and you should review table normalization.
Everything starts with a normalized table structure. Unfortunately you are starting with the relatively difficult Many to Many Relationship. Each Customer can have many tags which is a One to Many Relationship, but there wouldn't be much point to comparing customers unless they could have the same tags, which means each tag can have many customers as well. For example, create your tables with the corresponding primary and foreign keys and tell access about the relationhip by hitting the ribbon and selecting the relationships tool:
The raw data in the CustomersTags table is what you are interested in, but it is not userfriendly
----------------------------------------------------------------
| CustomerTagID | CustomerID | TagID |
----------------------------------------------------------------
| 1 | 1 | 1 |
----------------------------------------------------------------
| 2 | 3 | 1 |
----------------------------------------------------------------
| 3 | 2 | 3 |
----------------------------------------------------------------
| 4 | 2 | 4 |
----------------------------------------------------------------
| 5 | 2 | 5 |
----------------------------------------------------------------
| 6 | 3 | 1 |
----------------------------------------------------------------
| 7 | 2 | 1 |
----------------------------------------------------------------
| 8 | 3 | 5 |
----------------------------------------------------------------
| 9 | 2 | 2 |
This is why we use forms for entering and viewing the data. For speeds sake select CustomersTags and hit create table:
This is not user friendly so we don't show primary keys and replace (right-click and select change to) all the i'ds with user friendly combo-boxes. I also change the forms format to Data sheet.
I continue making the form prettier and add two unbound combo boxes to the header to filter the form with. You can do this with listboxes or checkboxes, or etc as well. For instance a multi-select list box so you can select multiple tags. In all cases you just set the forms filter
Option Compare Database
Option Explicit
'select the combobox afterupdate property to get these events
'you can use macro's if you want
'the exact code is very dependent on things like whether you have default values. I just show the most necessary case
Private Sub cmbCustomer_AfterUpdate()
If (IsNull(Me.cmbCustomer) Or IsNull(Me.cmbTag)) Then
'do nothing
Else
'filter the forms record source
Me.filter = "(CustomerID = " & Me.cmbCustomer & ") AND TagID = " & Me.cmbTag
Me.FilterOn = True
End If
End Sub
Private Sub cmbTag_AfterUpdate()
If (IsNull(Me.cmbCustomer) Or IsNull(Me.cmbTag)) Then
'do nothing
Else
'the filter is just the WHERE PART of an sql statement without the where
Me.filter = "(CustomerID = " & Me.cmbCustomer & ") AND TagID = " & Me.cmbTag
Me.FilterOn = True
End If
End Sub
Next, I switch the forms format to continuous forms and continue prettyifying because to my surprise the datasheet format was blocking setting the filter string.
Here is some guidance for making comboboxes, comboboxes are used for displaying userfriendly values instead of IDs. For instance instead of displaying the raw CustomerID we display the Customer_name but the value in the combobox is the CustomerID. The unbound combobox's value is the CustomerID corresponding to the user friendly Customer_Name the user selects. Whether the combobox is bound or unbound you set them up in the same way. Select a bunch of columns from some table or query then declare how many columns you are showing and their width. To hide a column set it's width to 0. The order of the columns determines the order of the numbers. I bound to the tables and both tables go ID then user friendly description so column-widths is 0,1
This example allows selecting impossible combinations of Customer and tag which will cause an error. One way to avoid this would be to set the rowsource of the alternate combobox when you choose a value on the first. For instance, when you pick a customer you restrict the tags which can be selected in the next combobox. This is cascading comboboxes:
Option Compare Database
Option Explicit
Private Sub cmbCustomer_AfterUpdate()
'it helps to use the query designer to get the sql right
Me.cmbTag.RowSource = "SELECT Tags.TagID, Tags.FriendlyDescription" & _
" FROM Tags INNER JOIN (Customers INNER JOIN CustomersTags ON Customers.CustomerID = CustomersTags.CustomerID)" & _
" ON Tags.TagID = CustomersTags.TagID WHERE (((CustomersTags.CustomerID)= " & Me.cmbCustomer & "))"
Me.cmbTag.Visible = True
End Sub
Private Sub cmbTag_AfterUpdate()
Me.filter = "(CustomerID = " & Me.cmbCustomer & ") AND TagID = " & Me.cmbTag
Me.FilterOn = True
Me.cmbCustomer.SetFocus
Me.cmbTag.Visible = False 'quick and dirty reset 'can't invis a focused control
Me.Refresh
End Sub

Update a MS Access table using a formula stored in another table

I have one table called PayerFormulas that includes a different formula for each payer. Another table is PayerData that has the payer and the data we receive. I want to populate PayerData.CheckNum using the appropriate formula stored in PayerFormulas. The CheckNum data in the table below is what I would like the result to be.
In pseudocode it feels like this is what I'm looking for.
Update PayerData PD
inner join PayerFormulas PF on PD.Payer = PF.Payer
set PD.Check = PF.Formula
I've tried the above in a regular Access query and it doesn't work. Trying the following code only puts the formula text in the table, not the result. I've looked at using Eval() in some way, but since my result will often include text, it doesn't look like the route to go down..
Sub testFormula()
Dim SQLString As String
Dim ActiveQuery As QueryDef
SQLString = "Update PayerData PD inner join PayerFormulas PF on PD.Payer = PF.Payer set PD.CheckNum = PF.Formula"
Set ActiveQuery = CurrentDb.CreateQueryDef("", SQLString)
ActiveQuery.Execute dbFailOnError
End Sub
PayerFormulas table
Payer | Formula
-----------|--------
Visa | mid([PayerData].[Data],3,2)
Mastercard | left([PayerData].[Data],2)
Amex | right([PayerData].[Data],2)
PayerData table
Payer | Data | CheckNum
-----------|--------|---------
Visa | 123456 | 34
Visa | ABCDEF | CD
Visa | qwerty | er
Mastercard | 123456 | 12
Mastercard | ABCDEF | AB
Mastercard | qwerty | qw
Amex | 123456 | 56
Amex | ABCDEF | EF
Amex | qwerty | ty
Thank you for any help!
Okay, to continue with the original example of 3 formulas. Your idea to use Eval() is valid but have to concatenate the variable provided by [Data] field. Modify the formulas table to break up the function parts into separate fields.
Payer | Func | Arg1 | Arg2
-----------|---------------------
Visa | mid | 3 | 2
Mastercard | left | 2 |
Amex | right | 2 |
Then in the query that joins tables:
CheckNum: Eval([Func] & "('" & [Data] & "', " & [Arg1] & ", " & [Arg2] & ")")
Note the apostrophe delimiters around [Data].
Or the args can be in one field and entered as comma separated value: 3, 2. Then:
CheckNum: Eval([Func] & "('" & [Data] & "', " & [Args] & ")")
Or, if you really want the formula in one field, enter it as: Mid(PayData,3,2), Left(PayData,2), Right(PayData,2). Then in query calculation, Replace PayData with value of Data field:
CheckNum: Eval(Replace([Formula], "PayData", "'" & [Data] & "'"))
BTW, don't really need to save the result to table, calculate when needed.
Only 3 formulas? Use conditional code in the VBA. Send a card identifier to the procedure as an argument:
Sub testFormulat(strC As String)
Dim result As String
Select Case strC
Case "Visa"
result = Mid(Me.Data, 3, 2)
Case "Mastercard"
result = Left(Me.Data, 2)
Case "AMEX"
result = Right(Me.Data, 2)
End Select
CurrentDb.Execute "Update PayerData set CheckNum = '" & result & "' WHERE ID = " & Me.ID
End Sub
Where will the Data value come from? Is code behind a form? Is form bound to the table result needs to be saved in? If so, the SQL is not needed, simply:
Me!PayerData = result

Access Query or VBA - Split data into line level

How do I get the following
Project | Code 1 | Code 2 | Code 3
1 | a | b | c
2 | a | d
translate into getting code on line level by project?
Project | Code
1 | a
1 | b
1 | c
2 | a
2 | d
I am currently doing this manually but the problem is I have 30 codes. My current process is do a select query and then do a union for all 30 codes. Can someone help me out with this? I know a vba script will do the job but weren't sure how to do this. Thanks!
You can support any number of project codes by creating a new table, let's call it ProjectCode.
So you'd have one table for Projects, that doesn't have anything to do with codes.
If codes have extra data attached, you can create a Code table and add that information.
ProjectCode helps the database to see how they are related by using a JOIN. the structure would be
ProjectID | CodeID
What I've done for cases like this, is to use the main form for projects, and have a continuous form in datasheet view as a subform, linking to the ProjectCode-like table, and link the ID from Projects to the ProjectID of the subform. (Link Master Fields & Link Child Fields properties of the subform object), note you'll want to hide ProjectID on the subform.
To get a list of codes, you can just query ProjectCode with the project ID. if you need additional information from projects, use a join:
SELECT * FROM Projects INNER JOIN ProjectCode ON ProjectCode . ProjectID = Projects.ID
WHERE ID = 'some id'
Note that the information from Projects will be repeated for each code.
With this function you can read data from a table with N codes fields named (code1, code2, ..., codeN) and write them to a table with only two fields:
Function Verticalize(numberofcodes As Integer)
Dim sql As String
Dim rst As New ADBDb.Recordset
sql = "SELECT * FROM tname"
rst.Open sql, CurrentProject.AccessConnection
While Not rst.EOF
For i = 1 To numberofcodes
DoCmd.RunSQL "insert into tdest values (" & rst(0) & "," & rst("code") & Str(i) & ")"
Next i
rst.MoveNext
Wend
End Function

VB Loop for MS Access that adds numbers

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.

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.