diff --git a/frappe/__init__.py b/frappe/__init__.py
index 55813cd4cb..967b851e0e 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -666,7 +666,7 @@ def is_whitelisted(method):
is_guest = session["user"] == "Guest"
if method not in whitelisted or (is_guest and method not in guest_methods):
- summary = _("You are not permitted to access this resource.")
+ summary = _("You are not permitted to access this resource. Login to access")
detail = _("Function {0} is not whitelisted.").format(bold(f"{method.__module__}.{method.__name__}"))
msg = f"{summary}
{detail} "
throw(msg, PermissionError, title=_("Method Not Allowed"))
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 5c550637b6..898fd81011 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -33,6 +33,7 @@
"editable_grid",
"quick_entry",
"grid_page_length",
+ "rows_threshold_for_grid_search",
"cb01",
"track_changes",
"track_seen",
@@ -697,6 +698,14 @@
"fieldname": "protect_attached_files",
"fieldtype": "Check",
"label": "Protect Attached Files"
+ },
+ {
+ "default": "0",
+ "depends_on": "istable",
+ "fieldname": "rows_threshold_for_grid_search",
+ "fieldtype": "Int",
+ "label": "Rows Threshold for Grid Search",
+ "non_negative": 1
}
],
"grid_page_length": 50,
@@ -775,7 +784,7 @@
"link_fieldname": "document_type"
}
],
- "modified": "2025-03-27 18:16:53.286909",
+ "modified": "2025-05-21 21:58:59.947374",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 3939240bfb..77a3864860 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -161,6 +161,7 @@ class DocType(Document):
restrict_to_domain: DF.Link | None
route: DF.Data | None
row_format: DF.Literal["Dynamic", "Compressed"]
+ rows_threshold_for_grid_search: DF.Int
search_fields: DF.Data | None
sender_field: DF.Data | None
sender_name_field: DF.Data | None
diff --git a/frappe/desk/doctype/note/note.py b/frappe/desk/doctype/note/note.py
index 96cf4c0972..8623beceb7 100644
--- a/frappe/desk/doctype/note/note.py
+++ b/frappe/desk/doctype/note/note.py
@@ -71,30 +71,37 @@ def has_permission(doc, user):
def get_unseen_notes():
- from frappe.query_builder.terms import ParameterizedValueWrapper, SubQuery
-
- def _get_unseen_notes():
- note = frappe.qb.DocType("Note")
- nsb = frappe.qb.DocType("Note Seen By").as_("nsb")
-
- return (
- frappe.qb.from_(note)
- .select(note.name, note.title, note.content, note.notify_on_every_login)
- .where(
- (note.notify_on_login == 1)
- & (note.expire_notification_on > frappe.utils.now())
- & (
- ParameterizedValueWrapper(frappe.session.user).notin(
- SubQuery(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name))
- )
- )
- )
- ).run(as_dict=1)
-
return (
frappe.cache.get_value(
f"{UNSEEN_NOTES_KEY}{frappe.session.user}",
- generator=_get_unseen_notes,
)
or []
)
+
+
+@frappe.whitelist()
+def reset_notes():
+ frappe.cache.set_value(f"{UNSEEN_NOTES_KEY}{frappe.session.user}", [])
+ return frappe.cache.get_value(f"{UNSEEN_NOTES_KEY}{frappe.session.user}")
+
+
+def _get_unseen_notes():
+ from frappe.query_builder.terms import ParameterizedValueWrapper, SubQuery
+
+ note = frappe.qb.DocType("Note")
+ nsb = frappe.qb.DocType("Note Seen By").as_("nsb")
+
+ results = (
+ frappe.qb.from_(note)
+ .select(note.name, note.title, note.content, note.notify_on_every_login)
+ .where(
+ (note.notify_on_login == 1)
+ & (note.expire_notification_on > frappe.utils.now())
+ & (
+ ParameterizedValueWrapper(frappe.session.user).notin(
+ SubQuery(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name))
+ )
+ )
+ )
+ ).run(as_dict=1)
+ frappe.cache.set_value(f"{UNSEEN_NOTES_KEY}{frappe.session.user}", results)
diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json
index 1e0eee5c37..39a6f99092 100644
--- a/frappe/desk/doctype/number_card/number_card.json
+++ b/frappe/desk/doctype/number_card/number_card.json
@@ -32,7 +32,9 @@
"dynamic_filters_section",
"dynamic_filters_json",
"section_break_16",
- "color"
+ "color",
+ "column_break_xtre",
+ "background_color"
],
"fields": [
{
@@ -209,6 +211,15 @@
"label": "Currency",
"options": "Currency"
},
+ {
+ "fieldname": "column_break_xtre",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "background_color",
+ "fieldtype": "Color",
+ "label": "Background Color"
+ },
{
"default": "0",
"description": "Check to display the full numeric value (e.g., 1,234,567 instead of 1.2M).",
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index 9ecfd1669f..2fb76958dc 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -23,6 +23,7 @@ class NumberCard(Document):
from frappe.types import DF
aggregate_function_based_on: DF.Literal[None]
+ background_color: DF.Color | None
color: DF.Color | None
currency: DF.Link | None
document_type: DF.Link | None
diff --git a/frappe/hooks.py b/frappe/hooks.py
index e1afc0bc4b..4c1d2397f2 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -88,6 +88,7 @@ on_session_creation = [
"frappe.core.doctype.user.user.notify_admin_access_to_system_manager",
]
+on_login = "frappe.desk.doctype.note.note._get_unseen_notes"
on_logout = "frappe.core.doctype.session_default_settings.session_default_settings.clear_session_defaults"
# PDF
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index a5e2dc07a5..9ebaaf61ac 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -469,11 +469,12 @@ frappe.Application = class Application {
if (frappe.boot.notes.length) {
frappe.boot.notes.forEach(function (note) {
if (!note.seen || note.notify_on_every_login) {
- var d = frappe.msgprint({ message: note.content, title: note.title });
+ var d = new frappe.ui.Dialog({ content: note.content, title: note.title });
d.keep_open = true;
- d.custom_onhide = function () {
+ d.msg_area = $('
').appendTo(d.body);
+ d.msg_area.append(note.content);
+ d.onhide = function () {
note.seen = true;
-
// Mark note as read if the Notify On Every Login flag is not set
if (!note.notify_on_every_login) {
frappe.call({
@@ -482,11 +483,13 @@ frappe.Application = class Application {
note: note.name,
},
});
+ } else {
+ frappe.call({
+ method: "frappe.desk.doctype.note.note.reset_notes",
+ });
}
-
- // next note
- me.show_notes();
};
+ d.show();
}
});
}
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js
index ee739ef891..11fcdaf058 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -852,8 +852,13 @@ export default class GridRow {
show_search_row() {
// show or remove search columns based on grid rows
+ let show_length =
+ this.grid?.meta?.rows_threshold_for_grid_search > 0
+ ? this.grid.meta.rows_threshold_for_grid_search
+ : 20;
this.show_search =
- this.show_search && (this.grid?.data?.length >= 20 || this.grid.filter_applied);
+ this.show_search &&
+ (this.grid?.data?.length >= show_length || this.grid.filter_applied);
!this.show_search && this.wrapper.remove();
return this.show_search;
}
diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js
index 042e437d26..d65315dc29 100644
--- a/frappe/public/js/frappe/widgets/number_card_widget.js
+++ b/frappe/public/js/frappe/widgets/number_card_widget.js
@@ -157,6 +157,8 @@ export default class NumberCardWidget extends Widget {
async render_card() {
this.prepare_actions();
this.set_title();
+ this.card_doc?.background_color &&
+ this.widget.css("background-color", this.card_doc.background_color);
this.set_loading_state();
if (!this.card_doc.type) {
diff --git a/frappe/tests/test_boot.py b/frappe/tests/test_boot.py
index b28cfa6ff1..92191a0283 100644
--- a/frappe/tests/test_boot.py
+++ b/frappe/tests/test_boot.py
@@ -1,6 +1,6 @@
import frappe
from frappe.boot import get_user_pages_or_reports
-from frappe.desk.doctype.note.note import get_unseen_notes, mark_as_seen
+from frappe.desk.doctype.note.note import _get_unseen_notes, get_unseen_notes, mark_as_seen
from frappe.tests import IntegrationTestCase
@@ -20,6 +20,7 @@ class TestBootData(IntegrationTestCase):
note.insert()
frappe.set_user("test@example.com")
+ _get_unseen_notes()
unseen_notes = [d.title for d in get_unseen_notes()]
self.assertListEqual(unseen_notes, ["Test Note"])