Use ContentControls to insert insert LastSavedBy Name, Date and Time information into Word form - ms-word

I've set up a word form with different sections, each section to be completed by a separate person.
Each section of the form includes a combination of dropdown lists or free text fields set up using named Content controls.
At the end of each section there is field for the staff member to record their name, the date (calendar icon), and a command button with an associated macro to save the form and protect the data once that section has been completed.
While this enables the data to be protected it does not prevent person A from completing the form and entering person B's name. Note, this would not be expected, but traceability is required for regulatory purposes.
I would like to update the macro to protect the sections, save the file (currently working), and then immediately after saving, have the macro populate another field directly under the manually entered name and date with the Microsoft advanced properties of Last Saved By and the date and time. This would confirm the user.
My macro below (InsertMSSavedDetails()) will extract the Microsoft required data, but only if I manually click on the form and run the macro, and then it saves wherever I click on the form and won't save to the named ContentControl box. I'd like to automate this last step so that it is not reliant upon the user and the data is associated with particular section.
My form includes the following code:
Module
Sub ProtectFieldsSections2()
' protects Sections 1 and 2
If MsgBox("Do you want to Lock and Protect this section from further editing?", vbYesNo) = vbNo
Then Exit Sub
Dim sec As Section
Dim cc As ContentControl
Set sec = ActiveDocument.Sections(2)
For Each cc In sec.Range.ContentControls
cc.LockContents = True
Next cc
End Sub
This document:
Private Sub CommandButton2_Click()
ProtectFieldsSection1
ProtectFieldsSection2
ActiveDocument.Save
End Sub
Current code for Adding Microsoft data - which technically works, but not in the manner I need:
Sub InsertMSSavedDetails()
'
ActiveDocument.SelectContentControlsByTitle ("MSSavedDetails")
Selection.TypeText Text:="Check data: "
Selection.Fields.Add Range:=Selection.Range, Type:=wdFieldEmpty, Text:= _
"LASTSAVEDBY ", PreserveFormatting:=True
Selection.Fields.Add Range:=Selection.Range, Type:=wdFieldEmpty, Text:= _
"SAVEDATE \# ""d/MM/yyyy h:mm:ss am/pm"" ", PreserveFormatting:=True
End Sub
Second issue with the above, is that it would only need to be added the once, when Sub CommandButton2_Click is first selected. If I were to update the Sub CommandButton2_Click, how would I stop if from repeating the cycle of save, insert Microsoft data, save, insert Microsoft data?
NOTE: Use of word vs PDF form. Presently, the form is used in word format, converted to PDF, and electronically signed, however this does not allow others to add their information to the remaining sections. Adobe LiveCycler has been used to design PDF forms but there were issues with maintaining images and graphs, and there were other issues experienced with the Adobe reader, so the option is to remain with word.
Thank you.

Not sure I have completely understood the problems, but
The problem with inserting the field codes LASTSAVEDBY and SAVEDATE is that unless you "lock" them, their values will always reflect the most recent save. Also, if you want the fields to go in a content control, you have to use a rich text content control.
Perhaps better to
save the document
get the underlying property values (the ones that LASTSAVEDBY and SAVEDATE display) and insert them
save the document
If you have content controls named (say)
Section1LastSavedBy
Section1SaveDate
Section2LastSavedBy
etc.
then
you know which commandbutton the user clicked so you know which section you are dealing with
your code can then look something like
Private Sub CommandButton2_Click()
ProtectFieldsSection1
ProtectFieldsSection2
With ActiveDocument
.Save
.SelectContentControlsByTitle("Section1LastSavedBy")(1).Range.Text = _
.BuiltinDocumentProperties("Last Author").Value
.SelectContentControlsByTitle("Section1SaveDate")(1).Range.Text = _
format(.BuiltinDocumentProperties("Last Save Time").Value,"D/MM/YYYY h:mm:ss am/pm")
.Save
End With
End Sub
If you want to construct the content control names programmatically based on Section number, you can.
If you want to lock the content controls against further editing, you can.
I am not sure what problem you are encountering here:
"Second issue with the above, is that it would only need to be added the once, when Sub CommandButton2_Click is first selected. If I were to update the Sub CommandButton2_Click, how would I stop if from repeating the cycle of save, insert Microsoft data, save, insert Microsoft data?"
unless you are using a Save event. If that's the problem we could revisit that part.
It's pretty difficult to stop people tampering with data in Word, but personally I would consider saving copies of these values in, e.g., a Custom XML Part (not mapped to the controls) or Document Variables - possibly also encrypt them - you can use Windows crypto APIs for that.
Incidentally, I suspect the problem you have with your existing code is that .SelectContentControlsBYTitle doesn't Select the controls in the normal Word sense. It just returns a collection of controls with that name.

Related

Microsoft Access 2010 lag in continuous form header subforms

I am a research student working on an access database where I have created a continuous form that is to be used in a sort of scrolling electronic health record format. The header includes information about the patient and the continuous form aspect is a series of patient visits. In the header, I have a few subforms, which populate based on linking on the patient_ID number which is posted in the header (generated by vba macro such that previously reviewed patients aren't revisited). These subforms seem to significantly lag such that they the results from the previous patient stick around going into the next one. The subforms contain user-selected pertinent data. Each form has its own table. They are linked primarily based on patient_ID.
I have tried:
macro on the header detail: on click, refresh. - seems to work, but not very elegant/intuitive
macro on the main form - same as above but on load, click, got focus, lost focus, open, activate -- none of them seem to do anything.
forced requery via vba (see below) on opening/etc of the form. Neither way has worked. Tried to run these on opening the header form.
Public Function RequeryMain()
Dim frmMain As Form
Set frm = Forms("FRM_continuous_reports_patient")
frm.Requery
End Function
Public Function RequeryHeader()
Dim frmHeader As SubForm
Set frmHeader = Forms("FRM_continuous_reports_patient").FRM_continuous_header_working
frmHeader.Requery
End Function
In the end, it is frustrating for users to have to click to clear the form for new entries. It works otherwise.
The end goal is for the form to open and have all the subforms load based on the newest patient_ID. This would likely have to involve a staggered load: (1) VBA script selects the next patient based on certain characteristics and passes the patient_ID to the main form; (2) load main continuous form based on patient_ID submitted to it; (3) load the header subforms and any pertinent data within (although should be blank for the first time these are seen); (4) on completion, back to (1).
From what I understand, this is already how it is working, however the subforms are loading too quickly? How can I fix this?
Hopefully someone can help explain how to remedy this/correct any misunderstandings I have about the mechanics of forms.
I know this will sound odd, but subforms actually load before parent form. Lag in subform display is not something I have encountered. Code should not be needed and likely will not correct. Must be something about the form/subform design, maybe their RecordSource. Would have to examine db to determine.
It is not necessary to create form objects in VBA just to requery. Is code behind the main form? Me.Requery will be enough for the main form. I always give subform container a name different from the object it holds, like ctrPatient. Then just Me.ctrPatient.Requery.
Why not put subforms in Detail section?
To answer your questions, and provide a condensed version of the logic referenced at: https://accessexperts.com/blog/2014/01/07/delay-loading-subforms-in-access/
is to:
In design view, set your subform SourceObject to "" (and save your form)
When you are ready to show the subform, just execute: Me..SourceObject = ""
When you are ready to navigate to the NEXT patient, clear the subform link: Me..SourceObject = ""
Now that should solve the issue of out of sync data between the main and sub.
You don't need to use the CASE statements, but they operate as if you had a bunch of "If" / "ElseIf" all nestled together -- except the CASE makes it easier to follow. It basically gets a value from a variable (i.e. Select Case MyVariable); then checks to see if it equals what you want (Case 1, Case 2, etc.), and if so, does whatever you code.

Access 2010 - enter references to form elements as the value of a text field

Is there a way to take a value from a field on a form and use it as a reference to a different field in the same form, and not just literally? I want to be able to manually enter something like [txtFlavor] in one field and have it show the actual flavor, the value of the field named "txtFlavor" in another field, and not just the string "[txtFlavor]". I'm basically trying to store some vba references (terminology?) in a table so I can bring up a string of text with references to values on the form.
I have been asked to create a system that will store letter templates in Access 2010 and allow users to choose a record with personal information and insert that info into a template letter, preferably displaying it on a form immediately in plain text. I already proposed using reports to do this but that was unacceptable to the end users. They really just want a form that combines
a) contact records, one at a time
with
b) letter templates, one at a time
I've been trying to store the template info with it's form references in a table, but I have yet to be able to make references pull data from another text field on the form.
Is it possible and/or sensible to try to store something like the following in a table, or to enter it into a field on a form?
[txtFlavor] & " is dull but popular."
and then have it show up elsewhere in the form as
Vanilla is dull but popular.
I sure feel dumb and am sure I've missed something obvious. Everything I do is just repeated literally and not interpreted as a reference.
You could get your users to create their templates using 'tags' as placeholders for the database information, in a similar way to how you would design a merge document in Word. So in your example above, the template document would look like:
{Flavor} is dull but popular.
When it comes time to create the merged result you would need to use the Replace function to change these tags to actual data values. So if you had a read-only text box on your form, the control source could be:
=Replace([txtTemplate], "{Flavor}", [Flavor])
I assume you would have lots of potential tags, so using this approach you would need to nest the Replace functions. I have split my nesting across multiple lines to make it a bit more readable:
=Replace(
Replace(
Replace([txtTemplate], "{EmpName}", [EmpName]),
"{EmpAddress}", [EmpAddress]),
"{EmpPhone}", [EmpPhone])
If you had many more database fields/tags this would start to become very unwieldy so I would recommend using some VBA to make life easier, maybe something along the lines of:
Dim rsSource As Recordset
Dim strMerge As String
Dim intField As Integer
'Assuming your form has a field called EmpNo (numeric data) that you can use to pull data...
Set rsSource = CurrentDb.OpenRecordset ("Select EmpName, EmpAddress, EmpPhone From Employees Where EmpNo = " & Me.EmpNo)
strMerge = txtTemplate
For intField = 0 to rsSource.Fields.Count - 1
strMerge = Replace(strMerge, "{" & rsSource(intField).Name & "}", rsSource(intField))
Next intField
txtMerge = strMerge
rsSource.Close
Set rsSource = Nothing
You could put this code in the AfterUpdate event of your txtTemplate text box and it would update the contents of the txtMerge text box. You either need your tag names to match your database columns, or else you could alias the columns in the Select statement so they match the tag names.

How to apply two filters to a split form to robustly update records in a table

I am storing data on eye colour, for example. I have a table about containing 100's of contact details for people in many different local authorities (LA). These contact details change frequently and a team of users will update the table when necessary.
I want to make a split form to do this for us. The idea is to be able to use a drop-down menu for both the LA (location) and the eye colour in the form, to filter the table below to show only the relevant contact details.
The user can then quickly find the correct record, click on it, this will autofill the rest of the form, and any details can be changed and then saved.
My problem is that I can't make the form robust enough to not erroneously overprint existing contact details, and I can't get a two step filter process to work. The contact details for previous records don't seem to clear, even when filtering for different places, so it is easy to muddle up and overprint records.
Here is a screen shot of the form so far:
example of the split form, filtered for records from Broadland but not by colour yet
Here is the VBA
Option Compare Database
Private Sub Detail_Click()
End Sub
Private Sub LA_AfterUpdate()
Me.Filter = "[LA] = " & Chr(34) & Me.LA & Chr(34)
Me.FilterOn = True
End Sub
Private Sub UpdateRecord_Click()
RunCommand acCmdSaveRecord
Me.Requery
End Sub
If anyone could help with:
How do I filter for colour as well as place?
How can I make the form save the amended record and then clear the form so that no records are overprinted?
If I filter for one thing and click to bring up the full record, how can I make the form drop this record and change to a different one if I decide to re-filter and pick a different record
I would be very grateful! Thank you in advance.
Answer for question 1
1.Open form Design View
2.Click at the table at the bottom of your form
3.Open Property Sheet for that table
4.Open Data at Property Sheet. You will see Link Master Fields and Link Child Fields. Click at ... sign and it will appear Subform Field Linker. Fill in the master and child field's column as you desired (eg. LA, color, place). Then the form will show the result.
Answer for Question 2
You have to use vba code under UPDATE RECORD button. For clearing the form after saving the record, use DoCmd.OpenForm "your form name",,,,acFormAdd
Example answer for question 2:
Private Sub TESTSAVE_Click()
If MsgBox("Changes have been made to this record." _
& vbCrLf & vbCrLf & "Do you want to save these changes?" _
, vbYesNo, "Changes Made") = vbYes Then
DoCmd.RunCommand acCmdSaveRecord
DoCmd.Close
DoCmd.OpenForm "NewATN2",,,,acFormAdd
Else
DoCmd.RunCommand acCmdUndo
End If
End Sub

How can I increase a form field every time the document is printed?

I know someone which has a single-page MS Word document for receipts.
One headline includes RECEIPT #<number>. At the moment he looks up which number was on the last printed receipt and adjust the receipt number manually to be that number plus one before he prints out a single copy of the document.
I thought this could be improved by using a form field holding the number which is then increased by one every time the document is printed. I didn't found anything supported by MS Word out-of-the-box, but I think that it can be done using VBA. It's years ago that I had to program in this language and I never did anything with form fields in Word and print events.
Can anyone point me in the right direction with some sample code which can do this? Automatic saving of the document after adjusting the number would be welcome, too.
Word lets one control the DocumentBeforePrint event, which I think would give you the result you need. After a Text Form Field has been added to the document itself, add this code to the ThisDocument VBA Declarations section:
Option Explicit
Private WithEvents app As Application
Then edit the Document_Open() sub to read:
Private Sub Document_Open()
Set app = Application
ActiveDocument.Variables("ReceiptNumber").Value = ActiveDocument.FormFields(1).Result
End Sub
Finally, create the DocumentBeforePrint sub with the following code:
Private Sub app_DocumentBeforePrint(ByVal Doc As Document, Cancel As Boolean)
Dialogs(wdDialogFilePrint).Show
With ActiveDocument
.Variables("ReceiptNumber").Value = .Variables("ReceiptNumber").Value + 1
.FormFields(1).Result = .Variables("ReceiptNumber").Value
.Save
End With
Cancel = True
End Sub
This code will launch the Print dialog box and, after printing, increment the counter and Save the document. The Cancel = True line cancels the standard Print dialog box, so that the code does not try to print the document twice. (One can also increment the counter after printing by removing Dialogs(wdDialogFilePrint).Show and Cancel = True.)
I think it's worth mentioning that once the code is in place, set this process up by double-clicking in the document's form field (the one that will contain the receipt number) and select "Number" for the field type and enter the first receipt number that needs to be printed as the "Default number."
Once the form field's defaults are entered, save the document manually and then close it. Now each time it is opened, the contents of the form field will be assigned to the variable and the DocumentBeforePrint event will increment the field upon each printing of the document. Should the user need to reset the field (due to printer jam or some other unforeseen event), he should double-click the field, change the default value, click OK in the dialog box, save the document, and close it (to clear out the previously assigned value for the counter's variable). As before, opening the document will make it ready for printing and incrementing. Hope this helps.

How do you edit records from a VBA form that you want to interactively select?

I have a set of ComboBox's in an MS Access 2003 DB that are all bound to fields in a single table. However, the data that they allow you to select doesn't come from that table and instead comes from various other tables. This works fine for the record creation story but now I want to be able to edit the record retroactively. The problem is that I can't figure out how to refill the form elements without writing a bunch of custom code.
My initial inclination is to provide a combo box that limits your choices to record IDs and then do a custom query and use that to set the selected values in all of different form elements. However, I feel like I should be able to do something as simple as DoCmd.GoToRecord , , , ID and the form should repopulate just fine. I'm not opposed to doing the busy work but I'm sure I'm just missing something in my relatively puny knowledge of VBA and Access.
Just to add to the mix, I would offer two approaches, one recommended, the other not.
Approach 1: If you've bound your form to the whole data table (this is the non-recommended approach), you can use the combo box wizard to navigate to the requested record, but I wouldn't recommend it in recent versions of Access:
a. it doesn't allow you to properly name the combo box before it creates code.
b. the code is just WRONG.
Here's the code I just produced in my test database:
Dim rs As Object
Set rs = Me.Recordset.Clone
rs.FindFirst "[InventoryID] = " & Str(Nz(Me![Combo2], 0))
If Not rs.EOF Then Me.Bookmark = rs.Bookmark
This is wrong in so many ways it's just remarkable. This is what the code should be:
With Me.RecordsetClone
.FindFirst "[ID]=" & Me!cmbMyComboBox
If Not .NoMatch Then
If Me.Dirty Then Me.Dirty = False
Me.Bookmark = .Bookmark
Else
MsgBox "Not Found!"
End If
End With
There is no need to clone the form's recordset when the RecordsetClone already exists.
There is no reason to use an object variable when you can just directly use the pre-existing object.
There needs to be a check for a dirty record before departing the record because if you don't force the save, errors in the save process can lead to lost data.
But the better approach is this:
Approach 2: Use the combo box to change the form's underlying recordsource.
The AfterUpdate event of your combo box would look something like this:
If Not IsNull(Me!cmbMyComboBox) Then
Me.Recordsource = Me.Recordsource & " WHERE [ID]=" & Me!cmbMyComboBox
End If
Now, this only works the first time, as on the second resetting of the Recordsource, you end up with two WHERE clauses, which is not good. There are two approaches:
a. assuming that the form opens without a WHERE clause, store the opening recordsource value in a module-level variable in the form's OnLoad event:
Private Sub Form_Load()
strRecordsource = Left(Me.Recordsource,Len(Me.Recordsource)-1)
End Sub
And at the module level, define strRecordsource accordingly:
Dim strRecordsource As String
Then in the combo box's AfterUpdate event, you have this instead:
Me.Recordsource = strRecordsource & " WHERE [ID]=" & Me!cmbMyComboBox
Now, if your form opens with a WHERE clause already defined, it gets more complicated, but I'll not go into that and leave it as an exercise to the reader what the best approach might be.
I presume that you've already set up the row sources for each combo box. So long as you haven't limited the combo box to that list; it should display what you have stored in that column.
However, if your Combo Box changes its list for each row you can do something like this in the record's OnCurrent event or the field's GotFocus event:
Me.combo_box_name.Requery
After re-reading your question, I think I see what you are trying to achieve. You're on the right track with GotoRecord, although I would probably use OpenForm in this case, because it has a WhereCondition property that allows you to use SQL to specify exactly what record to open. It sounds like you want to implement a "jump to record" type functionality in your form, where the user selects a record ID from a list and the form changes to display the selected record.
One possibility is to switch to the new record each time the user selects an item in the ComboBox. You can handle this in the ComboBox's Click event.
I'll use a simple example: suppose you have a Students table, and a StudentForm for viewing/editing records in the Students table. The StudentForm has a ComboBox cboStudentID that is bound to the Students.ID column via it's RowSource property. When you select a student ID in the ComboBox, the StudentsForm will switch to display the corresponding student record.
In the Click event handler for the ComboBox, you can code this "jump to record" functionality with something like the following:
Private Sub cboStudentID_Click()
Dim recordID As Long
'The ItemData property will return the value of the bound'
'column at the specified index.'
recordID = cboStudentID.ItemData(cboStudentID.ListIndex)
'Jump to the record. This assumes we want to use the same form.'
'You can change the form name if you want to open a different form when'
'the user selects an ID from the ComboBox.'
DoCmd.OpenForm "StudentForm", WhereCondition:="Student.ID=" & recordID
End Sub
As David W. Fenton points out in the comments, you can shorten the following line:
recordID = cboStudentID.ItemData(cboStudentID.ListIndex)
to this:
recordID = Me!cboStudentID
or just:
recordID = cboStudentID
since the default value of the ComboBox in this case will be the value of the bound column at the current ListIndex. In this case, you could just remove recordID altogether and code the Click event as follows:
Private Sub cboStudentID_Click()
DoCmd.OpenForm "StudentForm", WhereCondition:="Student.ID=" & cboStudentID
End Sub