Repository: frappe/frappe
Pull request: feat: configurable timeline date format
feat: make timeline date format configurable via settings

This feature introduces a system-wide setting that allows administrators to define the date format displayed in timeline views. It supports better customization, enabling formats like DD-MM-YYYY or MM/DD/YYYY etc.. to match regional preferences.

The timeline component now dynamically reflects the configured date format across the system, improving consistency and user experience.

Closes: #32498

  
![image](https://github.com/user-attachments/assets/03726765-a7fe-4284-8ba0-d1bd536a1363)

`no-docs`


Problem statement:
Configurable Timeline Date Format
Allow users to choose how dates are displayed in timelines — either as relative time (e.g., "2 weeks ago") or exact date/time (e.g., "12 Apr 2025, 3:45 PM").
Users should be able to select their preferred format in settings.

Hints from issue discussion:
is anyone working on this

Commit message: feat: Add user-configurable setting with system fallback

Patch to review:
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 6c38d7f302a7..282549abecd4 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -25,6 +25,7 @@
   "float_precision",
   "currency_precision",
   "rounding_method",
+  "show_absolute_datetime_in_timeline",
   "permissions",
   "apply_strict_user_permissions",
   "column_break_21",
@@ -707,12 +708,18 @@
    "fieldname": "max_report_rows",
    "fieldtype": "Int",
    "label": "Max Report Rows"
+  },
+  {
+   "default": "0",
+   "fieldname": "show_absolute_datetime_in_timeline",
+   "fieldtype": "Check",
+   "label": "Show Absolute Datetime in Timeline"
   }
  ],
  "icon": "fa fa-cog",
  "issingle": 1,
  "links": [],
- "modified": "2025-05-19 14:17:40.748786",
+ "modified": "2025-06-05 20:54:53.538436",
  "modified_by": "Administrator",
  "module": "Core",
  "name": "System Settings",
diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py
index 5caf06536ca5..2b07fe6bc6d5 100644
--- a/frappe/core/doctype/system_settings/system_settings.py
+++ b/frappe/core/doctype/system_settings/system_settings.py
@@ -93,6 +93,7 @@ class SystemSettings(Document):
 		rounding_method: DF.Literal["Banker's Rounding (legacy)", "Banker's Rounding", "Commercial Rounding"]
 		session_expiry: DF.Data | None
 		setup_complete: DF.Check
+		show_absolute_datetime_in_timeline: DF.Check
 		store_attached_pdf_document: DF.Check
 		strip_exif_metadata_from_uploaded_images: DF.Check
 		time_format: DF.Literal["HH:mm:ss", "HH:mm"]
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index a673f6226c86..dc9d2bc6a5cf 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -61,6 +61,7 @@
   "form_sidebar",
   "timeline",
   "dashboard",
+  "show_absolute_datetime_in_timeline",
   "change_password",
   "new_password",
   "logout_all_sessions",
@@ -827,6 +828,12 @@
    "fieldname": "dashboard",
    "fieldtype": "Check",
    "label": "Dashboard"
+  },
+  {
+   "default": "0",
+   "fieldname": "show_absolute_datetime_in_timeline",
+   "fieldtype": "Check",
+   "label": "Show Absolute Datetime in Timeline"
   }
  ],
  "icon": "fa fa-user",
@@ -885,7 +892,7 @@
   }
  ],
  "make_attachments_public": 1,
- "modified": "2025-03-17 11:29:39.254304",
+ "modified": "2025-06-05 23:55:27.884061",
  "modified_by": "Administrator",
  "module": "Core",
  "name": "User",
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 04d7daf970c0..9112bf7d2eab 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -126,6 +126,7 @@ class User(Document):
 		search_bar: DF.Check
 		send_me_a_copy: DF.Check
 		send_welcome_email: DF.Check
+		show_absolute_datetime_in_timeline: DF.Check
 		simultaneous_sessions: DF.Int
 		social_logins: DF.Table[UserSocialLogin]
 		thread_notify: DF.Check
diff --git a/frappe/public/js/frappe/form/footer/base_timeline.js b/frappe/public/js/frappe/form/footer/base_timeline.js
index b28c2c7706f0..e0194ac27dd9 100644
--- a/frappe/public/js/frappe/form/footer/base_timeline.js
+++ b/frappe/public/js/frappe/form/footer/base_timeline.js
@@ -138,7 +138,14 @@ class BaseTimeline {
 		let timeline_content = timeline_item.find(".timeline-content");
 		timeline_content.append(item.content);
 		if (!item.hide_timestamp && !item.is_card) {
-			timeline_content.append(`<span> · ${comment_when(item.creation)}</span>`);
+			timeline_content.append(
+				`<span> · ${
+					frappe.boot.user.show_absolute_datetime_in_timeline === 1 ||
+					frappe.boot.sysdefaults.show_absolute_datetime_in_timeline === "1"
+						? frappe.datetime.str_to_user(item.creation)
+						: comment_when(item.creation)
+				}</span>`
+			);
 		}
 		if (item.id) {
 			timeline_content.attr("id", item.id);
diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js
index 790291702697..cd62558f38a2 100644
--- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js
+++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js
@@ -95,8 +95,10 @@ frappe.ui.form.Sidebar = class {
 				__("{0} created this", [get_user_link(this.frm.doc.owner)])
 			) +
 			" · " +
-			comment_when(this.frm.doc.creation);
-
+			(frappe.boot.user.show_absolute_datetime_in_timeline === 1 ||
+			frappe.boot.sysdefaults.show_absolute_datetime_in_timeline === "1"
+				? frappe.datetime.str_to_user(this.frm.doc.creation)
+				: comment_when(this.frm.doc.creation));
 		let modified_message =
 			get_user_message(
 				this.frm.doc.modified_by,
@@ -104,7 +106,10 @@ frappe.ui.form.Sidebar = class {
 				__("{0} last edited this", [get_user_link(this.frm.doc.modified_by)])
 			) +
 			" · " +
-			comment_when(this.frm.doc.modified);
+			(frappe.boot.user.show_absolute_datetime_in_timeline === 1 ||
+			frappe.boot.sysdefaults.show_absolute_datetime_in_timeline === "1"
+				? frappe.datetime.str_to_user(this.frm.doc.modified)
+				: comment_when(this.frm.doc.modified));
 
 		if (user_list.length === 1) {
 			// same user created and edited
diff --git a/frappe/utils/user.py b/frappe/utils/user.py
index db7de785c9ba..680a1d786f07 100644
--- a/frappe/utils/user.py
+++ b/frappe/utils/user.py
@@ -224,6 +224,7 @@ def load_user(self):
 				"language",
 				"last_name",
 				"mute_sounds",
+				"show_absolute_datetime_in_timeline",
 				"send_me_a_copy",
 				"user_type",
 				"onboarding_status",


Code review comments:
diff_hunk: @@ -138,7 +138,14 @@ class BaseTimeline {
 		let timeline_content = timeline_item.find(".timeline-content");
 		timeline_content.append(item.content);
 		if (!item.hide_timestamp && !item.is_card) {
-			timeline_content.append(`<span> · ${comment_when(item.creation)}</span>`);
+			timeline_content.append(
+				`<span> · ${
+					frappe.boot.user.show_absolute_datetime_in_timeline === 1 ||
+					frappe.boot.sysdefaults.show_absolute_datetime_in_timeline === "1"
path: frappe/public/js/frappe/form/footer/base_timeline.js
text: @user1:
Thank you for making the changes. Could you please explain why the value is being treated as a string in the system setting? Also, can it be treated as an integer instead?

@author:
when i fetched from the sysdefaults it returns a string ....

but when i am fetching from user it returns and int

@user1:
Can you change this === to ==? It may fail, as both are checkbox fields, so I think strict checking isn't required here.