Enhancing Transparency in Microsoft Dynamics 365 Business Central: creators and modifiers of records.
- Russell Kallman
- Apr 21
- 4 min read
In today's fast-paced business world, transparency and accountability are vital for smooth operations. Back in 2020 Wave 2, Microsoft helpfully added four news fields to track who created and modified a record in the database. As always Zhu did a good job of running through the change.
Unfortunately, as is too often the case, Microsoft left the job undone in many respects. The feature is not integrated into the existing change logs and isn't shown to users by default on any pages.
The default answer on the forums is to use the Page Inspector as seen in this video by All My Systems.
At least in our business that is both too technically challenging and confusing. Good maybe for administrators, but few others.
Then there are the appsource apps. There is Created-by on Documents which adds specific fields, but more has the purpose of tracking on posted documents who created the original. Also Change Log Quick Access and a few others in that vein.
Key requirements for our solution
We wanted the information to appear consistently always in the same place
We wanted the information to take up as little room as possible
We wanted the information to be digestible by a regular non-power user
We wanted the core code to be re-usable and to drop-in and be self-aware of its own context.
Leveraging Built-in Features in Business Central
Microsoft Dynamics 365 Business Central comes equipped with several features to effectively help us achieve this even without object orientation, multiple inherentence and interfaces. In no particular order we used:
Subpage with subpage links similar to the Doc. Attach List Factbox
Temporary table given us fields to set filters on but without loading any data
Similar to API pattern we analyse filters and use them to load specific data
Recordref and Fieldref to get consistent data regardless of the record

The new CardPart page User and Change Info
This is the key page which does all the work, with one support codeunit that performs a few helper functions used elsewhere in our code.
The supporting codeunit:
Displays a change log specific to the record being viewed
Translated text to proper case for readibility of user names (less shouting)
A few key innovations:
Rather than displaying four fields in a grid or group, often that contain duplicate information or provide confusing information, we interpret the data for the user figuring our how many people are involved and compensate for the fact that Business Central as some atypical meanings of modified/created.
We take the filters and use them to interpret context, so this page itself does not need to be aware of its context at all.
using System.Diagnostics;
using System.Security.User;
using System.Security.AccessControl;
page 50006 "User and Change Info"
{
PageType = CardPart;
ApplicationArea = All;
UsageCategory = Administration;
SourceTable = "Change Log Entry";
SourceTableTemporary = true;
layout
{
area(Content)
{
group(CreatedByGroup)
{
ShowCaption = false;
field(CreatedBy; GetShortenedUserChangeHistory())
{
Caption = 'User Details';
ToolTip = 'Shows key information on what was changed and when';
ShowCaption = false;
}
}
}
}
actions
{
area(Processing)
{
action(History)
{
ApplicationArea = All;
Image = History;
Caption = 'Change Log Entries';
ToolTip = 'Shows entries in the change log if they exist. Specific logging needs to be turned on for fields';
Enabled = ChangeLogExists;
trigger OnAction()
var
begin
CommonCU.ShowValueHistory(ReferenceRecRef.RecordId);
end;
}
}
}
trigger OnFindRecord(Which: Text): Boolean
var
ReferenceRecordId: RecordId;
TableNoFilter: Text;
ReferenceTableNo: Integer;
ReferenceSystemId: Guid;
RecordIDFilter: Text;
SystemIdFilter: Text;
begin
Rec.FilterGroup(4);
RecordIDFilter := Rec.GetFilter("Record ID");
SystemIdFilter := Rec.GetFilter("Changed Record SystemId");
TableNoFilter := Rec.GetFilter("Table No.");
if (SystemIdFilter = '') or (TableNoFilter = '') then exit; //no valid filters
Evaluate(ReferenceTableNo, TableNoFilter);
Evaluate(ReferenceRecordId, RecordIDFilter);
Evaluate(ReferenceSystemId, SystemIdFilter);
//ReferenceRecRef := ReferenceRecordId.GetRecord();
if ReferenceRecRef.Number = 0 then
ReferenceRecRef.Open(ReferenceTableNo);
ReferenceRecRef.GetBySystemId(ReferenceSystemId);
ChangeLogExists := CommonCU.ValueHistoryExists(ReferenceRecRef.RecordId);
end;
var
ReferenceRecRef: RecordRef;
local procedure GetUserNameFromSecurityId(UserSecurityID: Guid): Code[80]
var
User: Record User;
begin
User.SetLoadFields("Full Name");
User.Get(UserSecurityID);
exit(user."Full Name");
end;
local procedure GetCreatedUserNameFromRecRef(): Text[80]
var
FieldRef: FieldRef;
begin
FieldRef := ReferenceRecRef.Field(ReferenceRecRef.SystemCreatedByNo);
exit(CommonCU.ConvertToProperCase(GetUserNameFromSecurityId(FieldRef.Value)));
end;
local procedure GetModifiedUserNameFromRecRef(): Text[80]
var
FieldRef: FieldRef;
begin
FieldRef := ReferenceRecRef.Field(ReferenceRecRef.SystemModifiedByNo);
exit(CommonCU.ConvertToProperCase(GetUserNameFromSecurityId(FieldRef.Value)));
end;
local procedure GetCreatedOnDateTime(): DateTime
var
FieldRef: FieldRef;
begin
FieldRef := ReferenceRecRef.Field(ReferenceRecRef.SystemCreatedAtNo);
exit(FieldRef.Value);
end;
local procedure GetModifiedOnDateTime(): DateTime
var
FieldRef: FieldRef;
begin
FieldRef := ReferenceRecRef.Field(ReferenceRecRef.SystemModifiedAtNo);
exit(FieldRef.Value);
end;
local procedure GetShortenedUserChangeHistory(): Text[250]
var
CreatedUser: Text[80];
ModifiedUser: Text[80];
Duration: Duration;
GapInMinutes: Decimal;
SameUser: Boolean;
SameTime: Boolean;
begin
CreatedUser := GetCreatedUserNameFromRecRef();
ModifiedUser := GetModifiedUserNameFromRecRef();
if CreatedUser = ModifiedUser then SameUser := true;
Duration := GetModifiedOnDateTime() - GetCreatedOnDateTime();
GapInMinutes := Duration / 60000;
if GapInMinutes < 10 then SameTime := true;
if SameUser then
if SameTime then
exit(StrSubstNo('Created by %1 on %2 and not yet modified', CreatedUser, GetCreatedOnDateTime()))
else // implied different time
exit(StrSubstNo('Created by %1 on %2 and last modified on %3', CreatedUser, GetCreatedOnDateTime(), GetModifiedOnDateTime()))
else // implied different user
if SameTime then
exit(StrSubstNo('Created by %1 on %2 and quickly modified by %3', CreatedUser, GetCreatedOnDateTime(), ModifiedUser))
else //timplied different time and user
exit(StrSubstNo('Created by %1 on %2 and last modified by %3 on %4', CreatedUser, GetCreatedOnDateTime(), ModifiedUser, GetModifiedOnDateTime()));
end;
var
CommonCU: CodeUnit "TFB Common Library";
ChangeLogExists: Boolean;
}
All that is needed to add this information is on one of your own pages to add the part and the end of the content section, or add it in an AddLast section to a page extension. The only change required on these pages is to change the Constant for the Table No. field being passed. We haven't found a way to pass that dynamically yet (open to ideas).
part(UserAndChange; "User and Change Info")
{
ApplicationArea = All;
SubPageLink = "Table No." = Const(Database::"Non-Conformance Report"), "Changed Record SystemId" = field(SystemId);
}
Boosting Trust Through Transparency
We often have a question around who was the last user to change a record, or a question of when a record was last updated. The lack of inbuilt functionality within business central to facilitate that answer quickly is a disappointment.
Comentarios