Dynamics GP – Identify who deleted a batch using the SQL transaction log

Introduction

On occasion a batch can be accidentally deleted from the system, and for one reason or another it would be interesting to find out which user did this. In this post I aim to show how this might be possible using some SQL functions to query the SQL transaction log. (I appreciate there are options available to prevent users deleting batches however I’m not covering this)

Pre-requisite and Considerations

One pre-requisite is that the SQL database must be in the FULL recovery model. This won’t work in the SIMPLE model as the transaction log information is lost at each checkpoint.

Another consideration you must make is that the process of posting a batch in Dynamics GP effectively deletes the batch header from the SY00500 table as well. Therefore, the result set from the query could include both genuine deletes, caused by posting batches, and deletes that have happened accidentally. However if you know a little detail about the batch that you wish to track like the type of batch that was deleted, and approximately the number of transactions in the batch, I’m hoping the result you get back from this query will assist you in making an educated guess as to if this is actually the deleted batch you were looking for.

The Theory

In SQL every INSERT\UPDATE\DELETE operation is first logged in the transaction log before being written to the database. Therefore, theoretically using a SQL function you can query the transaction log looking for DELETEs to tables and objects.

I mentioned earlier that the database must be in the FULL recovery model. To expand on this if the database is in the SIMPLE recovery model then after the INSERT\UPDATE\DELETE transaction commits, and a checkpoint occurs, that transaction log information is lost. Therefore you won’t be able to query the transaction log for deletes when the database is in SIMPLE recovery model.

However when the database is in the FULL recovery model, the log information isn’t lost until a LOG backup is performed. Therefore using a SQL function you can query the log looking for deletes.

Finally, all is not lost if you have performed a log backup since the delete occurred as you can use another SQL function to query the transaction log backup file that resides on disk. You just need to know the file name of the backup and its location on disk.

Use FN_DBLOG to query the transaction log

To demonstrate the process, I first created the following batch in Dynamics GP which contains one transaction.

I then deleted the batch from Dynamics GP and using the query below I can identify SQL transactions that include a delete on the batch headers table. (SY00500). Please note this needs running the company database.

SELECT [transaction name], 
       users.NAME, 
       [transaction id], 
       operation, 
       allocunitname, 
       [begin time], 
       [end time], 
       [transaction name] 
FROM   Fn_dblog(NULL, NULL) trnlog 
       LEFT JOIN sys.sysusers users 
              ON trnlog.[transaction sid] = users.sid 
WHERE  [transaction id] IN (SELECT [transaction id] 
                            FROM   Fn_dblog(NULL, NULL) 
                            WHERE  operation IN ( 'LOP_DELETE_ROWS' ) 
                                   AND allocunitname LIKE '%sy00500%')   

This gives the following results

As you can from the results above there’s a SQL Transaction that includes a deletion to the batch header table (SY00500) and associated objects (i.e. the indexes). This is SQL Transaction ID 0000-00045d05. As part of the same transaction you can see deletes to GL tables and objects therefore its clear this is a GL batch that was deleted. You can also see the user who deleted the batch in the “name” column.

Use FN_DUMP_DBLOG to query the transaction log

If a transaction log backup has occurred since the batch was deleted, you won’t be able to use the query above as the log will have been cleared. However you can query the SQL transaction log backup file directly using a different SQL function called FN_DUMP_DBLOG.

To test this I performed a transaction log backup and gave it a file name of C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\Backup\TWO18_3.trn and used the query below to look for the same delete I performed earlier. (please note in this query I had to change a few things as the object name didn’t work without joining to another system table)

SELECT [transaction name], 
       users.NAME, 
       [transaction id], 
       operation, 
       Object_name(part.object_id) AS ObjectName, 
       [begin time], 
       [end time], 
       [transaction name] 
FROM   Fn_dump_dblog (NULL, NULL, N'DISK', 1, 
N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\Backup\TWO18_3.trn' 
, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, 
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT) trnlog 
LEFT JOIN sys.sysusers users 
       ON trnlog.[transaction sid] = users.sid 
LEFT JOIN sys.partitions part 
       ON trnlog.partitionid = part.partition_id 
WHERE  [transaction id] IN (SELECT [transaction id] 
                            FROM   Fn_dump_dblog (NULL, NULL, N'DISK', 1, 
N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\Backup\TWO18_3.trn' 
, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, 
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT) trnlog 
LEFT JOIN sys.partitions part 
       ON trnlog.partitionid = part.partition_id 
 WHERE  operation IN ( 'LOP_DELETE_ROWS' ) 
        AND Object_name(part.object_id) = 'sy00500')   

This gave me the output below which is the same as the original output. Its just the data resides in the transaction log backup file now. (i.e. its the same SQL Transaction ID)

If you don’t find the delete you can just alter the backup file name and keep searching through your backups.

Conclusion

Therefore although not 100% ideal this is one method that can assist in identifying a user that has deleted a batch in Dynamics GP.

You can also use the same method to find out which user deleted other transactions you just need to alter the object name.

I’d also like to note that although I have tweaked the SQL scripts I’ve used in this post myself, I most certainly had to stand on the shoulders of several SQL giants along the way. I’m sure everyone is aware the SQL community is also awesome at sharing information to help others like me!

Thanks for reading

Dynamics GP – Clear Activity in Bank Management – Delete from CBEU1020

I recently had an issue where a user was stuck in a batch in the Bank Management module. You can usually run the “Clear Activity” option to resolve the issue however on this occasion it didn’t work. I found I had to manually delete a row in an SQL table to clear the lock and allow the user access to the batch.

The exact issue they received is as follows:

With this error message the first thing to try is to clear activity and although this didn’t work on this occasion I’ll detail the steps below.

Select “Tools > Routines > Financial > Bank Management > Clear Activity”

Select “Clear Activity”

Ensure there are no users in Dynamics GP and click “Yes” to the message below. (rather than asking everyone to log out I usually just ensure there’s nobody doing any Bank Management activities)

After doing this you will be prompted with the message below confirming activity is cleared

However I found I still couldn’t access the batch. On investigation there is still some activity for the batch in the CBEU1020 table which needed clearing. I therefore ran the SQL query below in the company database to delete the row. (replace TWO18 with the name of your company database)

I could then access the batch.

Thanks for reading

Dynamics GP – Letter Writing Assistant – Letter Templates missing following upgrade to Dynamics GP 2018 R2

Following a recent client upgrade of Dynamics GP to version 2018 R2 I found that when selecting “Write Letters > Print Debtors” the window was blank:

I opened the C:\Program files (x86)\Microsoft Dynamics\GP2018R2\Data\DEX.INI file to find the Letters Directory location and when I checked each sub folder in this shared folder the word documents (.DOC files) were present.

On investigation it seems previous versions of Dynamics GP used to expect files in the DOC format however its now expecting them to be in the DOCX format. Therefore, I opened each one and saved them as DOCX files and they are now available.

I hope this helps anyone else who may encounter this issue.

Thanks for reading