So I'm working on automating a tedious process that uncle sam makes us do while deployed. I have a database where we put in a date for each day we are deployed into a table. For instance I have 4/10/2017 through 7/11/2017 listed.
Each day needs the date and location displayed and I need to put six results stacked vertically then move to the right to do the next six three more times.
I have one report named 2282report which is the master one with four subLocation[1-4]s in the appropriate spot. Originally I had it do TOP 6 then the next one would do TOP 6 but where ID > 6 but then I moved to make the date the ID as the date can't be duplicated anyway. I'm unsure of the proper way to make them linked so that the next subreport will display continue the rest.
The report looks like this when ran. I will also have over 90 days usually to list so I will need to create a second page to the main report.
What I'm thinking I'll have to do is create a new subreport for the entire location block but I don't know how to make the report_details move to a new column once it shows 6 results.
Another option I just thought of is to leave the subreports blank then make the master report set the controlsource for each one via vba. I feel this one may work because then it can check to see if there are more days then there are lines so that it can create a new page to continue. But then I need to figure out how to make it continue to the next page. There will also be a bottom section that will only have 16 days versus the 24 on the top.
You might be tempted to use VBA in the various report event handlers, but even if this might work, from my experience that would only lead to headaches trying to get everything to format properly. Instead, I recommend creating a new reporting table with a page number on each row. Source the main report from the sorted page number list and bind a multi-column subreport via the page number field. Populate the reporting table with a simple VBA procedure that correctly paginates the rows according to your scheme.
First the reporting table (add constraints as you find necessary):
CREATE TABLE SubReportTable (PageNum LONG, PageOrdinal LONG, _
Ordinal LONG, LastPage BIT, [Date of Service] DATE, [Location] TEXT)
On the SubReport:
Set RecordSoucre property: SubReportTable (alternatively specify a query that sorts on the desired fields)
Set Number of Columns to 4 along with other column settings (padding, direction, etc.).
Set CanGrow property to No on Detail section and other controls as appropriate.
Resize columns and detail section to properly fit all columns on page. (This will likely require going back and forth between print preview and design on the Main Form.)
On the main report, set the following properties:
RecordSource property: SELECT SubReportTable.PageNum FROM SubReportTable WHERE (((SubReportTable.LastPage)=False)) GROUP BY SubReportTable.PageNum ORDER BY SubReportTable.PageNum
Detail section property Force New Page to After Section.
SubReport object's Link Master Fields and Link Child Fields both to PageNum
Resize SubReport object to fit all columns properly.
Duplicate the behavior on the Main Report on a separate "Last Page" report. Set this report to select the proper subset of records based on the pagination data in the reporting table (i.e. LastPage = True). Depending on how different the last page with 16 records is formatted, it might also require creating a separate SubReport just for the 16 records, but you might get away with using the same SubReport as the main report... that'll be your problem to determine.
RecordSource property: SELECT SubReportTable.PageNum FROM SubReportTable WHERE ((SubReportTable.LastPage = True)) GROUP BY SubReportTable.PageNum ORDER BY SubReportTable.PageNum
Finally some code to populate the reporting table. You can either run this procedure directly from the VBA Immediate Window, or put it in some button's click event handler. The pagination logic can be tweaked to get the right amount of records on the last page.
Public Sub PrepareSubReporTable()
On Error GoTo Catch_PrepareSubReporTable
Dim db As Database
Dim rs As Recordset2
Dim rows As Long, pgs24 As Long, rowsLast24 As Long, rows16 As Long
Dim i As Long, p As Long, pi As Long
Set db = CurrentDb
db.Execute "DELETE * FROM [SubReportTable]", dbFailOnError
db.Execute _
"INSERT INTO SubReportTable ( PageNum, PageOrdinal, Ordinal, LastPage, [Date of Service], [Location] )" & _
" SELECT Null AS PageNum, Null AS PageOrdinal, Null AS Ordinal, False as LastPage," & _
" [Data].[Date of Service], [Data].[Location]" & _
" FROM [Data]" & _
" ORDER BY [Data].[Date of Service], [Data].[Location];", _
dbFailOnError
rows = db.OpenRecordset("SELECT Count(*) FROM SubReportTable").Fields(0)
pgs24 = rows \ 24
rows16 = rows - 24 * pgs24
If rows16 > 16 Then
rowsLast24 = rows16
pgs24 = pgs24 + 1
rows16 = 0
Else
rowsLast24 = 24
End If
Set rs = db.OpenRecordset( _
"SELECT * FROM SubReportTable" & _
" ORDER BY [Date of Service], [Location];")
i = 0
Do Until rs.EOF
p = i \ 24 + 1
rs.Edit
rs![PageNum] = p
If p > pgs24 Then
rs![lastPage] = True
pi = (i - pgs24 * 24) Mod 16 + 1
Else
pi = i Mod 24 + 1
End If
rs![PageOrdinal] = pi
i = i + 1
rs![Ordinal] = i
rs.Update
rs.MoveNext
Loop
rs.Close
Exit Sub
Catch_PrepareSubReporTable:
MsgBox Err.Number & ": " & Err.Description, _
vbOKOnly Or vbExclamation, "Error in PrepareSubReporTable"
End Sub
Now generate the main report and the last-page report, either manually or in VBA code somewhere.
Note: I used the field name PageNum instead of Page because that seemed to cause problems with a SubReport binding during print preview... probably because Page is the name of an existing variable / function for reports.
Related
Where I work we receive electronic meters from customers and try to solve the errors they have. We will receive 4-8 meters at a time that all have the same problem, same specs, same everything, the only thing different between each meter is the Serial Number. I want to be able to enter every serial number, and the common specs all in one form to create multiple records.
Here's a pic of what I have for the form. I was able to create records for just one serial number at a time, but I would like to do it all at once to make data entry quicker and easier.
Meter Entry Form
So summary, Multiple Meters, all identical specs, different serial numbers. I want to enter it all into a form and have multiple records created. Thanks for any help or insight you can provide me.
-Chris
You could bind a subform to the table that stores your meter records and then have some unbound fields on your main form that allows you to enter the information that would be repeated in your batch of records. You could also put another unbound text box on the main form to specify the number of records you want that will have this repeated information.
So in the mock-up below, you'd specify how many records you want (red box), e.g. 10 records:
Then you'd supply the data that would be repeated for these 10 records (blue boxes):
You'd then click a button that would create the number of records specified with the repeated information given:
It would then just be a case completing the unique serial number for each of the records in the batch you have generated.
Here's the VBA I used on the Add button:
Private Sub cmdAddRecords_Click()
batchAdd Me.txtRecords
Me.tblMeters_sub.Requery
End Sub
...and the batchAdd sub routine it calls:
Public Sub batchAdd(records As Integer)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim i As Integer
Set db = CurrentDb
Set rs = db.OpenRecordset("tblMeters")
i = 1
Do While i <= records
rs.AddNew
rs!SerialNumber = ""
rs!MeterFirmware = Me.MeterFirmware
rs!MeterCatalog = Me.MeterCatalog
rs!Customer = Me.Customer
rs!MeterKh = Me.MeterKh
rs!MeterForm = Me.MeterForm
rs!MeterType = Me.MeterType
rs!MeterVoltage = Me.MeterVoltage
rs.Update
i = i + 1
Loop
rs.Close
Set rs = Nothing
Set db = Nothing
End Sub
Here's a link to the mock-up (if you want a closer look).
Update
In response to your query about whether the subform could be filtered so it doesn't just become a big list of all meters, you could add another field to the tblMeters table that will take the date and time that you added records to the table:
You'd then need to add another line to the batchAdd sub that will put the system time and date in this new field:
...
Do While i <= records
rs.AddNew
rs!SerialNumber = ""
rs!MeterFirmware = Me.MeterFirmware
rs!MeterCatalog = Me.MeterCatalog
rs!Customer = Me.Customer
rs!MeterKh = Me.MeterKh
rs!MeterForm = Me.MeterForm
rs!MeterType = Me.MeterType
rs!MeterVoltage = Me.MeterVoltage
rs!DateAdded = Now ' <-- HERE!
rs.Update
i = i + 1
Loop
...
You'll then need to change the subform's Record Source property (Design View > select subform > Property Sheet > Data tab > Record Source):
Put the following SQL in there:
SELECT TOP 15 tblMeters.SerialNumber, tblMeters.MeterFirmware, tblMeters.MeterCatalog,
tblMeters.Customer, tblMeters.MeterType, tblMeters.MeterForm, tblMeters.MeterKh,
tblMeters.MeterVoltage, tblMeters.DateAdded
FROM tblMeters
ORDER BY tblMeters.DateAdded DESC;
... which will order the records by the date/time field (most recent at the top) and then show only the first 15 of these records. If you want a different number of records change the TOP 15 bit to a different number of your choosing.
When you click "Add", your new batch of records should be added to the top of the list and the list should stay at a maximum of 15 records (or whatever number you specify in TOP ...)
Be aware that when I was testing this, clicking the "Add" button rapidly a few times seemed to cause the sql to not bother with the TOP ... filter, but as long there's like a second or more between each "Add" click it seemed to work fine.
Hope this helps.
The initial problem that led me to learn VBA is as follows:
You have a table that can be up to 10,000 rows (several hundred pages) long in a Word document. The table has a title in the form of a paragraph above the first row. This title is styled such that it links to a Table of Contents (Style = "Caption"). The table must be broken at the last row on each page, and the title must be inserted before the new table but in a different style that is not linked to the Table of Contents(Style = "Caption Cont").
The first page will look like this:
The second page will look like this:
My first solution was relatively hackey and not at all elegant. I've managed to put together the following solution that works quite well. However, the initial process of determining the row number at which the table crosses pages is pretty slow due to the use of Range.Information. I'm wondering if there's a faster way to determine the bottom row on the page.
Putting the document into wdNormalView shaves off about a second per page, even with Application.ScreenUpdating = False...
The program requires that your cursor is somewhere inside the table, which is fine and not a functionality I wish to remove.
It currently does about 120 pages per minute, with the majority of the time being spent on determining the row to split at (i.e. splitNum). I'm sure it can be much faster with a different method of determining splitNum.
I CANNOT ADD AN ADDITIONAL ROW TO THE TABLE FOR THE PURPOSES OF USING "REPEAT HEADER ROWS".
It would violate regulations that are enforced in my industry, and a non-conforming document can be a huge hit to the company and future business
Code:
Sub tblSplit()
Dim timeCheck As Double
Application.ScreenUpdating = False
Application.ActiveWindow.View = wdNormalView
timeCheck = Time
On Error GoTo ErrH
Dim crossRef As Range, delRange As Range, tblR As Range, newTbl As Range
Dim tblNumField As Range, tblNum As String
Set tblNumField = Selection.Tables(1).Range
tblNumField.MoveStart wdParagraph, -1
tblNum = tblNumField.Words(2)
Set crossRef = Selection.Tables(1).Range
Set thisTbl = Selection.Tables(1).Rows(1).Range
Set tblR = Selection.Tables(1).Range
Insert cross-reference to title with style "Caption Cont"
crossRef.Move wdCharacter, -2
crossRef.InsertCrossReference ReferenceType:="Table", ReferenceKind:= _
wdOnlyCaptionText, ReferenceItem:=tblNum, InsertAsHyperlink:=True, _
IncludePosition:=False, SeparateNumbers:=False, SeparatorString:=" "
crossRef.Text = vbCr & " (Cont.)" & vbTab
crossRef.MoveStart wdCharacter, 1
crossRef.Style = "Caption Cont."
crossRef.Collapse wdCollapseStart
crossRef.InsertCrossReference ReferenceType:="Table", ReferenceKind:= _
wdOnlyLabelAndNumber, ReferenceItem:=tblNum, InsertAsHyperlink:=True, _
IncludePosition:=False, SeparateNumbers:=False, SeparatorString:=" "
crossRef.MoveEnd wdParagraph, 1
Delete duplicate title
Set delRange = crossRef.Duplicate
crossRef.MoveEnd wdParagraph, 1
crossRef.Copy
delRange.Text = vbNullString
Find row at which table spans two pages
Dim splitNum As Long, n As Long, i As Long, pageNum As Long
pageNum = tblR.Rows(1).Range.Information(wdActiveEndAdjustedPageNumber)
i = 15
Do
If tblR.Rows(i).Next.Range.Information(wdActiveEndAdjustedPageNumber) <> pageNum Then
splitNum = i
Exit Do
End If
i = i + 1
Loop Until i = 100 'arbitrary cap to prevent infinite loop
n = 1
Split and format
Do
DoEvents
'Split and format
tblR.Tables(n).Split (splitNum)
tblR.Tables(n).Rows.Last.Borders(wdBorderBottom).LineStyle = wdLineStyleSingle
'Paste the stuff
Set newTbl = tblR.Tables(n + 1).Range
newTbl.Move wdParagraph, -2
newTbl.Paste
newTbl.MoveEnd wdParagraph, 1
'Clear excess
newTbl.Paragraphs.Last.Range.Text = vbNullString
'Next
n = n + 1
Loop Until tblR.Tables(n).Rows.Count < splitNum
Restore state, report time, safe-exit and error handler set-up for debugging
Application.ActiveWindow.View = wdPrintView
Application.ScreenUpdating = True
MsgBox "Pages completed: " & n & vbCr & _
"Time (sec): " & DateDiff("s", timeCheck, Time) & vbCr & _
"Seconds per page: " & CDbl(DateDiff("s", timeCheck, Time)) / CDbl(n) & vbCr & _
"Pages per minute: " & n / DateDiff("s", timeCheck, Time) * 60
Exit Sub
ErrH:
Application.ScreenUpdating = True
Err.Raise Err.Number
Stop
End Sub
Here is a workaround that makes it seem as if the paragraph is separate from the table, but it's really the first row. I created a table of data with a couple hundred rows, then inserted a new row on the top of the table. The paragraph goes into this empty row at the top. Assuming the rest of the table has borders enabled, disable the top, left, and right borders for the first row only. Then enable "repeat header row". It looks like this when you're done:
and then the second page:
The fundamental problem you will have with any table-splitting approach is that it doesn't take account of the fact that Word uses the active printer driver to optimise the page layout.
Consequently, you may end up with what appears as the last row on a page when one printer is used ending up at the top of the next page when another printer is used. Alternatively, your headings might end up at the bottom of the page when a another printer is used.
As for your 'Caption'/'Caption Cont' Style machinations, that is all quite unnecessary if you create a full-with row for the caption at the top of the table and mark both the first and second rows as table header rows. Only the entry at the top of the table will appear in the Table of Contents. No code required.
This can be done in a much simpler way by inserting the copy of the table caption in a row at the top of the second part of a split table.
Split the table at the end of the first page as you already do. The rest of the activities now focus on the second part of the table.
For the second part of the table insert a single row at the top of the table.
Make the new single row a repeating table heading.
Paste the copy of the table caption in the row at the top of the second part of the table.
This will achieve the effect you are trying to achieve.
I have an excel sheet that I use as database, and a search form that allow user to search for information, based on some criterias, and display the filtered results in a new sheet.
I am using SQL to fetch data, and display them to the user.
I use something like top open connection, then to create a record set and pass my sql request to it
m_Connection.Open "Provider=Microsoft.ACE.OLEDB.12.0;" & _
"Data Source=" & ThisWorkbook.FullName & ";Extended Properties=""Excel 12.0;HDR=Yes;"";"
Set OpenRecordset = CreateObject("ADODB.Recordset")
OpenRecordset.Open sql, GetConnection(), 3, 3, &H1
Set rst = OpenRecordset("SELECT * FROM [Database$] Where " & Myconditions & ";")
Everything works fine, but I need to allow users to choose the column headers names and order that may be different from what i have in the sheet from which i make my select that i call database
We need this because users are in different countries, so the display name will be configured based on the country of the user, but also we want to allow user to change the display name and order of the fields based on their needs and because the display name may be too long (multi line).
My real "database" sheet is about 110 columns, and about 1000 records (rows).
I can't publish the real data because it's confidential, but to help you understand me, I created this example that represent what I have
let us suppose I have this "database"
the user enter this search screen to select the information he needs from the database
I wish that the user will get this result and not in the same order and display of the database
as you can see, the display names and orders of the columns in the result page is different from the "database" sheet
I would like to create a configuration page, in which the user can specify the display name he wants and the order in which he wants the fields to appears. something like that
Is there any fast way to do that directly in my Recordset in SQL/EXCEL or I should after I fetch data, change the headers in excel sheet using vba ? if so, I have to make a kind of array in vba that contains both database Names and display names and replace the names of the database by its corresponding just before I show the result page shows ?
any suggestions ?
same question about the order of the fields, how to sort them based on the order the user choosed ? any fast way ?
Thanks for anyone who can help with the best way to do that
You could do the following, it loops through the data range based on the number of rows in the range being the min and max of positions available, it looks for these rankings in turn, in column C, then checks if shown, then add's the field name and it's alias to an array. This array is then joined. So using data similar to yours, in columns of the the same ordering, I called:
GenerateOrderedSQL("table 1",range("a2:d6"),3,4) A1:D1 contained headers
This called my function
Function GenerateOrderedSQL(strInputTable As String, _
rngRangeForSelection As Excel.Range, _
lngOrderColumn As Long, _
lngShowColumn As Long) As String
Dim l As Long
Dim fPos As Long
Dim lfPos As Long
Dim a() As Variant
l = rngRangeForSelection.Rows.Count
ReDim a(l)
For fPos = 1 To l
lfPos = Application.WorksheetFunction.Match(fPos, _
rngRangeForSelection.Columns(lngOrderColumn), 0)
If rngRangeForSelection.Cells(lfPos, lngShowColumn).Value = "Yes" Then
a(fPos-1) = "[" & rngRangeForSelection.Cells(lfPos, 1) & _
"] AS [" & rngRangeForSelection.Cells(lfPos, 2) & "]"
a(fPos-1) = a(fPos-1) & IIf(fPos < l, ",", vbNullString)
End If
Next
Debug.Print "SELECT " & Join(a, vbNullString) & " FROM [" & strInputTable; "]"
End Function`
This gave the following
SELECT [Fname] AS [First Name],[Lname] AS [Last Name],[Zip] AS [Zip],[City] AS [City] FROM [table 1]
I have an Excel workbook with two dynamic OLE DB queries. I'm having issues with refreshes.
To set things up I have a SQL table-valued function as the source. The data is of the nature of...
SGrp SG_Desc SKU SKU_Desc Server Billed
1 Item 1 111 whatever 15 12
1 Item 2 222 some more 10 9
2 Item 3 333 zzz 10 8
3 Item 4 555 abc 20 18
On the first sheet ("Overall") I have a data connection that summarizes the groups with the command text of dynamically modified with one button.
SELECT SGrp, SG_Desc, SUM(Served) AS Served, SUM(Billed) AS Billed FROM mySQLdb ('8/19/2018','8/25/2018') WHERE SGrp <> '' GROUP BY SGrp, SG_Desc ORDER BY SG_Desc
I then have a cell with a data validation list that selects the group and a button to execute the VBA to dynamically modify the other connection. There's also two cells with the report start & end date for filtering. When I push the "Detail" button it runs the code below.
Private Sub RunDetail_Click()
Dim StartDate As Date
Dim EndDate As Date
Dim SGrp As String
Range("A1").Value = Range("G8").Value2
StartDate = Sheets("Overall").Range("H1").Value
EndDate = Sheets("Overall").Range("H2").Value
SGrp = Sheets("Overall").Range("A1").Value
SGrp = LTrim(RTrim(SGrp))
With ActiveWorkbook.Connections("CJP_DeliveryRecap_Detail").OLEDBConnection
.CommandText = "SELECT SKU, SKU_Desc, Served, Billed FROM mySQLdb ('" & StartDate & "','" & EndDate & "') WHERE SG_Desc='" & SGrp & "'"
' .Refresh
' ActiveWorkbook.Connections("CJP_DeliveryRecap_Detail").Refresh
End With
'RefreshOLEDB
'ThisWorkbook.RefreshAll
ActiveWorkbook.Connections("CJP_DeliveryRecap_Detail").Refresh
'Application.CalculateUntilAsyncQueriesDone
Application.Wait (Now + TimeValue("0:00:03"))
Dim rc As Integer
Dim i As Integer
Worksheets("Detail").Activate
With Worksheets("Detail").Range("CJP_DeliveryRecap_Detail")
rc = .Rows.Count
End With
With Worksheets("Detail").Range("E1048576")
.Select
.End(xlUp).Select
End With
i = Selection.Row
Worksheets("Detail").Range("E5").Select
Worksheets("Detail").Range("E5:G" & i).ClearContents
Worksheets("Detail").Range("E5").Value = 1
Worksheets("Detail").Range("F5").Value = "=+CJP_DeliveryRecap_Detail[#Served]*E5"
Worksheets("Detail").Range("G5").Value = "=+CJP_DeliveryRecap_Detail[#Billed]*E5"
Sheets("Detail").Range("E5:G5").Copy Sheets("Detail").Range("E6:E" & rc + 4)
Sheets("Detail").Range("F1").Value = ("=SUM(F5:F" & rc + 4 & ")")
Sheets("Detail").Range("G1").Value = ("=SUM(G5:G" & rc + 4 & ")")
End Sub
So, what is it doing? The first "strange" thing is I occasionally get errors in the code that I have to continue on. A lot of times, but not all, it hits the Application.Wait line, then the Worksheets("Detail").Activate line, and sometimes the line where I set values or copy data.
There are some comments where I've been testing various refreshes etc. The issue is because while when the code completes it displays the data conn detail correctly but the calculations as to the size of the results is from the prior set. If I click the button a second time then it calculates them correctly. I would of course prefer not to have a arbitrary 3sec delay but simply run the rest of the code after the resultant records have been retrieved.
Where am I going wrong as I've been banging my head against the wall with this. Most of what I do is in Access but Excel was the proper tool in this case.
I just thought of, but not thrilled to, would it work to have a separate sub to handle the record size calcs and cell values clear\set and call that from the button click or would that still not be generated until after the original sub ends.
Thanks in advance,
You need to wait till query is complete before proceeding with calculations.
What you need is to add .BackgroundQuery = False to your connection before .refresh
With ActiveWorkbook.Connections("CJP_DeliveryRecap_Detail").OLEDBConnection
.BackgroundQuery = False
.CommandText = "SELECT SKU, SKU_Desc, Served, Billed FROM mySQLdb ......"
.Refresh
End With
This should help
If the stored procedure being called from Excel does not contain "SET NOCOUNT ON" it will not run properly from Excel. I had the same issue and SET NOCOUNT ON was not included in my stored proc; the second i added it, it worked!
I have to create a text box in MS Access where users are able to see the top 3 records of a particular result set. So even if the query results in 5 records I only want it to display the top 3 records as three textboxes (sometimes the result may also be 1,2 or 0 records).
I took the easy way out and created a new subform which was connected to the parent form using master/child field. The textbox was placed in the details part of the subform and as a recordsource of the subfrom used the following query:
Select top 3 tbl1.column1, tbl1.column2
from tbl1
column1 is the control source for the textbox and column2 is the column I have used for master/child link.
Now the catch is that the query works fine when I use it without top 3. But when I use top 3 the textbox suddenly disappears and the subform is completely blank.
I am not able to identify the cause of the error. My guess is that it has something to do with type of the subform. Not sure.
Is there any other way I can have a text box whose number can vary on the basis of the results?(but limiting the resultset to 3)
Appreciate the help.
Textbox are not meant to hold more than 1 value.
You are trying to assign three results of 2 columns to one textbox(No can do).
Use listbox to populate as you are doing, assigning the query you just wrote in the rowsource of the list(no subforms needed). This way users will see the three records.
You could use a textbox in order to accomplish what you are trying to do. But will require some VBA coding to accomplish this.
Public function CombineValuesForTextBox() as string
Dim rst as dao.recordset
Dim strSQL as string
strSQL = "SELECT TOP 3 tbl1.Column1 as field1, tbl1.Column2 as field2 " & _
"FROM tbl1;"
set rst = currentdb.openrecordset(strsql)
if rst.recordcount = 0 then 'Checks if the recordset has records or not
CombineValuesForTextBox = "No records found"
goto EndCode 'Or replace with what actions to take if no records are found
else
rst.movelast 'Forces the recordset to fully load
rst.movefirst
do while not rst.eof
if CombineValuesForTextBox = "" or CombineValuesForTextBox = empty then
CombineValuesForTextBox = rst![field1] & " - " & rst![Field2]
else
CombineValuesForTextBox = CombineValuesForTextBox & vbcrlf & _
rst![field1] & " - " & rst![Field2]
end if
Loop
end if
rst.close
set rst = nothing
EndCode:
if not rst is nothing then
rst.close
set rst = nothing
end if
end function
Then on your form put in the code (be sure the textbox is unbound...)
me.textbox = CombineValuesForTextBox