There are different ways to do audit trails in Access. Up until Access 2010, these had to be implemented on the Form level. But Access 2010 has Data macros which operate on the Table level, so they will capture if someone bypasses your security (see my blog on Login Security using VBA) and makes changes in the table itself.
Setting up such an AuditTrail is a little more cumbersome but does provide a greater level of protection.
The first step is creating a table to hold the Audit Trail information. The following shows the design of this table:
The fields are pretty obvious. This table records the table and field that was changed, the RecordID (PK Value) of the changed record, the old and new value and who changed the record and when.
The next step is to open the table you want to audit in Design Mode. From the Table Design ribbon pull down the list of Data Macro events and select After Update event as shown below:
This will open the Macro Design environment where you can build the macro.
The screen shot below shows the macro commands to cover one field:
The first command is an IF command that is triggered if the field value changes. It uses the Updated function with the field name as the argument. The next step is the Create a Record command which uses the argument of tblAuditTrail. This will be the name of your Audit Trail table.
From there you add a SetField command for each field in your Audit Trail table (except for the autonumber PK, which is filled in automatically). The first field is the Tablename which is hardcoded. The next is the RecordID which is filled in by a reference to the fieldname. Then the ChangeBy field is populated with the ID of the user. In this example I am using the fOSUsername() function found at http://www.mvps.org/access (see Note). Next, I populate the FieldName, which is again hardcoded. Following are the Old and New values. I use the Old command with the fieldname as the property to capture the previous value. Then reference the tablename.fieldname. Finally, I set the ChangeDate to Now().
To place an Audit Trail on other fields, you can collapse the If and then copy and paste to add another set. Change the fieldname information as needed.
Save the macro and close the editor.
You can test by making changes to the fields you have place an audit trail on and then checking tblAuditTrail to make sure the changes were recorded.
You can also capture Deletes similarly using the After Delete event and writing all the fields to a deleted records table. Again using Create A Record and SetField for each field.
I have a sample file (see link below) that contains a table with Audit Trail and the Audit trail table as well as a form to edit the audit trail table and modules with the needed functions. Note: Both Front end and back end need to be in the same folder.
Note: If you use my Login Security techniques or some other way of capturing the current user, you may need to create your own function to replace fOSUsername(). Following is an example:
Public Function fUser()
fUser = Forms!frmLogin!cboUser
End Function
You can assign to fUser whatever you use to capture the current logged in user.
Limitation: This does not work with certain data types. An error 20342 is thrown with description The Updated function is not supported for memo, rich text, hyperlink, OLE Object, multi-value, or attachment fields. If you have one of those data types, then I would recommend using a VBA alternative (see Comments). In the case of multi-value or attachment data types, you can replace then with a child table.
Hello,
I greatly appreciate this walk through and descriptions with the pictures and examples but I have some questions as I am having troubling getting this to work correctly.
Please let me know if you have a second and how I can contact you.
Thank you,
Eric
Feel free to post your questions in a comment or reply. You can also post in the Access forum at AskMeHelpDesk.com. I monitor that forum.
Scott
Great thank you.
I followed the steps above and changed the fields to fit my database and tables. Saved the macro and tried testing it in two ways.
1. Directly in the table it should be monitoring – nothing shows up in the tblAuditTrail
2. From a form which is linked to the table – I receive an error pop-up which states “An argument to the Updated function was invalid. The field name must be provided as a string value enclosed in quotation marks.” nothing shows up in the tblAuditTrail
I only have one large table and am testing it on one field before moving forward.
Check out the picture links below to see if you notice any mistakes.
Error: https://www.dropbox.com/s/bjbzgb4p06v4ls3/Error_Acces1234.png
Macro: https://www.dropbox.com/s/xi532x4ccgygj52/Macro_Access1234.png
(hopefully the links work)
Any ideas?
Thank you,
Eric
Not sure if this is the problem, but in the screenshot it starts off with:
If Updated(“Business Onwer”)
It looks like your table name is Business Owner. That misspelling would cause the macro not to work. It would definitely cause the error message you received. So I suspect that is the problem.
Scott
Eric,
I just noticed that same misspelling in a couple of the clauses. And I meant field name not tablename.
Scott
Scott,
Thank you so much for looking carefully. I should have caught that, I was not paying close enough attention. I works!
Thank you very much. I will be sure to pass this sit and the other site you mentioned along. I was searching for your exact post for a while and you made it extremely easy. I am sure I will be looking at your other stuff.
Eric
Hi Scott,
Thanks for this tutorial! It works perfect.
You wrote:
You can also capture Deletes similarly using the After Delete event and writing all the fields to a deleted records table. Again using Create A Record and SetField for each field.
I am trying to get the delete function working but it doesn’t.
Could you please help me with the macro code by giving an example of what should be in there like you did with the update function?
I made the backup database (should it all be text fields or exact the same as the original database?)
Hope to hear from you.
Greetings from the Netherlands,
Stephan
The only difference should be the event you tie the code to. The other difference is you need to capture every field.
Scott
Thanks for your quick response!
But do you mean that in the after delete macro I have to put exactly the same code? By which I mean:
Create a record in tblBackup
If updated(“Id”) Then
Setfield
Name tblBackup.RecordId
Value = [tblFiles].[Id]
etc for every field?
Do the fields in the Backup database (tblBackup) have to be of the exact same type as the original database (tblFiles)
Stephan
I am having trouble with the code, I am getting the following error: “Type mismatch on field “tblAudit.RecordID”.
tblAudit.RecordID is a number – long integer
the data I am trying to send into tblAudit.RecordID is an Autonumber – long integer.
I am using this macro to track changes to one field in a table, the tblAudit.RecordID is filled with the Primary Key field of tblDocuments.
Can this be solved? Thanks.
Hi Nina,
Ok, so you set up the Audit trail table with a field named RecordID that is set for Number Long Integer. And you are trying to populate that field with an AutoNumber field from the record being modified? And you have a Setfield Action in your macro that assigns to tblAudit.RecordID the PK from the table being modified?
That should work. Are you sure that you have the datatypes correct?
Scott
Thanks for the instructions.
For the most part, it seems to work very well. However, I did run into a limitation it seems, I am receiving error 20342 with description (The Updated function is not supported for memo, rich text, hyperlink, OLE Object, multi-value, or attachment fields.) Is there anyway around it? I want to still keep track of memo fields so I can see all the changes in one table. Thanks!
First, I want to thank you for reporting that. I will revise the blog to mention that. But if its not supported I can see no way around it, using data macros. However, if your app can use VBA, you can use a VBA based Audit Trail. Here’s the code I use:
Public Sub AuditTrail(strTable As String, strField As String, lngRecord As Long, varOld As Variant, varNew As Variant)
Dim strSql As String, strUser As String
strUser = DLookup(“[EmpID]”, “tblParameters”)
strSql = “INSERT INTO tblAuditTrail (Tablename, Fieldname, RecordID, OldValue, NewValue, ChangeDate, ChangeBy) ” & _
“VALUES(‘” & strTable & “‘, ‘” & strField & “‘, ” & lngRecord & “, ‘” & varOld & “‘, ‘” & varNew & “‘, #” & _
Now() & “#, ‘” & strUser & “‘);”
CurrentDb.Execute strSql
End Sub
This is called by the following line of code into the Before Update event of each control that you want to audit:
Call AuditTrail(Screen.ActiveForm.RecordSource, Screen.ActiveControl.ControlSource, Me.PrimaryKey, Screen.ActiveControl.OldValue, Screen.ActiveControl)
You only need to change Me.PrimaryKey to reflect the control bound to your Primary Key. That should work with all but multi-value fields and attachment datatypes. If there is a need for an audit trail on such types, then I wouldn’t use them but use a child table instead.
Scott
Scott
Scott,
This solution looks fantastic. Could you give some hints as to how to set up the tblParameters to store the current users name or ID?
Many thanks
Leah
You can check my other blog article on Login Security using VBA for ways to capture the logged in User. Another option is using the function found here: http://access.mvps.org/access/api/api0008.htm. You can then run an UPDATE query to assign the loginID to tblParameters table. Or you can just substitute that function.
Scott
Scott,
I can get the fOSUserName function to work on a form with an unbound text box but cannot figure out how to make this work in a query to append into a table. Any chance you could help out? Many thanks.
Cheers,
Leah
Add a column to your Append query like so:
user: fOSUsername()
Select the field you want to append to.
Scott
Can you explain how i can make this work if i have a memo and attachment field. I don’t need the attachment field to work but i want to make sure it won’t cause problem
I’m not sure it will work on an attachment field. I’ve never tried it. I don’t think it would work on any field that allows multiple values. As far as a memo field. you might have to modify your audit table to include memo fields and then modify the code to append the old and new values to those fields instead.
Scott
i don’t have multiple value field. If someone add an attachment, will it do nothing or will access make an error message ?
I have very little experience with macro/vba and i think an audit trail that doesn’t keep the old/new attachments will be better than no audit trail at all.
Etienne,
First, I just reviewed the blog and I forgot, I included a Limitations section at the end which specifies that it won’t work with certain fields including attachments or memo fields. And that it will throw an error message with those fields.
The way this is designed is you can audit only the fields you want to audit.. So you would add an If block for each field you want to audit.
Scott
Scott,
Thanks for your blog, I am new to Access. I am trying to use fOSUusername, how and where do I create this module?
“Paste the following code in a new module and call the function fOSUserName”
Steve
The code for the function goes in a global module. You can open the Visual Basic Editor (VBE) and add a new global module, or add the code to an existing module.
You can then use the function like you would any built in function.
Scott
Scott, thanks for the tutorial but for some reason I’m not having any luck. For testing I’m just making changes to one field ScrapWeight and tracking the user and date. It never “visibly” logs the changes. My primary key in tblAudit is EditRecordID and if I manually add a record to the tblAudit the new ID will be 22 for example. I then open my ScrapDetail table in a datasheet and make a few changes to the ScrapWeight. They don’t appear in the tblAudit but now if I manually enter another record in tblAudit my next ID is 25. It acts like it is maybe logging the change but deleting it right after. I’m using the AfterUpdate event. Any suggestions?
There should be no visible action after a change is made. Otherwise this was very thoroughly tested. I would need to see what your macro looks like to tell what might be going wrong. If you can’t reproduce maybe you can upload a copy to a shared cloud storage and send me the link.
Scott
Scott, I created a test database. Just two tables, Audit and Test. Added a data macro to log the time and date to Audit table when TestValue in the Test table changes. I never see anything logged. Here is the shared link..
https://drive.google.com/file/d/0Bxk3gaxEn_V5QlIzZkdqc0FGWG8/edit?usp=sharing
I must be missing something.
Scott, I found the problem. I built my first macro using the image you have on your blog. It shows the Create Record before the IF statement. When I looked at your sample database I noticed it. Also I was not putting my field name in quotes and the UsysApplicationLog show me that error but I would have thought Access would have raised an error when I tried to save the macro. Anyway all is working well and I appreciate the help.
Yes, I noticed the field name problem, but I still couldn’t get it to work. I was going to work on it more this weekend before getting back to you.
Are you saying the screenshot is wrong and the Create Record should be within the IF? If so, thank you for catching that and I will update the blog.
Scott
If you compare what you have in the screenshot to the actual macro in the database it looks to be wrong in the screenshot. It should be IF then Create Record.
Thanks again, I’ve changed the blog post to reflect the correct macro.
Scott
Pingback: Track who made last change
Thank you for the comment, but Data Macros do not exist in the MDB format. They are specific to Access 2010 or later. Therefore, this particular blog article is aimed only at that audience.
Scott
P.S. I removed the link as there are any number of audit trail examples out there.
Hi Scott,
Your macro works great on the back end (_be) database with my table. But I can’t make it work for the front end. Are there any changes that I need to make to the modules that you have included in your test db?
Thank you,
Nora
Since a data macro works directly on the table, it won’t work on a linked table. The macro HAS to be in the back end. Why would you want it in the Front end?
Scott
I figured it out. I had to re-link the table in production to the back-end. Thanks again for the excellent tool!
Nora
Ahh now I understand. Yes, if you add a data macro, you should relink.
Scott
The macro is in the back end. I just had trouble logging changes to the audit trail from the front end. But I re-linked the tables and now it’s working. Thanks!!!!!
Scott,
Great example and it works like a charm on the backend side. Thank you for this.
I tried to import your same example with the web front end using Sharepoint 2010 (after taking out the fOSUserName from everything; after update and the module due to web compatiblity issues) where it ends up not working anymore. No error messages, nothing.
Is there a method to relink web tables? I have been resyncing with Sharepoint, but I don’t think it works…
Have you or anyone else tried this with Access Services?
Any assistance would be greatly appreciated.
Ling
Sorry, but I haven’t tried it with a WEB app as yet.
Scott
Hi Scott,
First of all thanks for a great example on how to use macros. Unfortunatley i have run into a little bit of problem and maybe you can help.
I need to keep record of location history for material my database handles.
Every bit of material has a locationID set to it that is a auto generated database number. So when i run your example my OldValue is stored as the number values of my locations and not the names of my locations.
Is there any way to connect my OldValue outputed as a locationID to the name of the location in my locationtable? I have tried to make a relashionship between the locationID in my locationtable and the OldValue field in a query but it still outputs only a number for the location and not the name so some link is missing if its even possible.
Thanks in advance for any advice.
Regards
Per
I think the problem here is that the Datatype for OldValue is a Text datatype and your LocationID is Long Integer (autonumber). Sio what I would do is create a query like:
SELECT RecordID, ChangeDate, ChangeBy, CLong(Oldvalue) as Previous, CLong(NewValue) as New
FROM tblAuditTrail
WHERE Fieldname = “Location”;
This will give you a list of changes in Location. Then create another query using the above and add your Location table twice, joininf Previous to LocationID in one instance and New to LocationID in the other instance. Bring in all the fields you want and add the Location name from each instance and give them aliases of NewValue and PreviousValue. This second query should show the location names.
However, if you need to show a history of locations, I would be more inclined to use a history table, rather than an audit trail.
Scott
Sure and you are on the right track. You don’t need to change anything in the process I outlined. In the query you need to include both the Audit trail table and the location table. You need to add the Location table TWICE. Then create a join between the OldValue and the LocationID and the New Value and the LocationID. Then add the Location name from each instance of the Location table to the query grid. Finally, add aliases to each of those columns. Add OldLocation: in the front of the column linked to the Old Value and NewLocation: for the column linked to the New Value. When you run the query, you should see the location names labeled properly.
Scott
wonderful put up, very informative. I ponder why the opposite experts of this sector
do not notice this. You must proceed your
writing. I am confident, you have a huge readers’ base already!
First, thank you for posting this excellent tutorial. I have it working, and it has been a great introduction to data macros. I do have one problem that I can’t seem to work through. The DB I have built uses a login form with a combo box for the username, that pulls from a User Table. One of the combo box columns is the user password. A small macro compares the user password entry to the combo box column. If the passwords match, we move on to a main nav form, while the login form stays open and gets hidden. I would like to be able to get the current user name from the hidden form, and get that into the audit record as the current user is entering data into various forms. I have tried various ways to get the data into a form, I even managed to create a temporary variable to store that bit of info, but haven’t been able to figure out how to the user name into the Audit Table record. Do you have any ideas on how I can do this?
Did you try changing the macro to:
Value=Tempvars(“struser”)
Or whatever name you gave to the Tempvar?
Scott
Dean, Did you see the Note I added to the blog? Have you gotten that to work for you?
Scott
Scott, thank you for taking the time to reply. This I think is what I wasn’t able to figure out. I spent quite a bit of time searching about for how to handle variables in data macros, and didn’t find anything that answered my question. My DB is on my work machine, so I’ll have to wait until Monday morning to give it a try. As a follow up question, can you recommend any good resources on learning the ins and outs of data macros? Finally, after browsing around your site, I came across your post on login security using VBA, which is exactly how my login is set up, except I used the macro editor to build it. I don’t remember where I found the tutorial a couple years ago, but it looks like it came straight from yours. So I would like to thank you for that as well. I will let you know how your suggestion works out.
Adding Tempvars(“UserName”) to the SetField doesn’t appear to work. In fact, when I add this SetField, it breaks the macro and the entire Create A Record section fails. Using the variable as the default value in a form text box, makes it work, but this kind of defeats the purpose of having the data macro tied directly to the table. Any thoughts?
Hmm, A function works, but a control reference or Tempvar doesn’t work. Doesn’t make sense, but try this:
Add a function in a global module:
Public Function fCheckUser()
fCheckUser = Tempvars(“username”).Value
End Function
Then use =fCheckUser() instead of =fOsUsername()
Scott
Hello Scott. I’ve tried to execute your instructions. However, I cannot seem to make it work. I’m a noob. So where and how exactly do I add a TempVar? What do I put as “username”?
Did you see my latest response to Dean? it appears that Tempvars don’t work in a Data macro. So you can create a public function.
In a Global Module add a function like so:
Public Function fUserName()
fUsername = Forms!frmLogin!cboUser
End Function
or use
Tempvars.Add “Username”, Forms!frmLogin!cboUser
fUsername = Tempvars(“Username”)
then substitute fUsername() for fOSUsername() in the Audit macro.
I just tested this and either should work.
Scott
Hi Scott, I know I might be so annoying right now. Like I said, your code worked, but I’d have to always keep the form hidden or invisible.
I set my login in form to close using this code:
DoCmd.Close acForm, “f_Login”, acSaveNo
Then I tried using TempVars, but Access tells me “Invalid Outside Procedure.” I used this as a global module:
TempVars.Add “Username”, Forms!f_Login!cboUserName
fUserName = TempVars(“Username”)
I just changed my form name and cbo box name. What does that mean?
Another thing, when I try to go back to my Data Macros in t_Department, Access tells my that the values: Now(), fUserName(), and even the Updated(“dDepartmentHead”) are invalid. But they used to work well.
Why are you closing the form? Much of the techniques are predicated on leaving the form open and hidden so you can reference the combobox and its values in other parts of the app.
The reason the Tempvars didn’t work is because you closed the form. If you want to use Tempvars rather than leave the form open, you can, but you have to se the Tempvars BEFORE you close the form.
The only reason Now() might cease to work if there is a missing reference. Open the VBE and check Tools>References to see.
Scott
I see. So, either way, I’d have to keep the login form open, but invisible? Is that it? If so, then what’s the main difference between:
Public Function fUserName()
fUsername = Forms!frmLogin!cboUser
End Function
AND
Tempvars.Add “Username”, Forms!frmLogin!cboUser
fUsername = Tempvars(“Username”)?
When do I use one over the other? And which one do you think is better?
If you are keeping the form open , there is little difference between the two. The advantage of using Tempvars is that they can be used in SQL statements as criteria. If you decide to use Tempvars, then once you set the value of the Tempvar, you can close the form since the Tempvar retains its value until reset.
Scott
Hmm. Okay. I think I’ll just use the other one because it’s easier. I still could not do it using TempVars, and i feel hopeless. Still, thank you so much! 🙂
Hi Scott,
I just found your blog while doing a search for an audit trail using a data macro. Your examples above were extremely helpful! But, of course, I can’t get the fOSUsername() to work. I keep getting an application error that says Error Number -8988 “The function ‘fOSUsername’ is not valid for expressions used in data macros.
Do you have any idea what I could have entered wrong?
Thanks!
Terri
Hi Again!
Well, I had the Module named the same as the function…. I fixed it and now it works PERFECTLY!!!!
Thanks Again for your Tutorial 🙂
Terri
Hi Scott,
first i’m from Germany and therefore sorry for my bad english but however I hope you understand me :-(. I found this post yesterday and it’s quite the thing I’m searching for. The integration in the DB works if i change a recordset directly in the table. But if i change a record over the form level there is no record inside the tblAudit. What do i have to do, that it is also an record in the tblAudit if i do a change over the form level?? I’m using Access 2013.
regards from Germany
Andreas
I just tested my sample using Access 2013 and it works as it should. What is the Recordsource of your form?
Scott
Hi Scott,
everything works fine now. I’ve done a little mistake in the code s its only a Layer 8 Problem named Andreas :-). Sorry for the late response.
Andreas
Hi Scott,
First off all thanks for the tutorial. Everything works perfectly on tables that are not linked. I have a difficulty to set it up correctly on linked tables. Is it possible to sort out this problem?
Set the data macro on the table from the back end. It should work
Scott
Hi Scott, thanks for this very interesting and useful example. I use Access 2010 and I have been able to create a database of my own (Front end and back end) – I just put the macros in the backend database. When editing data in the database from the front-end using a form, it works perfectly. What confuses me a little is that given the macros are on the data table, I don’t understand why a record is not created in the audit table when I change the data table directly without using a form. This doesn’t work from either the front-end or from the back-end.
You might wonder why I am even bothered about this! Well, that is my boss often goes into the data table he wants auditing without using a form.
Any suggestions would me most appreciated.
Thank you, Simon
Hi Simon,
Well first I would say you need to train your boss better ;). But I just tested this on my test version. I went directly into a table from the front end and made a change and it was reflected in the Audit trail table.
Scott
Is there a way to make a simple macro that will monitor the whole table instead of each field?
The After Update event DOES monitor the whole table. Data macro event events are on the table level not the field level. However, if you want to track both the old and new values, you have to add a record for each field. If you just want to record the new values you could use a History table and just append the new record to it with a timestamp and user info.
Scott
Hi again Scott, I tested again and changing the table directly from the front end did indeed create a row in the audit trail table. So that’s great.
Now I am trying to work out an implementation plan – since the back end database is in use about 12 hours a day then I need to do the work to prepare the database using a copy of the back end. So how does this plan sound:
1) Make copy of back end to my computer.
2) Add audit trail table to back end copy.
3) Modify table that needs audit trail to add the macros.
4) Test on back end copy for all columns in table to be audited.
5) Carry out tests in step 4 using form and direct access to table.
Out of hours steps – ensuring no-one is using the database!!
1) Remove all data from table to be audited in back end copy.
2) Remove all test data from audit trail table.
3) Rename live table to become audited in live back end.
4) Copy in new tables from back end copy to live back end.
5) Load live data from renamed table (step 3) into new auditable table.
6) Rename back end database in live environment.
7) Copy back end copy database to live environment and rename to live name.
8) Run tests again to ensure that implementation has been successful.
9) In case of problems, revert live database to the renamed copy (step 6).
Does this sound sensible?
Just needed to write it down to bounce it off someone who knows about Access, since no-one else here does !!
Thanks, Simon
It sounds workable. but I would do it differently. First test to see how long it takes you to add the audit trail macros to the table(s). Then I would just do it directly to the back end (making a safety copy first). Unless you feel it takes too long for the back end to be out of service. I would compare how long it takes to add the macros to how long it takes to copy all the data. I’m just not that comfortable with copying the data.
Scott
thanks for a great bit progamming it works wonders
i have it set up with a little mod to tell me the change type in table “new ,chage or delete”
what i swas swonderign is do you happen to know an easy way to restore a recored from the data that is “deleted ” whether this is via a macro or vba
thansk
in advance
craig
Once a record is deleted from a table it is not recoverable. So a better way is to either flag the record so its doesn’t show in queries. Or move it to an Archive table.
Scott
i have worked out how to recover the deleted table info from the audit table
thank you
my next question is about “memo fields” or long txt fields
how do i do the same for those fields as i am doing for all the other fields in my tables?
i have two particular fields in two tables that i am “concerned ” about
thanks
in advance
craig
If you are going to audit memo fields, then the field in the Audit table that you store the Old and new values will need to be a memo field. Otherwise is should be the same.
Scott
The macro works with back-end tables after re-linking, as mentioned above. HOWEVER, when I save the application as a .accde (smaller executable file, to distribute to the users), I get the same error again (.accdb works fine; .accde doesn’t). I have tried re-linking from the .accde executable file – re-linking is successful, but now get the old error : The function ‘fOSUsername’ is not valid for expressions used in data macros. Thanks much, Scott, if you have any insights into this.
Try using Environ(“Username”) instead of the fOSusername() function.
Scott
Hi Scott,
Everything working for me but it showing me value rather than the field name in FieldName in tblAudit.. please tell me how you put hardcoded..?? thanks!
First, if the ID field is your primary key or an Autonumber, then there is no reason to Audit it as it will never change. Second, without seeing what you actually entered as the macro, it’s hard to say what’s wrong. What I suggest you do is make a screen shot of the macro and the post in the Access forum at http://www.askmehelpdesk.com. You can attach the screen shot there and I can, hopefully, see the problem.
Scott
Hi Scott,
I have 2 tables, tblBusDriver and tblAudit.
in tblBusDriver i have three fields, ID,BusID and DriverID
Now I did everything as you mentioned.
And still not recording new records in tblAudit.. please help!!
I have posted in http://www.askmehelpdesk.com but there is no option to attach my screenshot..
here is a link of my screenshot:
https://drive.google.com/open?id=0B9uKuwzDH_rQMy11Y2RUb3J5MTQ
here is a latest screenshot:
https://drive.google.com/file/d/0B9uKuwzDH_rQMy11Y2RUb3J5MTQ/view?usp=sharing
Hi Scott,
It started working.. I just changed the DataType to Text..
Thank you mate!
Glad its working, but I see a problem in your screenshot. The Fieldname value should be a text string naming the field that was updated. So if you are updating the DriverID field, then the value should be “Driver_ID”
Scott
I, too, am having a problem with the “fOSUserName” function and I don’t think there is anyway around it.
From the Application Log File, I have the following error:
“The function ‘fOSUserName’ is not valid for expressions used in data macros.” (#-8988)
However, here’s the quirky part of it:
This database in question is used by a commercial application that uses ODBC to communicate with the database (the application is flexible enough such that, as long as the necessary tables are in the database, it doesn’t matter whether its an Access, SQL Server, or Oracle database). If I’m working directly within the Access database, it work just fine; no errors and the data gets written to the audit table. However, when I’m editing the same table via the application, I get the above error.
Therefore, I think that the function is only available when the Jet engine is available/active (ie: Access is open/running). If it’s not – which is the case when the commercial application is hitting against the database via ODBC – the function is not available and the “trigger” fails.
Any other alternatives? (this is the same situation for “Environ”)
Interesting analysis and I suspect its correct. The Alternative is to store the user’s ID somewhere during Login and then reference that value in the Data Macro. Tempvars will probably work. Maybe a form reference or DLookup.
Sorry, I don’t have the time now to test it.
Scott
Hi Scott,
Every thing is working fine but can’t do Check or Uncheck fields
how to get it worked with Yes/No check fields that are updated?
eg: In table tblEmployees, if any Employee resigns and i uncheck the field “Active”. so how it will be audited in tblAuditTrail..
Thanks
The macro should store a 0 or -1 in the Old and New value fields of the Audit Table.
Scott
Thanks Scott, It worked..
But the problem is now with the Text field.. When I change an Employee name, it doesn’t audit in audit table where the field datatypes in both the table is Text.. What am I doing wrong..??
Sorry Scott, Skip the above comment,
The Text field is working now..!
Scott,
Can I audit Numbers, text, dates etc. fields in One field of Audit Table? Or I need to create separate fields for all those in Audit table..? Thanks!
I thought I answered this. The Oldvalue and NewValue fields are text fields. Doesn’t matter the datatype of the actual fields, they are stored as text in the Audit table.
Scott
Note that the OldValue and NewValue fields in tblAuditTrail are Text Datatypes. So when you write those values to the table they have to be converted to text. So no, you don’t need separate fields for different data types.
Scott
Scott,
This has been a huge help, I’m pretty new to Access and came across this so thanks. But I’m using this on an Access Web App and it works like a charm, except the portion where the username is pulled. Any insight on this?
You have to pull the user from someplace else. I’m not sure how the user is identified in a Web App.
Scott
Hi Scott, how do I create audit log for search only. Audit log will record date/time, username, search field (unique ID) and search results displayed in the form. It has no option to add, modify or delete in the database.
Regards,
Rick
The blog gives instructions to write the changes to a table. Once in the table you can make a searchable form or just query the table.
Scott
Thanks for your prompt response. Not sure if I explained correctly. This is a read only database. User can only search using primary key and information displayed read only. All I need is to add search results into audit table along with date/time, username. I’m using your sample files so everything else if working in sample files except I need to add search result in the form to audit file.
Thanks a lot!
Rick
I’m not understanding what you mean by “add search results”. Can you explain in greater detail?
If user to enter a ref ID and search delivery details, I wish to record outcome of the search. For example user enter ref ID 1234 into the search field for database to look for delivery date, address of delivery (already in the database), I would like Audit log to record ref ID 1234 entered in the search field and delivery date, address displayed to the user at the time of search.
OK, just add a table; tblPrevSearches. Add the files you want to capture. Create a bound form to that table with a New Search button to save the specs of the search.
I didn’t encounter a problem with the back end in a different folder (re:, your note above) after I copied the basGetUserID module – to get fOSUsername() – to the Front End, so a copy is now in both the Front and Back ends. Perhaps it doesn’t need to be in the back end if all activity is done in the front end…?
A back end contain only tables. Usually nothing else.
Scott
How do I write old value to an audit file regardless of any changes made or not in the table. Currently it record old/new values when trigger modify to existing values but I like to record old values when even no modify taken place. Much appreciated if someone can give coding.
Thanks
I don’t understand your reasoning here. The OldValue property is empty until a change is made. So if no change is made the value is what is in the table.
Scott
Hii Scott am trying to implement the above, just to be sure do i create the user and the level tables in the back end ? if yes so how does the code know the name of the back end table ?
I think you are mixing two of my articles. The Audit Trail doesn’t require user or level tables. Those are used in my Login Security article. And those tables are linked from the front end.
Scott
no its the users level one that i am testing
Again, there is no users level to the audit trail.
Hello, thanks for your audit trail solution here. However, it doesn’t seem to like lookup fields as this triggers off an error in the Application error log. Any suggestions as how I can include these lookups fields in the audit trail?
Thanks
Is this for a web app? If not, then I would NOT recommend using lookup fields on the table level. They are more trouble then they are worth.
What error are you getting and what code are you using for the field?
Scott
No, this is not a web app. Yes I’ve learnt that lookup fields are more trouble than they’re worth! It is coming out with an error saying ‘The identifier ‘[TblStatusTracking]’ could not be found.’ and ‘The updated function is not supported for memo, rich text, hyperlink, OLE object, multi-value or attachment fields.’
Any suggestions?
There is the answer, you cannot use any of those types with a Data Macro. So its not really Lookup fields, but rather the MVF. I’m not a big fan of MVFs. You should be using a Junction Table to model the many to many relationship.
Or you can use a VBA based Audit Trail. There are many examples of those.
Scott
Thanks for sharing your knowledge.
Works great!
Hey Scott. Nice article on creating audit trails. I’m having trouble though. I created the data macro like you instructed on the table to the audit table. But nothin seems to be writing to the audit table after I make a change. Any suggestions on how to alleviate this?
You have to create the data macro for each field that you want to audit. Did you create the Audittrail table in the same file as the table you are auditing?
Scott
I was testing the macro on one field to see if I changed it that it would write to the audit table. And yes, currently my audit table is in the same database as the data table.
The techniques in the blog article were thoroughly tested so I know they work. If you make a change to the field that is covered by the audit and a record is not being created in the Audit table, then something was done wrong. Without seeing what you did its hard to know what.
I created a test DB with the exact same tables and field names as your example in this article. It still doesn’t create a record in the audit trail. My tables were in the same database. I can send you the test database if you’re up for helping more thanks for your help this far.
You can start a thread in the Access forum at askmehelpdesk.com. You can attach the file (Zip it first) to your post there and I will take a look at it.
Scott
Not strictly this thread, but I am trying to create a custom shortcut menu. I’ve tried using the Office library in VBA but unfortunately the msoControlButton object can only reference the menu item by means of its ID (the example I found uses ([Office.CommandBar].Controls.Add(msoControlButton, 15948) – ie, “Print”). I can’t find a full list of the IDs anywhere. Next I tried using macros but haven’t been able to get to grips with it. In addition, I can’t seem to find an option to edit any existing macros. I’d really prefer to go down the VBA route. Does anyone know where I can find the IDs of the command bar controls?
Sue,
I’m not real clear on what you are trying to do, not sure what you mean by a “shortcut menu”. If you are looking to create or edit the Ribbon in Access. I would suggest checking out this site:
http://www.ribboncreator.de/en/
If you need more help I would suggest posting in the forums at utteraccess.com.
Scott
Thanks for the prompt reply, Scott.
I am trying to create a shortcut menu to reference from File/Options/Current database in the section Ribbon and Toolbar Options via the drop-down box headed ‘Shortcut menu bar’. By a ‘shortcut menu’ I mean the little menus that pop up when you right-click on a form. At the moment I can’t find a way of customising them – you can completely disable them or they have to be the default. I have already customised the Ribbon and the main Toolbar. Research on the Microsoft support site suggests the only way to do it is via macros or a VBA function.
Via the official Microsoft site, I have created a couple of shortcut menus in VBA using sample code written by Edwin Blancovitch (Advanced Developers.net). The menus work as I would expect, but don’t give me the commands I want to reference. There is no pointer to where I can find the control IDs and searching Tech support finds nothing. I hoped you might have come across this yourself?
Kind regards
Sue
Hi Scott
I think I may have found the right part of the Microsoft help file to enable me to go ahead with the shortcut menus. If it works, I’ll let you know what happens!
Sue
One of my friends has a tool for creating right click menus. You can find some of what he’s done here: https://www.experts-exchange.com/questions/26656176/Creating-ShortCut-menus-using-VBA.html
I believe he has a more user friendly tool available for sale.
Scott
Hi Scott
This is the way to create a shortcut menu in Access 2016:
First:
1. Create a new macro (eg TestShortcutMenu) 2. On the Design section of the ribbon, make sure the Action catalogue is enabled. This will give you the items you need in a column on the right-hand side of the screen. 3. From the Action catalogue, drag âGroupâ into the macro and name the group (the group name will not show in the finished menu) 4. For each item you need on the shortcut menu:
a. Drag a submacro into the macro group b. Select the action RunMenuCommand c. Select the item you want to add to the menu from the drop-down list
5. Save the group macro.
Next:
6. Create another new macro (eg CreateTestShortcutMenu) 7. From âUser Interface Commandsâ in the Action catalogue, drag Add Menu into the macro 8. From the list of macros, select the macro you created in (1) above. 9. To replace all other shortcut menus in the current database, type the name of the menu created in (6) above in âShortcut Menu Barâ for the current database (File/Options/Current database/Ribbon and Toolbar Options). 10. To replace the shortcut menu on specific forms/reports
a. Design the form/report and display the properties b. On the Other tab, select âYesâ for the item âShortcut menuâ c. Type the name of the menu created in (6) above in âMenu Barâ.
Hope you find this useful!
Kind regards
Sue
I am getting an “Update Function must contain exactly one child element which is an identifier”
Does this work in web apps? If not could you show give me an idea as to where I could find a web app data macro that will store changes?
It should work in WEB Apps since Data macros work in Web Apps. It sounds like you aren’t filling in one of the parameters correctly.
Where can I get the fOSUSERNAME? Have you been able to apply that within a webapp? I would like to grab name for the change log but do not see anywhere to add the code you posted on your site.
There is a link in the article where you can get fOSUsername(). But that is VBA code so won’t work in a WEB App.
If you use this in a WEBApp you have to provide some way of capturing the user. Probably using a Tempvar. You can then reference the tempvar in the macro code.
Do you have a web app macro to copy a record? I have seen a “cloning” macro but seems a bit over complex, so figured I would see if there was anything more simple done.
I don’t do much with AWAs, but you should be able to create a SQL query that copies a record then run that from a macro.
Would you have a good reference for AWA’s? Just can’t figure out a simple copy record function (which should just be a function!!) that can be done via click. I was not even able to add the sql query in the web app. Any help would be MUCH Appreciated!!
I am having trouble getting this to work. Maybe I’m missing something. I set up my 2 tables and set up the different fields on the data macro. However, when I make a change to the referenced cell, no record is made on the audit table. Any trouble shooting questions you can give me would be appreciated.
Can you show me a screen shot of the macro you used?
Scott
I figured out the issue. It’s the fOSUsername() function that I have:
Function fOSUserName() As String
‘ Returns the network login name
Dim lngLen As Long, lngX As Long
Dim strUserName As String
strUserName = String$(254, 0)
lngLen = 255
lngX = apiGetUserName(strUserName, lngLen)
If (lngX > 0) Then
fOSUserName = Left$(strUserName, lngLen – 1)
Else
fOSUserName = vbNullString
End If
End Function
It highlighted ‘apiGetUserName’ and said sub or function not defined.
Yes, the external function doesn’t always work. You can try using Environ(“Username”) instead. Or maybe set a Tempvar.
Scott
Also, make sure you have the apiGetusername function in a global module.
Scott
Thanks, that worked!
Hi Scott
Hope you can read this. I created two MS Access documents. One I created with several forms based on a main data sheet. Another one I created with several forms based on different main data sheets. May I ask how to indicate the form name the users amended by creating SetField in the Macro mode in MS Access. Thank you so much.
Hi David
Try using Screen.ActiveForm.Name That should plug in the name of the form.
Hope that helps,
Scott
Hi Scott
Thanks for your prompt reply. I tried your suggestion and no entry was automatically created. Any other suggestion 🙂
When i press ctrl+s old value and current value become same i.e old value changes to current value without any update in current value.
Pleass help me out
I have made 2 different audit tables, one for my memo fields/long text fields and another audit table for all other fields. When I try to have the data macros create records in multiple tables it only creates a record in the first table that I put in the data macro.
Why would you have 2 tables? But you would need to have 2 Create A Record Actions. Each one with its own If Updated action.
I’m not worrying about the Long text fields anymore, I cant get them to track the changes. Everything else is tracking correctly.
I’m trying to get the username to track with the changes and I can’t get it to work. I tried Module
Public Function fUser()
fUser = Forms!frmLogin!cboUser
End Function
I even tried Make the Value for set field =Forms!frmLogin!cbouser and I couldn’t get it to work. I have my database split with front end and backend. How do I get the USer_ID to get passed to my backend database?
Hi Kerri, Is frmLogin open when you try to run fUser()? It has to be open, but it can be hidden (Visible set to No)
Scott,
I can get all but the fOSUername to work in the table macro. Has something changed in Access 2016 that doesn’t allow me to run a function?
Mike
Actually yes it has. Running a function using data macros is a problem. I believe you can use Environ(“username”) instead.
Okay, thanks for your prompt reply Scott!
Mike
Hi Scott,
I was referring the process (found bit interesting) with regards to Data Macro to track changes made in a form (Access 2016).
The question that I have is would this process/macro be helpful, if followed for a MS Access file which has linked tables from SQL Server.
Thanks,
Rakesh
No it wouldn’t. Data Macros apply only to tables in an ACE database.
Thanks, for such a fast reply…
Could you please be able to advise me how this is possible for the given situation (MS Access file which has linked tables from SQL Server)
Thanks,
Rakesh
I’m very inexperienced with VBA. I have only one memo field (long text) in my database that I want to track thru a form and I don’t know how to convert it to VBA. I have a screenshot of my data macro but don’t see a way to attach it here.
When you say you want to track the memo field, I’m gathering you want to track edits to it, correct?
Set up a table like the one mentioned in my blog article:
tblTrackChange
TrackChangeID (PK Autonumber)
Tablename
Fieldname
EditDate
OldValue (long text)
NewValue (long text)
In the After Update event of the control bound to the memo field use code like
Dim strSQL As String
strSQL = “INSERT INTO tblTrackChange (Tablename, Fieldname, EditDate, OldValue, NewValue) ” & _
“VALUES(‘tablename’, ‘fieldname’, #” & Now() & “#, ‘” & Me.controlname.OldValue & “‘, ‘” & _
Me.Controlname & “‘);”
CurrentDB.Execute strSQL, dbFailOnError
That should track changes to that field.
Thank you. Here is a link to a screenshot of the data macro that obviously doesn’t work: https://cdmcd.co/93p49W
tblAuditTrail
ChangeID (PK autonumber)
SessionID (from tblProposalData)
SubmissionID (from tblProposalData)
SessionNum (from tblProposalData)
SessionTitle (from tblProposalData)
FieldName
OldValue
NewValue
ChangeDate (Now())
ChangeBy (NetworkUserName())
I got it to work. Thank you so much for your help!
Glad to help.
Hi Scott,
Many thanks for this blog post, it’s helped me quite a bit! I am however running into issues when a lookup field is edited. The record is set, but the result is simply the list value from the lookup table rather than the “New Value” or “Old Value”.
I’m guessing the solution is to not use lookup fields, but is there some way to capture a part of the lookup table record other than the list value? I suppose ideally if the old record was 1|pancake|blueberries and the new 4|muffin|raspberries, I would want the old and new value to be “pancake” and “muffin” rather than “1” and “4”.
Sorry for the poor terminology- I hope it makes at least some sense!
Well yes, using Lookup fields is NOT recommended. Lookups should be done on the table level using list controls, not on the table level. . However, that wouldn’t change the issue. The question is are your lookups based on a Value List or a table. If they are a Value List, that’s not a good idea. Value Lists are only good for short, static lists. Instead they should be based on a table. So, you can use a query to return Pancake or Muffin instead of 1 or 4. But 1 or 4 is what is stored in the table. And what will be noted as the New and Old values.
So I think my lack of terminology has led to confusion- not surprise there! Sorry.
My Lookup fields are linked to tables in a separate backend db. It sounds like you’re saying that I can’t return a more descriptive value than the record position within a table (1 or 4 in this example). Is there not some way to store a value from a different field of the same record?
Not while using a data macro. However, your terminology shows a lack of understanding. You refer to “the record position within a table”. That is NOT what is being stored. The ID field value is your Primary Key, not a record position. Every table should have a primary key field that uniquely identifies a record. Sometimes that PK is a specific value sometimes, its a autogenerated value. But the PK is unique and unchanging. So its not a Record position.
As i said, you can use the PK value to join to the lookup table and pull the descriptive value.
Ah, I understand now- at least with regards to the primary key. Thank you for your help, Scott. I will try to find a way to join to the lookup table as you suggest.