From 9699bca09592ff634b4718acb26db86a49f2c0f7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 20 Aug 2025 19:00:45 +0530 Subject: [PATCH 01/30] feat: active app telemetry --- frappe/api/__init__.py | 9 +++- frappe/hooks.py | 5 ++ frappe/pulse/__init__.py | 0 frappe/pulse/app_activity_event.py | 82 ++++++++++++++++++++++++++++++ frappe/pulse/client.py | 68 +++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 frappe/pulse/__init__.py create mode 100644 frappe/pulse/app_activity_event.py create mode 100644 frappe/pulse/client.py diff --git a/frappe/api/__init__.py b/frappe/api/__init__.py index a8736db67e..32a4257c11 100644 --- a/frappe/api/__init__.py +++ b/frappe/api/__init__.py @@ -1,5 +1,6 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +from contextlib import suppress from enum import Enum from werkzeug.exceptions import NotFound @@ -9,6 +10,7 @@ from werkzeug.wrappers import Request, Response import frappe import frappe.client from frappe import _ +from frappe.pulse.app_activity_event import log_app_activity from frappe.utils.response import build_response @@ -63,7 +65,12 @@ def handle(request: Request): if data is not None: frappe.response["data"] = data - return build_response("json") + data = build_response("json") + + with suppress(Exception): + log_app_activity(arguments) + + return data # Merge all API version routing rules diff --git a/frappe/hooks.py b/frappe/hooks.py index 8d16696f41..b5fd13d131 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -214,6 +214,11 @@ scheduler_events = { "0/10 * * * *": [ "frappe.email.doctype.email_account.email_account.pull", ], + # 6 hours + "0 */6 * * *": [ + "frappe.pulse.app_activity_event.send", + ], + # Hourly but offset by 30 minutes "30 * * * *": [], # Daily but offset by 45 minutes diff --git a/frappe/pulse/__init__.py b/frappe/pulse/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/pulse/app_activity_event.py b/frappe/pulse/app_activity_event.py new file mode 100644 index 0000000000..d24d7934ea --- /dev/null +++ b/frappe/pulse/app_activity_event.py @@ -0,0 +1,82 @@ +import frappe +from frappe.modules import get_doctype_module +from frappe.utils.caching import site_cache + +from .client import is_enabled, post_events + +KEY = "pulse:active_apps" +EXPIRY = 60 * 60 * 12 # 12 hours + + +def log_app_activity(args): + if not is_enabled() or frappe.session.user in ("Guest", "Administrator"): + return + + status_code = frappe.response.http_status_code or 0 + if status_code and not (200 <= status_code < 300): + return + + method = args.get("method") or frappe.form_dict.get("method") + doctype = args.get("doctype") or frappe.form_dict.get("doctype") + + if not method and not doctype: + return + + app_name = None + if method and "." in method and not method.startswith("frappe."): + app_name = method.split(".", 1)[0] + + if not app_name and doctype: + module = get_doctype_module(doctype) + app_name = app_module_map().get(module) + + if app_name and app_name != "frappe": + _mark_active(app_name) + + +def send(): + if not is_enabled(): + return + + active_apps = frappe.cache.get_value(KEY) or set() + if not active_apps: + return + + events = [] + for app in active_apps: + events.append( + { + "name": "app_activity", + "app": app, + "app_version": _get_app_version(app), + } + ) + + try: + if post_events(events): + frappe.cache.delete_value(KEY) + except Exception: + frappe.log_error(title="Failed to send app activity events") + + +def _mark_active(app): + active_apps = frappe.cache.get_value(KEY) or set() + if app not in active_apps: + active_apps.add(app) + frappe.cache.set_value(KEY, active_apps) + ttl = frappe.cache.ttl(KEY) + if ttl in (-1, None): + frappe.cache.expire(KEY, EXPIRY) + + +def _get_app_version(app_name: str) -> str: + try: + return frappe.get_attr(app_name + ".__version__") + except Exception: + return "0.0.1" + + +@site_cache() +def app_module_map(): + defs = frappe.get_all("Module Def", fields=["name", "app_name"]) + return {d.name: d.app_name for d in defs} diff --git a/frappe/pulse/client.py b/frappe/pulse/client.py new file mode 100644 index 0000000000..343a95e100 --- /dev/null +++ b/frappe/pulse/client.py @@ -0,0 +1,68 @@ +from datetime import datetime, timezone + +import frappe +from frappe.utils import get_request_session +from frappe.utils.caching import site_cache +from frappe.utils.frappecloud import on_frappecloud + + +@site_cache() +def is_enabled() -> bool: + return ( + not frappe.conf.get("developer_mode", 0) + and not frappe.conf.get("pulse_disabled", 0) + and frappe.conf.get("pulse_api_key") + and on_frappecloud() + and frappe.get_system_settings("enable_telemetry") + ) + + +def post_events(events): + events = _sanitize_events(events) + session = _create_session() + resp = session.post(_get_ingest_url(), data=events, timeout=5.0) + return 200 <= resp.status_code < 300 + + +def _create_session(): + api_key = frappe.conf.get("pulse_api_key") + session = get_request_session() + if api_key: + session.headers.update({"Authorization": f"Bearer {api_key}"}) + return session + + +def _get_ingest_url(): + host = frappe.conf.get("pulse_host") or "https://pulse.m.frappe.cloud" + if not host.startswith("http"): + host = "https://" + host + host = host.rstrip("/") + + endpoint = frappe.conf.get("pulse_ingest_endpoint") or "/api/method/pulse.api.ingest" + endpoint = endpoint.lstrip("/") + + return f"{host}/{endpoint}" + + +def _sanitize_events(events): + _events = [] + if not isinstance(events, list): + _events = [events] + + for event in events: + if not isinstance(event, dict) or "name" not in event: + continue + event["site"] = event["site"] or frappe.local.site + event["timestamp"] = event["timestamp"] or _utc_iso() + event["frappe_version"] = event["frappe_version"] or _get_frappe_version() + _events.append(event) + + return _events + + +def _get_frappe_version() -> str: + return getattr(frappe, "__version__", "unknown") + + +def _utc_iso() -> str: + return datetime.now(timezone.utc).isoformat(timespec="seconds") From cf884f2b7b55c4e5a943520828ed0f9baf0b6a40 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 22 Aug 2025 19:04:55 +0530 Subject: [PATCH 02/30] chore: fix formatting --- frappe/hooks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/hooks.py b/frappe/hooks.py index b5fd13d131..347b855591 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -218,7 +218,6 @@ scheduler_events = { "0 */6 * * *": [ "frappe.pulse.app_activity_event.send", ], - # Hourly but offset by 30 minutes "30 * * * *": [], # Daily but offset by 45 minutes From 4ec052a971e998359392c95241c821e60ca49cb3 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 22 Aug 2025 19:08:06 +0530 Subject: [PATCH 03/30] fix: linter errors --- frappe/pulse/app_activity_event.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/pulse/app_activity_event.py b/frappe/pulse/app_activity_event.py index d24d7934ea..82a88fa97e 100644 --- a/frappe/pulse/app_activity_event.py +++ b/frappe/pulse/app_activity_event.py @@ -8,7 +8,7 @@ KEY = "pulse:active_apps" EXPIRY = 60 * 60 * 12 # 12 hours -def log_app_activity(args): +def log_app_activity(req_params): if not is_enabled() or frappe.session.user in ("Guest", "Administrator"): return @@ -16,8 +16,8 @@ def log_app_activity(args): if status_code and not (200 <= status_code < 300): return - method = args.get("method") or frappe.form_dict.get("method") - doctype = args.get("doctype") or frappe.form_dict.get("doctype") + method = req_params.get("method") or frappe.form_dict.get("method") + doctype = req_params.get("doctype") or frappe.form_dict.get("doctype") if not method and not doctype: return From 7f0765fc9eec2b60a8ef90512801f36a6df16a3b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 30 Aug 2025 13:31:01 +0530 Subject: [PATCH 04/30] refactor: rename `app_activity` to `app_heartbeat` --- frappe/api/__init__.py | 5 ++--- frappe/hooks.py | 2 +- .../pulse/{app_activity_event.py => app_heartbeat_event.py} | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) rename frappe/pulse/{app_activity_event.py => app_heartbeat_event.py} (93%) diff --git a/frappe/api/__init__.py b/frappe/api/__init__.py index 32a4257c11..7d7a665ce7 100644 --- a/frappe/api/__init__.py +++ b/frappe/api/__init__.py @@ -8,9 +8,8 @@ from werkzeug.routing import Map, Submount from werkzeug.wrappers import Request, Response import frappe -import frappe.client from frappe import _ -from frappe.pulse.app_activity_event import log_app_activity +from frappe.pulse.app_heartbeat_event import log_app_heartbeat from frappe.utils.response import build_response @@ -68,7 +67,7 @@ def handle(request: Request): data = build_response("json") with suppress(Exception): - log_app_activity(arguments) + log_app_heartbeat(arguments) return data diff --git a/frappe/hooks.py b/frappe/hooks.py index 347b855591..0ed8bc2042 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -216,7 +216,7 @@ scheduler_events = { ], # 6 hours "0 */6 * * *": [ - "frappe.pulse.app_activity_event.send", + "frappe.pulse.app_heartbeat_event.send", ], # Hourly but offset by 30 minutes "30 * * * *": [], diff --git a/frappe/pulse/app_activity_event.py b/frappe/pulse/app_heartbeat_event.py similarity index 93% rename from frappe/pulse/app_activity_event.py rename to frappe/pulse/app_heartbeat_event.py index 82a88fa97e..4a5e496d8e 100644 --- a/frappe/pulse/app_activity_event.py +++ b/frappe/pulse/app_heartbeat_event.py @@ -8,7 +8,7 @@ KEY = "pulse:active_apps" EXPIRY = 60 * 60 * 12 # 12 hours -def log_app_activity(req_params): +def log_app_heartbeat(req_params): if not is_enabled() or frappe.session.user in ("Guest", "Administrator"): return @@ -46,7 +46,7 @@ def send(): for app in active_apps: events.append( { - "name": "app_activity", + "name": "app_heartbeat", "app": app, "app_version": _get_app_version(app), } @@ -56,7 +56,7 @@ def send(): if post_events(events): frappe.cache.delete_value(KEY) except Exception: - frappe.log_error(title="Failed to send app activity events") + frappe.log_error(title="Failed to send app heartbeat events") def _mark_active(app): From 6ebe7fc26e1928d384b35636dc3f326b2ed089d8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 30 Aug 2025 17:19:39 +0530 Subject: [PATCH 05/30] refactor: pulse client --- frappe/api/__init__.py | 4 +- frappe/hooks.py | 5 +- frappe/pulse/app_heartbeat_event.py | 92 ++++++++------------ frappe/pulse/client.py | 129 +++++++++++++++++++++------- frappe/pulse/utils.py | 97 +++++++++++++++++++++ 5 files changed, 234 insertions(+), 93 deletions(-) create mode 100644 frappe/pulse/utils.py diff --git a/frappe/api/__init__.py b/frappe/api/__init__.py index 7d7a665ce7..db7f96da50 100644 --- a/frappe/api/__init__.py +++ b/frappe/api/__init__.py @@ -9,7 +9,7 @@ from werkzeug.wrappers import Request, Response import frappe from frappe import _ -from frappe.pulse.app_heartbeat_event import log_app_heartbeat +from frappe.pulse.app_heartbeat_event import capture_app_heartbeat from frappe.utils.response import build_response @@ -67,7 +67,7 @@ def handle(request: Request): data = build_response("json") with suppress(Exception): - log_app_heartbeat(arguments) + capture_app_heartbeat(arguments) return data diff --git a/frappe/hooks.py b/frappe/hooks.py index 0ed8bc2042..67e7735a51 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -209,15 +209,12 @@ scheduler_events = { "frappe.automation.doctype.reminder.reminder.send_reminders", "frappe.model.utils.link_count.update_link_count", "frappe.search.sqlite_search.build_index_if_not_exists", + "frappe.pulse.client.send_queued_events", ], # 10 minutes "0/10 * * * *": [ "frappe.email.doctype.email_account.email_account.pull", ], - # 6 hours - "0 */6 * * *": [ - "frappe.pulse.app_heartbeat_event.send", - ], # Hourly but offset by 30 minutes "30 * * * *": [], # Daily but offset by 45 minutes diff --git a/frappe/pulse/app_heartbeat_event.py b/frappe/pulse/app_heartbeat_event.py index 4a5e496d8e..76a7b63327 100644 --- a/frappe/pulse/app_heartbeat_event.py +++ b/frappe/pulse/app_heartbeat_event.py @@ -1,27 +1,54 @@ import frappe from frappe.modules import get_doctype_module +from frappe.pulse.utils import get_app_version, get_frappe_version from frappe.utils.caching import site_cache -from .client import is_enabled, post_events +from .client import capture, is_enabled KEY = "pulse:active_apps" EXPIRY = 60 * 60 * 12 # 12 hours -def log_app_heartbeat(req_params): - if not is_enabled() or frappe.session.user in ("Guest", "Administrator"): +def capture_app_heartbeat(req_params): + if not should_capture(): return - status_code = frappe.response.http_status_code or 0 - if status_code and not (200 <= status_code < 300): - return - - method = req_params.get("method") or frappe.form_dict.get("method") - doctype = req_params.get("doctype") or frappe.form_dict.get("doctype") - + method, doctype = get_method_and_doctype(req_params) if not method and not doctype: return + app_name = get_app_name(method, doctype) + if app_name and app_name != "frappe": + capture( + event_name="app_heartbeat", + site=frappe.local.site, + app=app_name, + properties={ + "app_version": get_app_version(app_name), + "frappe_version": get_frappe_version(), + }, + interval="6h", + ) + + +def should_capture(): + if not is_enabled() or frappe.session.user in frappe.STANDARD_USERS: + return False + + status_code = frappe.response.http_status_code or 0 + if status_code and not (200 <= status_code < 300): + return False + + return True + + +def get_method_and_doctype(req_params): + method = req_params.get("method") or frappe.form_dict.get("method") + doctype = req_params.get("doctype") or frappe.form_dict.get("doctype") + return method, doctype + + +def get_app_name(method, doctype): app_name = None if method and "." in method and not method.startswith("frappe."): app_name = method.split(".", 1)[0] @@ -30,50 +57,7 @@ def log_app_heartbeat(req_params): module = get_doctype_module(doctype) app_name = app_module_map().get(module) - if app_name and app_name != "frappe": - _mark_active(app_name) - - -def send(): - if not is_enabled(): - return - - active_apps = frappe.cache.get_value(KEY) or set() - if not active_apps: - return - - events = [] - for app in active_apps: - events.append( - { - "name": "app_heartbeat", - "app": app, - "app_version": _get_app_version(app), - } - ) - - try: - if post_events(events): - frappe.cache.delete_value(KEY) - except Exception: - frappe.log_error(title="Failed to send app heartbeat events") - - -def _mark_active(app): - active_apps = frappe.cache.get_value(KEY) or set() - if app not in active_apps: - active_apps.add(app) - frappe.cache.set_value(KEY, active_apps) - ttl = frappe.cache.ttl(KEY) - if ttl in (-1, None): - frappe.cache.expire(KEY, EXPIRY) - - -def _get_app_version(app_name: str) -> str: - try: - return frappe.get_attr(app_name + ".__version__") - except Exception: - return "0.0.1" + return app_name @site_cache() diff --git a/frappe/pulse/client.py b/frappe/pulse/client.py index 343a95e100..e2edcdc97d 100644 --- a/frappe/pulse/client.py +++ b/frappe/pulse/client.py @@ -1,6 +1,10 @@ -from datetime import datetime, timezone +import time +from contextlib import suppress + +from orjson import JSONDecodeError import frappe +from frappe.pulse.utils import anonymize_user, ensure_http, parse_interval, utc_iso from frappe.utils import get_request_session from frappe.utils.caching import site_cache from frappe.utils.frappecloud import on_frappecloud @@ -17,52 +21,111 @@ def is_enabled() -> bool: ) -def post_events(events): - events = _sanitize_events(events) +def capture(event_name, site=None, app=None, user=None, properties=None, interval=None): + if not is_enabled(): + return + + try: + event_key = f"{event_name}:{site}:{app}:{user}" + if _is_ratelimited(event_key, interval): + return + + _queue_event( + { + "event_name": event_name, + "captured_at": utc_iso(), + "app": app, + "user": anonymize_user(user), + "site": site or frappe.local.site, + "properties": properties, + } + ) + _update_ratelimit(event_key, interval) + except Exception as e: + frappe.logger().error(f"Pulse event capture failed: {e!s}") + + +def _is_ratelimited(event_key, interval): + if not interval: + return False + + interval_seconds = parse_interval(interval) + last_sent_key = f"pulse:last_sent:{event_key}" + last_sent = frappe.cache.get_value(last_sent_key) + + if last_sent and time.monotonic() - float(last_sent) < interval_seconds: + return True + + return False + + +def _update_ratelimit(event_key, interval): + if not interval: + return + last_sent_key = f"pulse:last_sent:{event_key}" + frappe.cache.set_value(last_sent_key, time.monotonic(), expires_in_sec=86400) # 24h TTL + + +def _queue_event(event): + frappe.cache.lpush("pulse:events", frappe.as_json(event)) + frappe.cache.ltrim("pulse:events", 0, 4999) + + +def send_queued_events(): + batch_size = 100 + max_batches = 10 + for _ in range(max_batches): + events = get_next_batch(batch_size) + if not events: + break + try: + if not post(events): + frappe.logger().error("Pulse sending events failed: non-2xx response") + except Exception as e: + frappe.logger().error(f"Pulse sending events failed: {e!s}") + + +def get_next_batch(batch_size=100): + """Get batch of events from the queue""" + events = [] + for _ in range(batch_size): + event_json = frappe.cache.rpop("pulse:events") + if not event_json: + break + event_json = event_json.decode() + with suppress(JSONDecodeError): + data = frappe.parse_json(event_json) + events.append(data) + return events + + +def post(events): + # TODO: implement retry logic session = _create_session() - resp = session.post(_get_ingest_url(), data=events, timeout=5.0) + url = _get_ingest_url() + data = frappe.as_json({"events": events}) + resp = session.post(url, data=data, timeout=15) return 200 <= resp.status_code < 300 def _create_session(): api_key = frappe.conf.get("pulse_api_key") session = get_request_session() - if api_key: - session.headers.update({"Authorization": f"Bearer {api_key}"}) + session.headers.update( + { + "Content-Type": "application/json", + "X-Pulse-API-Key": api_key, + } + ) return session def _get_ingest_url(): host = frappe.conf.get("pulse_host") or "https://pulse.m.frappe.cloud" - if not host.startswith("http"): - host = "https://" + host + host = ensure_http(host) host = host.rstrip("/") - endpoint = frappe.conf.get("pulse_ingest_endpoint") or "/api/method/pulse.api.ingest" + endpoint = frappe.conf.get("pulse_ingest_endpoint") or "/api/method/pulse.api.bulk_ingest" endpoint = endpoint.lstrip("/") return f"{host}/{endpoint}" - - -def _sanitize_events(events): - _events = [] - if not isinstance(events, list): - _events = [events] - - for event in events: - if not isinstance(event, dict) or "name" not in event: - continue - event["site"] = event["site"] or frappe.local.site - event["timestamp"] = event["timestamp"] or _utc_iso() - event["frappe_version"] = event["frappe_version"] or _get_frappe_version() - _events.append(event) - - return _events - - -def _get_frappe_version() -> str: - return getattr(frappe, "__version__", "unknown") - - -def _utc_iso() -> str: - return datetime.now(timezone.utc).isoformat(timespec="seconds") diff --git a/frappe/pulse/utils.py b/frappe/pulse/utils.py new file mode 100644 index 0000000000..fd68a93a01 --- /dev/null +++ b/frappe/pulse/utils.py @@ -0,0 +1,97 @@ +import hashlib +from datetime import datetime, timezone + +import frappe + + +def anonymize_user(user): + """ + Create consistent anonymous ID from user email. + Same email always produces same anonymous ID. + """ + if not user or user in frappe.STANDARD_USERS: + return user + + # Use site-specific salt for additional security + site_salt = frappe.local.site or "default" + + # Create deterministic hash + hash_input = f"{user}:{site_salt}".encode() + user_hash = hashlib.sha256(hash_input).hexdigest() + + # Return first 12 characters for readability + return f"anon_{user_hash[:12]}" + + +def parse_interval(interval): + """ + Parse interval string or integer into seconds. + + Args: + interval: Can be: + - Integer: seconds (e.g., 3600) + - String: number + unit (e.g., "1h", "30m", "7d") + + Returns: + int: Total seconds + + Examples: + parse_interval(3600) -> 3600 + parse_interval("1h") -> 3600 + parse_interval("30m") -> 1800 + parse_interval("7d") -> 604800 + """ + if interval is None: + return None + + # If already an integer, return as-is (assuming seconds) + if isinstance(interval, int): + return interval + + # Parse string format + interval = str(interval).strip().lower() + + # Extract number and unit + if interval[-1].isdigit(): + # No unit specified, assume seconds + return int(interval) + + unit = interval[-1] + try: + number = int(interval[:-1]) + except ValueError: + raise ValueError(f"Invalid interval format: {interval}") + + # Convert to seconds + multipliers = { + "s": 1, # seconds + "m": 60, # minutes + "h": 3600, # hours + "d": 86400, # days + "w": 604800, # weeks + "y": 31536000, # years + } + + if unit not in multipliers: + raise ValueError(f"Invalid time unit '{unit}'. Use: s, m, h, d, w, y") + + return number * multipliers[unit] + + +def get_frappe_version() -> str: + return getattr(frappe, "__version__", "unknown") + + +def utc_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +def get_app_version(app_name: str) -> str: + try: + return frappe.get_attr(app_name + ".__version__") + except Exception: + return "0.0.1" + + +def ensure_http(url: str) -> str: + return url if url.startswith(("http://", "https://")) else "https://" + url From c230f92dd06cc98cbaff60d7cf9e773085a05a12 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 30 Aug 2025 20:57:06 +0530 Subject: [PATCH 06/30] chore: change namespace --- frappe/pulse/client.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/pulse/client.py b/frappe/pulse/client.py index e2edcdc97d..e4aa15693b 100644 --- a/frappe/pulse/client.py +++ b/frappe/pulse/client.py @@ -50,7 +50,7 @@ def _is_ratelimited(event_key, interval): return False interval_seconds = parse_interval(interval) - last_sent_key = f"pulse:last_sent:{event_key}" + last_sent_key = f"pulse-client:last_sent:{event_key}" last_sent = frappe.cache.get_value(last_sent_key) if last_sent and time.monotonic() - float(last_sent) < interval_seconds: @@ -62,13 +62,13 @@ def _is_ratelimited(event_key, interval): def _update_ratelimit(event_key, interval): if not interval: return - last_sent_key = f"pulse:last_sent:{event_key}" + last_sent_key = f"pulse-client:last_sent:{event_key}" frappe.cache.set_value(last_sent_key, time.monotonic(), expires_in_sec=86400) # 24h TTL def _queue_event(event): - frappe.cache.lpush("pulse:events", frappe.as_json(event)) - frappe.cache.ltrim("pulse:events", 0, 4999) + frappe.cache.lpush("pulse-client:events", frappe.as_json(event)) + frappe.cache.ltrim("pulse-client:events", 0, 4999) def send_queued_events(): @@ -89,7 +89,7 @@ def get_next_batch(batch_size=100): """Get batch of events from the queue""" events = [] for _ in range(batch_size): - event_json = frappe.cache.rpop("pulse:events") + event_json = frappe.cache.rpop("pulse-client:events") if not event_json: break event_json = event_json.decode() From 495e166ba4a0ae3538d21b9b7d5bc885b900360e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 2 Sep 2025 20:31:41 +0530 Subject: [PATCH 07/30] chore: whitelist `is_enabled` --- frappe/pulse/client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/pulse/client.py b/frappe/pulse/client.py index e4aa15693b..cceee11b5e 100644 --- a/frappe/pulse/client.py +++ b/frappe/pulse/client.py @@ -10,6 +10,7 @@ from frappe.utils.caching import site_cache from frappe.utils.frappecloud import on_frappecloud +@frappe.whitelist() @site_cache() def is_enabled() -> bool: return ( @@ -71,6 +72,9 @@ def _queue_event(event): frappe.cache.ltrim("pulse-client:events", 0, 4999) +def queue_length(): + return frappe.cache.llen("pulse-client:events") + def send_queued_events(): batch_size = 100 max_batches = 10 From ee2c4c20ce82b56095acfbc91169fbf73b054b2b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 3 Sep 2025 11:30:19 +0530 Subject: [PATCH 08/30] feat: use werkzeug `send_file` to allow range requests --- frappe/utils/response.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/frappe/utils/response.py b/frappe/utils/response.py index 4265ac64e9..b576277ae3 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -19,7 +19,6 @@ import werkzeug.utils from werkzeug.exceptions import Forbidden, NotFound from werkzeug.local import LocalProxy from werkzeug.wrappers import Response -from werkzeug.wsgi import wrap_file import frappe import frappe.model.document @@ -306,26 +305,26 @@ def send_private_file(path: str) -> Response: response = Response() response.headers["X-Accel-Redirect"] = quote(frappe.utils.encode(path)) response.headers["Cache-Control"] = "private,max-age=3600,stale-while-revalidate=86400" + response.headers["Accept-Ranges"] = "bytes" else: filepath = frappe.utils.get_site_path(path) - try: - f = open(filepath, "rb") - except OSError: + if not os.path.exists(filepath): raise NotFound - response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True) + mimetype = mimetypes.guess_type(filename)[0] or "application/octet-stream" - # no need for content disposition and force download. let browser handle its opening. - # Except for those that can be injected with scripts. + extension = os.path.splitext(path)[1] + blacklist = [".svg", ".html", ".htm", ".xml"] + as_attachment = extension.lower() in blacklist - extension = os.path.splitext(path)[1] - blacklist = [".svg", ".html", ".htm", ".xml"] + send_kwargs = dict(mimetype=mimetype, conditional=True, as_attachment=as_attachment) + environ = frappe.local.request.environ - if extension.lower() in blacklist: - response.headers.add("Content-Disposition", "attachment", filename=filename) - - response.mimetype = mimetypes.guess_type(filename)[0] or "application/octet-stream" + if as_attachment: + response = werkzeug.utils.send_file(filepath, environ, download_name=filename, **send_kwargs) + else: + response = werkzeug.utils.send_file(filepath, environ, **send_kwargs) return response From 67fc4a7ad4aabb609a8ba983757c00d3cd2e7021 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Wed, 17 Sep 2025 12:47:39 +0530 Subject: [PATCH 09/30] fix(QueryReport): respect user permissions in Link fields --- frappe/desk/query_report.py | 28 +++++++++++++------ .../js/frappe/views/reports/query_report.js | 4 +++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index b20cc92292..aa7598c768 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -199,10 +199,11 @@ def run( is_tree=False, parent_field=None, are_default_filters=True, + js_filters=None, ): if not user: user = frappe.session.user - validate_filters_permissions(report_name, filters, user) + validate_filters_permissions(report_name, filters, user, js_filters) report = get_report_doc(report_name) if not frappe.has_permission(report.ref_doctype, "report"): frappe.msgprint( @@ -894,25 +895,36 @@ def get_user_match_filters(doctypes, user): return match_filters -def validate_filters_permissions(report_name, filters=None, user=None): +def validate_filters_permissions(report_name, filters=None, user=None, js_filters=None): if not filters: return + # print(filters, "filters \n\n\n") + # print(js_filters, "js_filters \n\n\n") + + # print(frappe.query_reports["Trial Balance"], " query report \n\n\n") + + if isinstance(js_filters, str): + js_filters = json.loads(js_filters) + if isinstance(filters, str): filters = json.loads(filters) report = frappe.get_doc("Report", report_name) - for field in report.filters: - if field.fieldname in filters and field.fieldtype == "Link": - linked_doctype = field.options + + for field in report.filters + js_filters: + if hasattr(field, "as_dict"): + field = field.as_dict() + if field.get("fieldname") in filters and field.get("fieldtype") == "Link": + linked_doctype = field.get("options") if not has_permission( - doctype=linked_doctype, ptype="read", doc=filters[field.fieldname], user=user + doctype=linked_doctype, ptype="read", doc=filters[field.get("fieldname")], user=user ) and not has_permission( - doctype=linked_doctype, ptype="select", doc=filters[field.fieldname], user=user + doctype=linked_doctype, ptype="select", doc=filters[field.get("fieldname")], user=user ): frappe.throw( _("You do not have permission to access {0}: {1}.").format( - linked_doctype, filters[field.fieldname] + linked_doctype, filters[field.get("fieldname")] ) ) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 437a4179d1..a01026029a 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -719,6 +719,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { filters.prepared_report_name = this.prepared_report_name; } + // console.log(this.filters); + // console.log(frappe.query_reports[this.report_name].filters); + return new Promise((resolve) => { this.last_ajax = frappe.call({ method: "frappe.desk.query_report.run", @@ -730,6 +733,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { is_tree: this.report_settings.tree, parent_field: this.report_settings.parent_field, are_default_filters: are_default_filters, + js_filters: frappe.query_reports[this.report_name]?.filters, }, callback: resolve, always: () => this.page.btn_secondary.prop("disabled", false), From 94d31b5cdca48c180e2e71c49886000b3eb24730 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Wed, 17 Sep 2025 12:49:40 +0530 Subject: [PATCH 10/30] refactor: remove debigging statements --- frappe/desk/query_report.py | 5 ----- frappe/public/js/frappe/views/reports/query_report.js | 3 --- 2 files changed, 8 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index aa7598c768..98114eb1ca 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -899,11 +899,6 @@ def validate_filters_permissions(report_name, filters=None, user=None, js_filter if not filters: return - # print(filters, "filters \n\n\n") - # print(js_filters, "js_filters \n\n\n") - - # print(frappe.query_reports["Trial Balance"], " query report \n\n\n") - if isinstance(js_filters, str): js_filters = json.loads(js_filters) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index a01026029a..23e6abe610 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -719,9 +719,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { filters.prepared_report_name = this.prepared_report_name; } - // console.log(this.filters); - // console.log(frappe.query_reports[this.report_name].filters); - return new Promise((resolve) => { this.last_ajax = frappe.call({ method: "frappe.desk.query_report.run", From de3c5c7800bec194dca1d60631ccb11362bbf783 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 17 Sep 2025 13:32:50 +0530 Subject: [PATCH 11/30] chore: remove unused variables --- frappe/pulse/app_heartbeat_event.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/frappe/pulse/app_heartbeat_event.py b/frappe/pulse/app_heartbeat_event.py index 76a7b63327..9499ab85a1 100644 --- a/frappe/pulse/app_heartbeat_event.py +++ b/frappe/pulse/app_heartbeat_event.py @@ -5,9 +5,6 @@ from frappe.utils.caching import site_cache from .client import capture, is_enabled -KEY = "pulse:active_apps" -EXPIRY = 60 * 60 * 12 # 12 hours - def capture_app_heartbeat(req_params): if not should_capture(): From ec0a864d35e661cfe9c41f009c5fe8f22e09a073 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 17 Sep 2025 13:39:45 +0530 Subject: [PATCH 12/30] refactor: simplify `send_file` parameters --- frappe/utils/response.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/frappe/utils/response.py b/frappe/utils/response.py index b576277ae3..cf99e5ba02 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -312,19 +312,17 @@ def send_private_file(path: str) -> Response: if not os.path.exists(filepath): raise NotFound - mimetype = mimetypes.guess_type(filename)[0] or "application/octet-stream" - extension = os.path.splitext(path)[1] blacklist = [".svg", ".html", ".htm", ".xml"] as_attachment = extension.lower() in blacklist - send_kwargs = dict(mimetype=mimetype, conditional=True, as_attachment=as_attachment) - environ = frappe.local.request.environ - - if as_attachment: - response = werkzeug.utils.send_file(filepath, environ, download_name=filename, **send_kwargs) - else: - response = werkzeug.utils.send_file(filepath, environ, **send_kwargs) + response = werkzeug.utils.send_file( + filepath, + environ=frappe.local.request.environ, + conditional=True, + as_attachment=as_attachment, + download_name=filename if as_attachment else None, + ) return response From 888be118d3db0c48c1e39766d97d6a3e5a77fb07 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 17 Sep 2025 13:39:57 +0530 Subject: [PATCH 13/30] chore: fix linting errors --- frappe/pulse/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/pulse/client.py b/frappe/pulse/client.py index cceee11b5e..7ffe3a6e28 100644 --- a/frappe/pulse/client.py +++ b/frappe/pulse/client.py @@ -75,6 +75,7 @@ def _queue_event(event): def queue_length(): return frappe.cache.llen("pulse-client:events") + def send_queued_events(): batch_size = 100 max_batches = 10 From 7c60ce811a3cfd98531e2b5b4312efd4a6a3e3d7 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan Date: Wed, 17 Sep 2025 14:50:38 +0530 Subject: [PATCH 14/30] fix: check for none/empty js filters --- frappe/desk/query_report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 98114eb1ca..32cc80746b 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -899,6 +899,9 @@ def validate_filters_permissions(report_name, filters=None, user=None, js_filter if not filters: return + if js_filters is None: + js_filters = [] + if isinstance(js_filters, str): js_filters = json.loads(js_filters) From 2955e655251bce36230489a46da979a9ea14dd4d Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:15:40 +0200 Subject: [PATCH 15/30] fix: round seconds to minutes in Duration (#34020) --- cypress/integration/utils.js | 18 ++++++++++++++++++ frappe/public/js/frappe/utils/utils.js | 4 +--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/cypress/integration/utils.js b/cypress/integration/utils.js index e30d922429..083a03294a 100644 --- a/cypress/integration/utils.js +++ b/cypress/integration/utils.js @@ -49,6 +49,24 @@ context("Utils", () => { seconds: 0, }); }); + + run_util("seconds_to_duration", 60 * 60, { hide_seconds: 1 }).then((duration) => { + expect(duration).to.deep.equal({ + days: 0, + hours: 1, + minutes: 0, + seconds: 0, + }); + }); + + run_util("seconds_to_duration", 15 * 60, { hide_seconds: 1 }).then((duration) => { + expect(duration).to.deep.equal({ + days: 0, + hours: 0, + minutes: 15, + seconds: 0, + }); + }); }); it("should parse days, hours, minutes and seconds", () => { diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 5a5b75bdeb..37e0980cfd 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1144,8 +1144,6 @@ Object.assign(frappe.utils, { seconds_to_duration(seconds, duration_options) { const floor = seconds > 0 ? Math.floor : Math.ceil; - const round_base_60 = (seconds) => floor(seconds / 60 + (seconds > 0 ? 0.5 : -0.5)); - const total_duration = { days: floor(seconds / 86400), // 60 * 60 * 24 hours: floor((seconds % 86400) / 3600), @@ -1159,7 +1157,7 @@ Object.assign(frappe.utils, { } if (duration_options && duration_options.hide_seconds) { - total_duration.minutes += round_base_60(total_duration.seconds); + total_duration.minutes += Math.round(total_duration.seconds / 60); total_duration.seconds = 0; } From 4a830b49e820f4c1a3fe4e8ab83bc8f31586723a Mon Sep 17 00:00:00 2001 From: Ayush Chaudhari Date: Wed, 17 Sep 2025 16:58:26 +0530 Subject: [PATCH 16/30] fix: better redirect handling --- frappe/www/login.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/frappe/www/login.py b/frappe/www/login.py index c9a5e63816..cbd71bb77f 100644 --- a/frappe/www/login.py +++ b/frappe/www/login.py @@ -2,7 +2,7 @@ # License: MIT. See LICENSE -from urllib.parse import urlparse +from urllib.parse import urljoin, urlparse import frappe import frappe.utils @@ -202,17 +202,21 @@ def sanitize_redirect(redirect: str | None) -> str | None: Allowed redirects: - Same host e.g. https://frappe.localhost/path - - Just path e.g. /app + - Just path e.g. /app gets converted to https://frappe.localhost/app """ if not redirect: return redirect parsed_redirect = urlparse(redirect) - if not parsed_redirect.netloc: - return redirect parsed_request_host = urlparse(frappe.local.request.url) - if parsed_request_host.netloc == parsed_redirect.netloc: - return redirect + output_parsed_url = parsed_redirect._replace( + netloc=parsed_request_host.netloc, scheme=parsed_request_host.scheme + ) + if parsed_redirect.netloc: + if parsed_request_host.netloc != parsed_redirect.netloc: + output_parsed_url = output_parsed_url._replace(path="/app") + else: + output_parsed_url = output_parsed_url._replace(path=parsed_redirect.path) - return None + return output_parsed_url.geturl() From 9f3e32723141fceb0df64eb52902272de33f0c36 Mon Sep 17 00:00:00 2001 From: Ayush Chaudhari Date: Wed, 17 Sep 2025 16:58:36 +0530 Subject: [PATCH 17/30] test: better redirect handling --- frappe/tests/test_api.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py index aa40817bc4..4518fdc52d 100644 --- a/frappe/tests/test_api.py +++ b/frappe/tests/test_api.py @@ -461,13 +461,15 @@ class TestResponse(FrappeAPITestCase): def test_login_redirects(self): expected_redirects = { - "/app/user": "/app/user", - "/app/user?enabled=1": "/app/user?enabled=1", - "http://example.com": "/app", # No external redirect - "https://google.com": "/app", - "http://localhost:8000": "/app", + "/app/user": "http://localhost/app/user", + "/app/user?enabled=1": "http://localhost/app/user?enabled=1", + "http://example.com": "http://localhost/app", # No external redirect + "https://google.com": "http://localhost/app", + "http://localhost:8000": "http://localhost/app", "http://localhost/app": "http://localhost/app", + "////example.com": "http://localhost//example.com", # malicious redirect attempt } + for redirect, expected_redirect in expected_redirects.items(): response = self.get(f"/login?{urlencode({'redirect-to':redirect})}", {"sid": self.sid}) self.assertEqual(response.location, expected_redirect) From 3797756b43985532579a100b4e5de6059b4cbfc2 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 17 Sep 2025 21:06:20 +0200 Subject: [PATCH 18/30] chore: enable Norsk Bokmal (#33978) --- frappe/geo/languages.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/geo/languages.csv b/frappe/geo/languages.csv index 0a1a3d9dae..ce220cd3ee 100644 --- a/frappe/geo/languages.csv +++ b/frappe/geo/languages.csv @@ -53,6 +53,7 @@ mn,Монгол,0 mr,मराठी,0 ms,Melayu,0 my,မြန်မာ,0 +nb,Norsk Bokmål,1 nl,Nederlands,0 no,Norsk,0 pl,Polski,0 From cd6360318d12c4c84a589c8e52a5122bf7211286 Mon Sep 17 00:00:00 2001 From: MochaMind Date: Thu, 18 Sep 2025 00:51:29 +0530 Subject: [PATCH 19/30] fix: sync translations from crowdin (#34025) --- frappe/locale/bs.po | 8 +-- frappe/locale/fr.po | 6 +-- frappe/locale/hr.po | 8 +-- frappe/locale/nb.po | 122 ++++++++++++++++++++++---------------------- frappe/locale/sv.po | 8 +-- 5 files changed, 76 insertions(+), 76 deletions(-) diff --git a/frappe/locale/bs.po b/frappe/locale/bs.po index 376f215bb1..504a1b0d3f 100644 --- a/frappe/locale/bs.po +++ b/frappe/locale/bs.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-14 09:32+0000\n" -"PO-Revision-Date: 2025-09-15 18:13\n" +"PO-Revision-Date: 2025-09-17 18:30\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Bosnian\n" "MIME-Version: 1.0\n" @@ -5454,7 +5454,7 @@ msgstr "Kontakt" #: frappe/integrations/doctype/google_calendar/google_calendar.py:812 msgid "Contact / email not found. Did not add attendee for -
{0}" -msgstr "" +msgstr "Kontakt/e-pošta nije pronađena. Nije dodan učesnik za -
{0}" #. Label of the sb_01 (Section Break) field in DocType 'Contact' #: frappe/contacts/doctype/contact/contact.json @@ -13679,7 +13679,7 @@ msgstr "Je Primarno" #: frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py:43 msgid "Is Primary Address" -msgstr "" +msgstr "Primarna Adresa" #. Label of the is_primary_contact (Check) field in DocType 'Contact' #: frappe/contacts/doctype/contact/contact.json @@ -26298,7 +26298,7 @@ msgstr "Ovaj Mjesec" #: frappe/core/doctype/file/file.py:394 msgid "This PDF cannot be uploaded as it contains unsafe content." -msgstr "" +msgstr "Ovaj PDF se ne može prenijeti jer sadrži nesiguran sadržaj." #: frappe/public/js/frappe/ui/filters/filter.js:670 msgid "This Quarter" diff --git a/frappe/locale/fr.po b/frappe/locale/fr.po index 065c70bed7..f34dff706c 100644 --- a/frappe/locale/fr.po +++ b/frappe/locale/fr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-14 09:32+0000\n" -"PO-Revision-Date: 2025-09-15 18:13\n" +"PO-Revision-Date: 2025-09-17 18:29\n" "Last-Translator: developers@frappe.io\n" "Language-Team: French\n" "MIME-Version: 1.0\n" @@ -4340,7 +4340,7 @@ msgstr "" #. Label of the changed_values (HTML) field in DocType 'Permission Log' #: frappe/core/doctype/permission_log/permission_log.json msgid "Changes" -msgstr "" +msgstr "Modifications" #: frappe/email/doctype/email_domain/email_domain.js:5 msgid "Changing any setting will reflect on all the email accounts associated with this domain." @@ -8002,7 +8002,7 @@ msgstr "Vous n'avez pas de compte?" #: frappe/public/js/print_format_builder/HTMLEditor.vue:5 #: frappe/public/js/print_format_builder/LetterHeadEditor.vue:52 msgid "Done" -msgstr "" +msgstr "Terminé" #. Option for the 'Type' (Select) field in DocType 'Dashboard Chart' #: frappe/desk/doctype/dashboard_chart/dashboard_chart.json diff --git a/frappe/locale/hr.po b/frappe/locale/hr.po index c7625485fc..a535b4030a 100644 --- a/frappe/locale/hr.po +++ b/frappe/locale/hr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-14 09:32+0000\n" -"PO-Revision-Date: 2025-09-15 18:13\n" +"PO-Revision-Date: 2025-09-17 18:30\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Croatian\n" "MIME-Version: 1.0\n" @@ -5454,7 +5454,7 @@ msgstr "Kontakt" #: frappe/integrations/doctype/google_calendar/google_calendar.py:812 msgid "Contact / email not found. Did not add attendee for -
{0}" -msgstr "" +msgstr "Kontakt/e-pošta nije pronađena. Nije dodan sudionik za -
{0}" #. Label of the sb_01 (Section Break) field in DocType 'Contact' #: frappe/contacts/doctype/contact/contact.json @@ -13679,7 +13679,7 @@ msgstr "Je Primarno" #: frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py:43 msgid "Is Primary Address" -msgstr "" +msgstr "Primarna Adresa" #. Label of the is_primary_contact (Check) field in DocType 'Contact' #: frappe/contacts/doctype/contact/contact.json @@ -26298,7 +26298,7 @@ msgstr "Ovaj Mjesec" #: frappe/core/doctype/file/file.py:394 msgid "This PDF cannot be uploaded as it contains unsafe content." -msgstr "" +msgstr "Ovaj PDF se ne može prenijeti jer sadrži nesiguran sadržaj." #: frappe/public/js/frappe/ui/filters/filter.js:670 msgid "This Quarter" diff --git a/frappe/locale/nb.po b/frappe/locale/nb.po index 49cb11ebb8..0403353f4f 100644 --- a/frappe/locale/nb.po +++ b/frappe/locale/nb.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-14 09:32+0000\n" -"PO-Revision-Date: 2025-09-15 18:12\n" +"PO-Revision-Date: 2025-09-17 18:30\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Norwegian Bokmal\n" "MIME-Version: 1.0\n" @@ -4240,11 +4240,11 @@ msgstr "Kan ikke aktivere {0} for en dokumenttype som ikke kan registreres" #: frappe/core/doctype/file/file.py:262 msgid "Cannot find file {} on disk" -msgstr "" +msgstr "Kan ikke finne filen {} på disken" #: frappe/core/doctype/file/file.py:581 msgid "Cannot get file contents of a Folder" -msgstr "" +msgstr "Kan ikke hente filinnholdet i en mappe" #: frappe/printing/page/print/print.js:884 msgid "Cannot have multiple printers mapped to a single print format." @@ -4252,7 +4252,7 @@ msgstr "Kan ikke ha flere skrivere tilordnet til ett og samme utskriftsformat." #: frappe/public/js/frappe/form/grid.js:1133 msgid "Cannot import table with more than 5000 rows." -msgstr "" +msgstr "Kan ikke importere tabell med mer enn 5000 rader." #: frappe/model/document.py:1105 msgid "Cannot link cancelled document: {0}" @@ -4260,19 +4260,19 @@ msgstr "Kan ikke lenke til avbrutt dokument: {0}" #: frappe/model/mapper.py:175 msgid "Cannot map because following condition fails:" -msgstr "" +msgstr "Kan ikke mappe fordi følgende betingelse mislykkes:" #: frappe/core/doctype/data_import/importer.py:971 msgid "Cannot match column {0} with any field" -msgstr "" +msgstr "Kan ikke matche kolonnen {0} med noe felt" #: frappe/public/js/frappe/form/grid_row.js:175 msgid "Cannot move row" -msgstr "" +msgstr "Kan ikke flytte rad" #: frappe/public/js/frappe/views/reports/report_view.js:932 msgid "Cannot remove ID field" -msgstr "" +msgstr "Kan ikke fjerne ID-feltet" #: frappe/core/page/permission_manager/permission_manager.py:132 msgid "Cannot set 'Report' permission if 'Only If Creator' permission is set" @@ -4293,19 +4293,19 @@ msgstr "Kan ikke registrere {0}." #: frappe/desk/doctype/bulk_update/bulk_update.js:26 #: frappe/public/js/frappe/list/bulk_operations.js:366 msgid "Cannot update {0}" -msgstr "" +msgstr "Kan ikke oppdatere {0}" #: frappe/model/db_query.py:1130 msgid "Cannot use sub-query here." -msgstr "" +msgstr "Kan ikke bruke underspørsmål her." #: frappe/model/db_query.py:1162 msgid "Cannot use {0} in order/group by" -msgstr "" +msgstr "Kan ikke bruke {0} i rekkefølge/gruppe etter" #: frappe/public/js/frappe/list/bulk_operations.js:297 msgid "Cannot {0} {1}." -msgstr "" +msgstr "Kan ikke {0} {1}." #: frappe/utils/password_strength.py:181 msgid "Capitalization doesn't help very much." @@ -4313,12 +4313,12 @@ msgstr "Store bokstaver hjelper ikke noe særlig." #: frappe/public/js/frappe/ui/capture.js:294 msgid "Capture" -msgstr "" +msgstr "Ta bilde" #. Label of the card (Link) field in DocType 'Number Card Link' #: frappe/desk/doctype/number_card_link/number_card_link.json msgid "Card" -msgstr "" +msgstr "Kort" #. Option for the 'Type' (Select) field in DocType 'Workspace Link' #: frappe/desk/doctype/workspace_link/workspace_link.json @@ -4327,11 +4327,11 @@ msgstr "Kortskille" #: frappe/public/js/frappe/views/reports/query_report.js:262 msgid "Card Label" -msgstr "" +msgstr "Kortetikett" #: frappe/public/js/frappe/widgets/widget_dialog.js:262 msgid "Card Links" -msgstr "" +msgstr "Kortlenker" #. Label of the cards (Table) field in DocType 'Dashboard' #: frappe/desk/doctype/dashboard/dashboard.json @@ -4344,12 +4344,12 @@ msgstr "Kort" #: frappe/public/js/frappe/views/interaction.js:72 #: frappe/website/doctype/help_article/help_article.json msgid "Category" -msgstr "" +msgstr "Kategori" #. Label of the category_description (Text) field in DocType 'Help Category' #: frappe/website/doctype/help_category/help_category.json msgid "Category Description" -msgstr "" +msgstr "Kategoribeskrivelse" #. Label of the category_name (Data) field in DocType 'Help Category' #: frappe/website/doctype/help_category/help_category.json @@ -4421,20 +4421,20 @@ msgstr "Endret av" #. Name of a DocType #: frappe/desk/doctype/changelog_feed/changelog_feed.json msgid "Changelog Feed" -msgstr "" +msgstr "Endringslogg" #. Label of the changed_values (HTML) field in DocType 'Permission Log' #: frappe/core/doctype/permission_log/permission_log.json msgid "Changes" -msgstr "" +msgstr "Endringer" #: frappe/email/doctype/email_domain/email_domain.js:5 msgid "Changing any setting will reflect on all the email accounts associated with this domain." -msgstr "" +msgstr "Hvis du endrer en innstilling, vil det påvirke alle e-postkontoer som er knyttet til dette domenet." #: frappe/core/doctype/system_settings/system_settings.js:67 msgid "Changing rounding method on site with data can result in unexpected behaviour." -msgstr "" +msgstr "Endring av avrundingsmetode på stedet med data kan føre til uventet oppførsel." #. Label of the channel (Select) field in DocType 'Notification' #: frappe/email/doctype/notification/notification.json @@ -4449,7 +4449,7 @@ msgstr "Diagram" #. Label of the chart_config (Code) field in DocType 'Dashboard Settings' #: frappe/desk/doctype/dashboard_settings/dashboard_settings.json msgid "Chart Configuration" -msgstr "" +msgstr "Konfigurasjon av diagram" #. Label of the chart_name (Data) field in DocType 'Dashboard Chart' #. Label of the chart_name (Link) field in DocType 'Workspace Chart' @@ -4466,12 +4466,12 @@ msgstr "Diagram-navn" #: frappe/desk/doctype/dashboard/dashboard.json #: frappe/desk/doctype/dashboard_chart/dashboard_chart.json msgid "Chart Options" -msgstr "" +msgstr "Alternativer for diagram" #. Label of the source (Link) field in DocType 'Dashboard Chart' #: frappe/desk/doctype/dashboard_chart/dashboard_chart.json msgid "Chart Source" -msgstr "" +msgstr "Kilde for diagram" #. Label of the chart_type (Select) field in DocType 'Dashboard Chart' #: frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -4484,7 +4484,7 @@ msgstr "Diagramtype" #: frappe/desk/doctype/dashboard/dashboard.json #: frappe/desk/doctype/workspace/workspace.json msgid "Charts" -msgstr "" +msgstr "Diagrammer" #. Option for the 'Type' (Select) field in DocType 'Communication' #: frappe/core/doctype/communication/communication.json @@ -4522,7 +4522,7 @@ msgstr "Sjekk feilloggen for mer informasjon: {0}" #: frappe/website/doctype/website_settings/website_settings.js:147 msgid "Check this if you don't want users to sign up for an account on your site. Users won't get desk access unless you explicitly provide it." -msgstr "" +msgstr "Merk av for dette hvis du ikke vil at brukerne skal registrere seg for en konto på nettstedet ditt. Brukerne får ikke skrivebordstilgang med mindre du eksplisitt gir dem det." #. Description of the 'User must always select' (Check) field in DocType #. 'Document Naming Settings' @@ -4533,52 +4533,52 @@ msgstr "Marker denne hvis du vil tvinge brukeren til å velge en serie før du l #. Description of the 'Show Full Number' (Check) field in DocType 'Number Card' #: frappe/desk/doctype/number_card/number_card.json msgid "Check to display the full numeric value (e.g., 1,234,567 instead of 1.2M)." -msgstr "" +msgstr "Merk av for å vise hele tallverdien (f.eks. 1 234 567 i stedet for 1,2 millioner)." #: frappe/public/js/frappe/desk.js:235 msgid "Checking one moment" -msgstr "" +msgstr "Sjekker ett øyeblikk" #: frappe/website/doctype/website_settings/website_settings.js:140 msgid "Checking this will enable tracking page views for blogs, web pages, etc." -msgstr "" +msgstr "Hvis du merker av for dette, kan du spore sidevisninger for blogger, nettsider osv." #. Description of the 'Hide Custom DocTypes and Reports' (Check) field in #. DocType 'Workspace' #: frappe/desk/doctype/workspace/workspace.json msgid "Checking this will hide custom doctypes and reports cards in Links section" -msgstr "" +msgstr "Hvis du merker av for dette, skjules egendefinerte DocType-er og rapportkort i Links-delen" #: frappe/website/doctype/web_page/web_page.js:78 msgid "Checking this will publish the page on your website and it'll be visible to everyone." -msgstr "" +msgstr "Hvis du merker av for dette, publiseres siden på nettstedet ditt, og den blir synlig for alle." #: frappe/website/doctype/web_page/web_page.js:104 msgid "Checking this will show a text area where you can write custom javascript that will run on this page." -msgstr "" +msgstr "Hvis du merker av for dette, vises et tekstområde der du kan skrive egendefinert javascript som skal kjøres på denne siden." #: frappe/www/list.py:85 msgid "Child DocTypes are not allowed" -msgstr "" +msgstr "Underordnede DocType-er er ikke tillatt" #. Label of the child_doctype (Data) field in DocType 'Form Tour Step' #: frappe/desk/doctype/form_tour_step/form_tour_step.json msgid "Child Doctype" -msgstr "" +msgstr "Underordnet DocType" #: frappe/core/doctype/doctype/doctype.py:1648 msgid "Child Table {0} for field {1}" -msgstr "" +msgstr "Underordnet tabell {0} for feltet {1}" #. Description of the 'Is Child Table' (Check) field in DocType 'DocType' #: frappe/core/doctype/doctype/doctype.json #: frappe/core/doctype/doctype/doctype_list.js:52 msgid "Child Tables are shown as a Grid in other DocTypes" -msgstr "" +msgstr "Underordnede tabeller vises som et rutenett i andre DocType-er" #: frappe/database/query.py:660 msgid "Child query fields for '{0}' must be a list or tuple." -msgstr "" +msgstr "Underordnede spørringsfelt for '{0}' må være en liste eller en tupel." #: frappe/public/js/frappe/widgets/widget_dialog.js:651 msgid "Choose Existing Card or create New Card" @@ -4586,17 +4586,17 @@ msgstr "Velg eksisterende kort eller opprett nytt kort" #: frappe/public/js/frappe/views/workspace/workspace.js:571 msgid "Choose a block or continue typing" -msgstr "" +msgstr "Velg en blokk eller fortsett å skrive" #: frappe/public/js/form_builder/components/controls/DataControl.vue:18 #: frappe/public/js/frappe/form/controls/color.js:5 msgid "Choose a color" -msgstr "" +msgstr "Velg en farge" #: frappe/public/js/form_builder/components/controls/DataControl.vue:21 #: frappe/public/js/frappe/form/controls/icon.js:5 msgid "Choose an icon" -msgstr "" +msgstr "Velg et ikon" #. Description of the 'Two Factor Authentication method' (Select) field in #. DocType 'System Settings' @@ -4688,7 +4688,7 @@ msgstr "Klikk på knappen for å logge inn på {0}" #: frappe/templates/emails/data_deletion_approval.html:2 msgid "Click on the link below to approve the request" -msgstr "" +msgstr "Klikk på lenken nedenfor for å godkjenne forespørselen" #: frappe/templates/emails/new_user.html:7 msgid "Click on the link below to complete your registration and set a new password" @@ -4700,7 +4700,7 @@ msgstr "Klikk på lenken nedenfor for å laste ned dataene dine" #: frappe/templates/emails/delete_data_confirmation.html:4 msgid "Click on the link below to verify your request" -msgstr "" +msgstr "Klikk på lenken nedenfor for å bekrefte forespørselen din" #: frappe/integrations/doctype/google_calendar/google_calendar.py:118 #: frappe/integrations/doctype/google_contacts/google_contacts.py:41 @@ -4728,7 +4728,7 @@ msgstr "Klikk for å angi filtre" #: frappe/public/js/frappe/list/list_view.js:739 msgid "Click to sort by {0}" -msgstr "" +msgstr "Klikk for å sortere etter {0}" #. Option for the 'Delivery Status' (Select) field in DocType 'Communication' #: frappe/core/doctype/communication/communication.json @@ -5097,7 +5097,7 @@ msgstr "Kommentaroffentlighet kan bare oppdateres av den opprinnelige forfattere #: frappe/public/js/frappe/model/model.js:135 #: frappe/website/doctype/web_form/templates/web_form.html:129 msgid "Comments" -msgstr "" +msgstr "Kommentarer" #. Description of the 'Timeline Field' (Data) field in DocType 'DocType' #: frappe/core/doctype/doctype/doctype.json @@ -5470,12 +5470,12 @@ msgstr "" #. Label of the phone_nos (Table) field in DocType 'Contact' #: frappe/contacts/doctype/contact/contact.json msgid "Contact Numbers" -msgstr "" +msgstr "Kontaktnummere" #. Name of a DocType #: frappe/contacts/doctype/contact_phone/contact_phone.json msgid "Contact Phone" -msgstr "" +msgstr "Kontakttelefon" #: frappe/integrations/doctype/google_contacts/google_contacts.py:291 msgid "Contact Synced with Google Contacts." @@ -5591,15 +5591,15 @@ msgstr "Kontrollerer om nye brukere kan registrere seg med denne sosiale pålogg #: frappe/public/js/frappe/utils/utils.js:1036 msgid "Copied to clipboard." -msgstr "" +msgstr "Kopier til utklippstavlen" #: frappe/public/js/frappe/form/templates/timeline_message_box.html:93 msgid "Copy Link" -msgstr "" +msgstr "Kopier lenke" #: frappe/website/doctype/web_form/web_form.js:29 msgid "Copy embed code" -msgstr "" +msgstr "Kopier innbyggingskode" #: frappe/public/js/frappe/request.js:621 msgid "Copy error to clipboard" @@ -5607,7 +5607,7 @@ msgstr "Kopier feil til utklippstavlen" #: frappe/public/js/frappe/form/toolbar.js:507 msgid "Copy to Clipboard" -msgstr "" +msgstr "Kopier til utklippstavlen" #: frappe/core/doctype/user/user.js:480 msgid "Copy token to clipboard" @@ -5620,7 +5620,7 @@ msgstr "Opphavsrett" #: frappe/custom/doctype/customize_form/customize_form.py:123 msgid "Core DocTypes cannot be customized." -msgstr "" +msgstr "DocType-er i kjernen kan ikke tilpasses." #: frappe/desk/doctype/global_search_settings/global_search_settings.py:36 msgid "Core Modules {0} cannot be searched in Global Search." @@ -5632,23 +5632,23 @@ msgstr "Riktig versjon:" #: frappe/email/smtp.py:78 msgid "Could not connect to outgoing email server" -msgstr "" +msgstr "Kunne ikke koble til serveren for utgående e-post" #: frappe/model/document.py:1101 msgid "Could not find {0}" -msgstr "" +msgstr "Kunne ikke finne {0}" #: frappe/core/doctype/data_import/importer.py:933 msgid "Could not map column {0} to field {1}" -msgstr "" +msgstr "Kunne ikke tilordne kolonne {0} til felt {1}" #: frappe/database/query.py:564 msgid "Could not parse field: {0}" -msgstr "" +msgstr "Kunne ikke analysere feltet: {0}" #: frappe/desk/page/setup_wizard/setup_wizard.js:234 msgid "Could not start up:" -msgstr "" +msgstr "Kunne ikke starte opp:" #: frappe/public/js/frappe/web_form/web_form.js:383 msgid "Couldn't save, please check the data you have entered" @@ -5669,7 +5669,7 @@ msgstr "Antall" #: frappe/public/js/frappe/widgets/widget_dialog.js:540 msgid "Count Customizations" -msgstr "" +msgstr "Egendefinering av teller" #. Label of the section_break_5 (Section Break) field in DocType 'Workspace #. Shortcut' @@ -5686,7 +5686,7 @@ msgstr "Antall lenkede dokumenter" #. Label of the counter (Int) field in DocType 'Document Naming Rule' #: frappe/core/doctype/document_naming_rule/document_naming_rule.json msgid "Counter" -msgstr "" +msgstr "Teller" #. Label of the country (Link) field in DocType 'Address' #. Label of the country (Link) field in DocType 'Address Template' @@ -5709,7 +5709,7 @@ msgstr "Landskode er påkrevd" #. Label of the country_name (Data) field in DocType 'Country' #: frappe/geo/doctype/country/country.json msgid "Country Name" -msgstr "" +msgstr "Landsnavn" #. Label of the county (Data) field in DocType 'Address' #: frappe/contacts/doctype/address/address.json @@ -9885,7 +9885,7 @@ msgstr "FavIcon" #. Label of the fax (Data) field in DocType 'Address' #: frappe/contacts/doctype/address/address.json msgid "Fax" -msgstr "" +msgstr "Faks" #: frappe/public/js/frappe/form/templates/form_sidebar.html:33 msgid "Feedback" diff --git a/frappe/locale/sv.po b/frappe/locale/sv.po index 48d3c35591..8e273eb00d 100644 --- a/frappe/locale/sv.po +++ b/frappe/locale/sv.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-14 09:32+0000\n" -"PO-Revision-Date: 2025-09-15 18:12\n" +"PO-Revision-Date: 2025-09-17 18:30\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Swedish\n" "MIME-Version: 1.0\n" @@ -5453,7 +5453,7 @@ msgstr "Kontakt" #: frappe/integrations/doctype/google_calendar/google_calendar.py:812 msgid "Contact / email not found. Did not add attendee for -
{0}" -msgstr "" +msgstr "Kontakt / e-post hittades inte. Har inte lagt till deltagare för -
{0}" #. Label of the sb_01 (Section Break) field in DocType 'Contact' #: frappe/contacts/doctype/contact/contact.json @@ -13677,7 +13677,7 @@ msgstr "Är Primär" #: frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py:43 msgid "Is Primary Address" -msgstr "" +msgstr "Är Primär Adress" #. Label of the is_primary_contact (Check) field in DocType 'Contact' #: frappe/contacts/doctype/contact/contact.json @@ -26292,7 +26292,7 @@ msgstr "Denna Månad" #: frappe/core/doctype/file/file.py:394 msgid "This PDF cannot be uploaded as it contains unsafe content." -msgstr "" +msgstr "Denna PDF kan inte laddas upp eftersom den innehåller osäkert innehåll." #: frappe/public/js/frappe/ui/filters/filter.js:670 msgid "This Quarter" From 6746ce9043385ec054ff0ccd4a06d9aa6ad505d9 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 17 Sep 2025 21:25:33 +0200 Subject: [PATCH 20/30] fix(Number Card): permission query and frontend perms (#34023) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../desk/doctype/number_card/number_card.js | 41 ++++++++++-- .../desk/doctype/number_card/number_card.json | 4 +- .../desk/doctype/number_card/number_card.py | 65 +++++++++---------- 3 files changed, 68 insertions(+), 42 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index 274ca76848..c474489640 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -26,6 +26,7 @@ frappe.ui.form.on("Number Card", { frm.trigger("render_filters_table"); } frm.trigger("set_parent_document_type"); + frm.trigger("set_document_type_description"); if (!frm.is_new()) { frm.trigger("create_add_to_dashboard_button"); @@ -67,6 +68,8 @@ frappe.ui.form.on("Number Card", { }, type: function (frm) { + frm.trigger("set_document_type_description"); + if (frm.doc.type == "Report") { frm.set_query("report_name", () => { return { @@ -202,7 +205,9 @@ frappe.ui.form.on("Number Card", { let is_dynamic_filter = (f) => ["Date", "DateRange"].includes(f.fieldtype) && f.default; let wrapper = $(frm.get_field("filters_json").wrapper).empty(); - let table = $(` + let table = $(`
@@ -212,7 +217,10 @@ frappe.ui.form.on("Number Card", {
${__("Filter")}
`).appendTo(wrapper); - $(`

${__("Click table to edit")}

`).appendTo(wrapper); + + if (frm.has_perm("write")) { + $(`

${__("Click table to edit")}

`).appendTo(wrapper); + } let filters = JSON.parse(frm.doc.filters_json || "[]"); let filters_set = false; @@ -273,6 +281,10 @@ frappe.ui.form.on("Number Card", { } table.on("click", () => { + if (!frm.has_perm("write")) { + return; + } + if (!frappe.boot.developer_mode && frm.doc.is_standard) { frappe.throw(__("Cannot edit filters for standard number cards")); } @@ -332,8 +344,9 @@ frappe.ui.form.on("Number Card", { let wrapper = $(frm.get_field("dynamic_filters_json").wrapper).empty(); - frm.dynamic_filter_table = - $(` + frm.dynamic_filter_table = $(`
@@ -360,6 +373,10 @@ frappe.ui.form.on("Number Card", { ); frm.dynamic_filter_table.on("click", () => { + if (!frm.has_perm("write")) { + return; + } + if (!frappe.boot.developer_mode && frm.doc.is_standard) { frappe.throw(__("Cannot edit filters for standard number cards")); } @@ -454,4 +471,20 @@ frappe.ui.form.on("Number Card", { } } }, + + set_document_type_description: function (frm) { + if (frm.doc.type == "Custom") { + frm.set_df_property( + "document_type", + "description", + __( + "This card is visible only to Administrator and System Managers by default. Set a DocType to share with users who have read access.", + null, + "Number Card" + ) + ); + } else { + frm.set_df_property("document_type", "description", ""); + } + }, }); diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json index 39a6f99092..089786fbdc 100644 --- a/frappe/desk/doctype/number_card/number_card.json +++ b/frappe/desk/doctype/number_card/number_card.json @@ -38,7 +38,7 @@ ], "fields": [ { - "depends_on": "eval: doc.type == 'Document Type'", + "depends_on": "eval: ['Document Type', 'Custom'].includes(doc.type)", "fieldname": "document_type", "fieldtype": "Link", "in_list_view": 1, @@ -229,7 +229,7 @@ } ], "links": [], - "modified": "2025-05-21 17:33:04.908518", + "modified": "2025-09-17 21:00:11.351605", "modified_by": "Administrator", "module": "Desk", "name": "Number Card", diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index 2fb76958dc..29a3e144bf 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -7,9 +7,10 @@ from frappe.boot import get_allowed_report_names from frappe.model.document import Document from frappe.model.naming import append_number_if_name_exists from frappe.modules.export_file import export_to_files +from frappe.permissions import get_doctypes_with_read from frappe.query_builder import Criterion from frappe.query_builder.utils import DocType -from frappe.utils import cint, flt +from frappe.utils import flt from frappe.utils.modules import get_modules_from_all_apps_for_user @@ -78,51 +79,43 @@ class NumberCard(Document): def get_permission_query_conditions(user=None): - if not user: - user = frappe.session.user - - if user == "Administrator": + # The user param is ignored because `get_allowed_report_names` and `get_doctypes_with_read` don't support it. + if frappe.session.user == "Administrator": return - roles = frappe.get_roles(user) - if "System Manager" in roles: - return None + if "System Manager" in frappe.get_roles(): + return - doctype_condition = False - module_condition = False + allowed_reports = get_allowed_report_names() + allowed_doctypes = get_doctypes_with_read() + allowed_modules = [module.get("module_name") for module in get_modules_from_all_apps_for_user()] - allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()] - allowed_modules = [ - frappe.db.escape(module.get("module_name")) for module in get_modules_from_all_apps_for_user() - ] + nc = frappe.qb.DocType("Number Card") + conditions = ( + ((nc.type == "Report") & nc.report_name.isin(allowed_reports)) + | ((nc.type == "Custom") & nc.document_type.isin(allowed_doctypes)) + | ((nc.type == "Document Type") & nc.document_type.isin(allowed_doctypes)) + ) & (nc.module.isin(allowed_modules) | nc.module.isnull() | nc.module == "") - if allowed_doctypes: - doctype_condition = "`tabNumber Card`.`document_type` in ({allowed_doctypes})".format( - allowed_doctypes=",".join(allowed_doctypes) - ) - if allowed_modules: - module_condition = """`tabNumber Card`.`module` in ({allowed_modules}) - or `tabNumber Card`.`module` is NULL""".format(allowed_modules=",".join(allowed_modules)) - - return f""" - {doctype_condition} - and - {module_condition} - """ + return conditions.get_sql(quote_char="`") def has_permission(doc, ptype, user): - roles = frappe.get_roles(user) - if "System Manager" in roles: + # The user param is ignored because `get_allowed_report_names` and `get_doctypes_with_read` don't support it. + if frappe.session.user == "Administrator": return True - if doc.type == "Report": - if doc.report_name in get_allowed_report_names(): - return True - else: - allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read()) - if doc.document_type in allowed_doctypes: - return True + if "System Manager" in frappe.get_roles(): + return True + + if doc.type == "Report" and doc.report_name in get_allowed_report_names(): + return True + + if doc.type == "Custom" and doc.document_type in get_doctypes_with_read(): + return True + + if doc.type == "Document Type" and doc.document_type in get_doctypes_with_read(): + return True return False From 38fbe65d093917c3bab028c9fe865786c2090ec2 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Thu, 18 Sep 2025 01:24:17 +0530 Subject: [PATCH 21/30] fix: call `format()` on the result of `_()` (#34016) Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com> --- frappe/desk/query_report.py | 5 ++--- frappe/desk/reportview.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index b20cc92292..22f85df4ba 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -339,9 +339,8 @@ def export_query(): ) frappe.msgprint( _( - "Your report is being generated in the background. " - "You will receive an email on {0} with a download link once it is ready.".format(user_email) - ) + "Your report is being generated in the background. You will receive an email on {0} with a download link once it is ready." + ).format(user_email) ) return diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index ce708bfca1..d143f1bf27 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -394,9 +394,8 @@ def export_query(): frappe.msgprint( _( - "Your report is being generated in the background. " - "You will receive an email on {0} with a download link once it is ready.".format(user_email) - ) + "Your report is being generated in the background. You will receive an email on {0} with a download link once it is ready." + ).format(user_email) ) return From a716b3ea205b6d56f436e770ce26f278e3eeb0f1 Mon Sep 17 00:00:00 2001 From: Shreyas Sojitra Date: Thu, 18 Sep 2025 13:54:10 +0530 Subject: [PATCH 22/30] fix: validate mandatory fields before applying action Previously, workflow actions bypassed mandatory field validation since only workflow_state was updated. This patch ensures required fields are validated before transitions. Additionally, check_mandatory has been moved to frappe.ui.form for reuse and references updated accordingly. --- frappe/public/js/frappe/form/save.js | 222 +++++++++++------------ frappe/public/js/frappe/form/workflow.js | 2 + 2 files changed, 113 insertions(+), 111 deletions(-) diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index fe412aa241..b28e277a86 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -17,7 +17,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) { var save = function () { $(frm.wrapper).addClass("validated-form"); - if ((action !== "Save" || frm.is_dirty()) && check_mandatory()) { + if ((action !== "Save" || frm.is_dirty()) && frappe.ui.form.check_mandatory(frm)) { _call({ method: "frappe.desk.form.save.savedocs", args: { doc: frm.doc, action: action }, @@ -65,116 +65,6 @@ frappe.ui.form.save = function (frm, action, callback, btn) { }); }; - var check_mandatory = function () { - var has_errors = false; - frm.scroll_set = false; - - if (frm.doc.docstatus == 2) return true; // don't check for cancel - - $.each(frappe.model.get_all_docs(frm.doc), function (i, doc) { - var error_fields = []; - var folded = false; - - $.each(frappe.meta.docfield_list[doc.doctype] || [], function (i, docfield) { - if (docfield.fieldname) { - const df = frappe.meta.get_docfield(doc.doctype, docfield.fieldname, doc.name); - - if (df.fieldtype === "Fold") { - folded = frm.layout.folded; - } - - if ( - is_docfield_mandatory(doc, df) && - !frappe.model.has_value(doc.doctype, doc.name, df.fieldname) - ) { - has_errors = true; - error_fields[error_fields.length] = __(df.label, null, df.parent); - // scroll to field - if (!frm.scroll_set) { - scroll_to(doc.parentfield || df.fieldname); - } - - if (folded) { - frm.layout.unfold(); - folded = false; - } - } - } - }); - - if (frm.is_new() && frm.meta.autoname === "Prompt" && !frm.doc.__newname) { - has_errors = true; - error_fields = [__("Name"), ...error_fields]; - } - - if (error_fields.length) { - let meta = frappe.get_meta(doc.doctype); - let message; - if (meta.istable) { - const table_field = frappe.meta.docfield_map[doc.parenttype][doc.parentfield]; - - const table_label = __( - table_field.label || frappe.unscrub(table_field.fieldname) - ).bold(); - - message = __("Mandatory fields required in table {0}, Row {1}", [ - table_label, - doc.idx, - ]); - } else { - message = __("Mandatory fields required in {0}", [__(doc.doctype)]); - } - message = message + "

  • " + error_fields.join("
  • ") + "
"; - frappe.msgprint({ - message: message, - indicator: "red", - title: __("Missing Fields"), - }); - frm.refresh(); - } - }); - - return !has_errors; - }; - - let is_docfield_mandatory = function (doc, df) { - if (df.reqd) return true; - if (!df.mandatory_depends_on || !doc) return; - - let out = null; - let expression = df.mandatory_depends_on; - let parent = frappe.get_meta(df.parent); - - if (typeof expression === "boolean") { - out = expression; - } else if (typeof expression === "function") { - out = expression(doc); - } else if (expression.substr(0, 5) == "eval:") { - try { - out = frappe.utils.eval(expression.substr(5), { doc, parent }); - if (parent && parent.istable && expression.includes("is_submittable")) { - out = true; - } - } catch (e) { - frappe.throw(__('Invalid "mandatory_depends_on" expression')); - } - } else { - var value = doc[expression]; - if ($.isArray(value)) { - out = !!value.length; - } else { - out = !!value; - } - } - - return out; - }; - - const scroll_to = (fieldname) => { - frm.scroll_to_field(fieldname); - frm.scroll_set = true; - }; - var _call = function (opts) { // opts = { // method: "some server method", @@ -227,6 +117,116 @@ frappe.ui.form.save = function (frm, action, callback, btn) { } }; +frappe.ui.form.check_mandatory = function (frm) { + var has_errors = false; + frm.scroll_set = false; + + if (frm.doc.docstatus == 2) return true; // don't check for cancel + + $.each(frappe.model.get_all_docs(frm.doc), function (i, doc) { + var error_fields = []; + var folded = false; + + $.each(frappe.meta.docfield_list[doc.doctype] || [], function (i, docfield) { + if (docfield.fieldname) { + const df = frappe.meta.get_docfield(doc.doctype, docfield.fieldname, doc.name); + + if (df.fieldtype === "Fold") { + folded = frm.layout.folded; + } + + if ( + is_docfield_mandatory(doc, df) && + !frappe.model.has_value(doc.doctype, doc.name, df.fieldname) + ) { + has_errors = true; + error_fields[error_fields.length] = __(df.label, null, df.parent); + // scroll to field + if (!frm.scroll_set) { + scroll_to(doc.parentfield || df.fieldname); + } + + if (folded) { + frm.layout.unfold(); + folded = false; + } + } + } + }); + + if (frm.is_new() && frm.meta.autoname === "Prompt" && !frm.doc.__newname) { + has_errors = true; + error_fields = [__("Name"), ...error_fields]; + } + + if (error_fields.length) { + let meta = frappe.get_meta(doc.doctype); + let message; + if (meta.istable) { + const table_field = frappe.meta.docfield_map[doc.parenttype][doc.parentfield]; + + const table_label = __( + table_field.label || frappe.unscrub(table_field.fieldname) + ).bold(); + + message = __("Mandatory fields required in table {0}, Row {1}", [ + table_label, + doc.idx, + ]); + } else { + message = __("Mandatory fields required in {0}", [__(doc.doctype)]); + } + message = message + "

  • " + error_fields.join("
  • ") + "
"; + frappe.msgprint({ + message: message, + indicator: "red", + title: __("Missing Fields"), + }); + frm.refresh(); + } + }); + + return !has_errors; + + function is_docfield_mandatory(doc, df) { + if (df.reqd) return true; + if (!df.mandatory_depends_on || !doc) return; + + let out = null; + let expression = df.mandatory_depends_on; + let parent = frappe.get_meta(df.parent); + + if (typeof expression === "boolean") { + out = expression; + } else if (typeof expression === "function") { + out = expression(doc); + } else if (expression.substr(0, 5) == "eval:") { + try { + out = frappe.utils.eval(expression.substr(5), { doc, parent }); + if (parent && parent.istable && expression.includes("is_submittable")) { + out = true; + } + } catch (e) { + frappe.throw(__('Invalid "mandatory_depends_on" expression')); + } + } else { + var value = doc[expression]; + if ($.isArray(value)) { + out = !!value.length; + } else { + out = !!value; + } + } + + return out; + } + + function scroll_to(fieldname) { + frm.scroll_to_field(fieldname); + frm.scroll_set = true; + } +}; + frappe.ui.form.remove_old_form_route = () => { let current_route = frappe.get_route().join("/"); frappe.route_history = frappe.route_history.filter( diff --git a/frappe/public/js/frappe/form/workflow.js b/frappe/public/js/frappe/form/workflow.js index e3532fddc8..a4eaafb873 100644 --- a/frappe/public/js/frappe/form/workflow.js +++ b/frappe/public/js/frappe/form/workflow.js @@ -98,6 +98,7 @@ frappe.ui.form.States = class FormStates { frappe.workflow.get_transitions(this.frm.doc).then((transitions) => { this.frm.page.clear_actions_menu(); + const frm = this.frm; transitions.forEach((d) => { if (frappe.user_roles.includes(d.allowed) && has_approval_access(d)) { added = true; @@ -105,6 +106,7 @@ frappe.ui.form.States = class FormStates { // set the workflow_action for use in form scripts frappe.dom.freeze(); me.frm.selected_workflow_action = d.action; + if (!frappe.ui.form.check_mandatory(frm)) return frappe.dom.unfreeze(); me.frm.script_manager.trigger("before_workflow_action").then(() => { frappe .xcall("frappe.model.workflow.apply_workflow", { From 4ef2a722783d159911b0648b7f1cb32d7b20275a Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:40:38 +0200 Subject: [PATCH 23/30] feat: translate filename of exported report (#34046) --- frappe/desk/query_report.py | 2 +- frappe/desk/reportview.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index de664ea065..5b5c6a2218 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -416,7 +416,7 @@ def _export_query(form_params, csv_params, populate_response=True): if not populate_response: return report_name, file_extension, content - provide_binary_file(report_name, file_extension, content) + provide_binary_file(_(report_name), file_extension, content) def valid_report_name(report_name, suffix): diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index d143f1bf27..b51753bb2b 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -482,7 +482,7 @@ def _export_query(form_params, csv_params, populate_response=True): if not populate_response: return title, file_extension, content - provide_binary_file(title, file_extension, content) + provide_binary_file(_(title), file_extension, content) def append_totals_row(data): From 96a0f8246c5ac29dfa660e5cf4c22052fafe209b Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Thu, 18 Sep 2025 18:43:48 +0530 Subject: [PATCH 24/30] feat: provide an option to show or hide applied filters in report print view --- frappe/public/js/frappe/form/print_utils.js | 16 +++++++++++++++- .../js/frappe/views/reports/print_grid.html | 6 +++--- .../js/frappe/views/reports/query_report.js | 6 ++++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/frappe/public/js/frappe/form/print_utils.js b/frappe/public/js/frappe/form/print_utils.js index 03cf82902f..6c958b2ac5 100644 --- a/frappe/public/js/frappe/form/print_utils.js +++ b/frappe/public/js/frappe/form/print_utils.js @@ -1,4 +1,10 @@ -frappe.ui.get_print_settings = function (pdf, callback, letter_head, pick_columns) { +frappe.ui.get_print_settings = function ( + pdf, + callback, + letter_head, + pick_columns, + has_filters = false +) { var print_settings = locals[":Print Settings"]["Print Settings"]; var company = frappe.defaults.get_default("company"); @@ -47,6 +53,14 @@ frappe.ui.get_print_settings = function (pdf, callback, letter_head, pick_column }, ]; + if (has_filters) { + columns.push({ + label: __("Include filters"), + fieldtype: "Check", + fieldname: "include_filters", + }); + } + if (pick_columns) { columns.push( { diff --git a/frappe/public/js/frappe/views/reports/print_grid.html b/frappe/public/js/frappe/views/reports/print_grid.html index 7af27145a6..1b9dae8d24 100644 --- a/frappe/public/js/frappe/views/reports/print_grid.html +++ b/frappe/public/js/frappe/views/reports/print_grid.html @@ -3,9 +3,9 @@

{{ __(title) }}


{% endif %} -{% if subtitle %} -{{ subtitle }} -
+{% if subtitle && print_settings.include_filters %} + {{ subtitle }} +
{% endif %}
${__("Filter")}
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 7dff1236f5..9cffde3e82 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1785,7 +1785,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { false, (print_settings) => this.print_report(print_settings), this.report_doc.letter_head, - this.get_visible_columns() + this.get_visible_columns(), + true ); this.add_portrait_warning(dialog); }, @@ -1799,7 +1800,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { false, (print_settings) => this.pdf_report(print_settings), this.report_doc.letter_head, - this.get_visible_columns() + this.get_visible_columns(), + true ); this.add_portrait_warning(dialog); From 7ae1691f62a2bc61ea08eafadcb97a22aaa2dc36 Mon Sep 17 00:00:00 2001 From: MochaMind Date: Fri, 19 Sep 2025 00:37:33 +0530 Subject: [PATCH 25/30] fix: German translations --- frappe/locale/de.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/locale/de.po b/frappe/locale/de.po index 5dddcf7ce9..f4bb60cfb6 100644 --- a/frappe/locale/de.po +++ b/frappe/locale/de.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-14 09:32+0000\n" -"PO-Revision-Date: 2025-09-15 18:13\n" +"PO-Revision-Date: 2025-09-18 19:07\n" "Last-Translator: developers@frappe.io\n" "Language-Team: German\n" "MIME-Version: 1.0\n" @@ -9622,7 +9622,7 @@ msgstr "Import-Log exportieren" #: frappe/public/js/frappe/views/reports/report_utils.js:245 msgctxt "Export report" msgid "Export Report: {0}" -msgstr "Exportbericht: {0}" +msgstr "Bericht exportieren: {0}" #: frappe/public/js/frappe/data_import/data_exporter.js:26 msgid "Export Type" @@ -21076,7 +21076,7 @@ msgstr "Neu verknüpfen" #: frappe/core/doctype/communication/communication.js:138 msgid "Relink Communication" -msgstr "Relink Kommunikation" +msgstr "Kommunikation neu verknüpfen" #. Option for the 'Comment Type' (Select) field in DocType 'Comment' #: frappe/core/doctype/comment/comment.json From 1260a0c0e85aeb7897244cd1615b0c1089a01ea6 Mon Sep 17 00:00:00 2001 From: MochaMind Date: Fri, 19 Sep 2025 00:37:36 +0530 Subject: [PATCH 26/30] fix: Serbian (Cyrillic) translations --- frappe/locale/sr.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/locale/sr.po b/frappe/locale/sr.po index 3bfb34e471..b5a0a259fb 100644 --- a/frappe/locale/sr.po +++ b/frappe/locale/sr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-14 09:32+0000\n" -"PO-Revision-Date: 2025-09-15 18:13\n" +"PO-Revision-Date: 2025-09-18 19:07\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Serbian (Cyrillic)\n" "MIME-Version: 1.0\n" @@ -5453,7 +5453,7 @@ msgstr "Контакт" #: frappe/integrations/doctype/google_calendar/google_calendar.py:812 msgid "Contact / email not found. Did not add attendee for -
{0}" -msgstr "" +msgstr "Контакт / имејл није пронађен. Учесник није додат за -
{0}" #. Label of the sb_01 (Section Break) field in DocType 'Contact' #: frappe/contacts/doctype/contact/contact.json @@ -13678,7 +13678,7 @@ msgstr "Примарно" #: frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py:43 msgid "Is Primary Address" -msgstr "" +msgstr "Примарна адреса" #. Label of the is_primary_contact (Check) field in DocType 'Contact' #: frappe/contacts/doctype/contact/contact.json @@ -26297,7 +26297,7 @@ msgstr "Овај месец" #: frappe/core/doctype/file/file.py:394 msgid "This PDF cannot be uploaded as it contains unsafe content." -msgstr "" +msgstr "Овај PDF не може бити отпремљен јер садржи небезбедан садржај." #: frappe/public/js/frappe/ui/filters/filter.js:670 msgid "This Quarter" From 2cb15a1fc35ec0607b002441c292f197bb7d4ee1 Mon Sep 17 00:00:00 2001 From: MochaMind Date: Fri, 19 Sep 2025 00:37:39 +0530 Subject: [PATCH 27/30] fix: Swedish translations --- frappe/locale/sv.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/locale/sv.po b/frappe/locale/sv.po index 8e273eb00d..161618f087 100644 --- a/frappe/locale/sv.po +++ b/frappe/locale/sv.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-14 09:32+0000\n" -"PO-Revision-Date: 2025-09-17 18:30\n" +"PO-Revision-Date: 2025-09-18 19:07\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Swedish\n" "MIME-Version: 1.0\n" @@ -14000,7 +14000,7 @@ msgstr "Håll koll på alla uppdatering flöde" #. Description of a DocType #: frappe/core/doctype/communication/communication.json msgid "Keeps track of all communications" -msgstr "Konversation Översikt" +msgstr "Håller koll på all kommunikation" #. Label of the defkey (Data) field in DocType 'DefaultValue' #. Label of the key (Data) field in DocType 'Document Share Key' From 6af45b87921e632dd5774b4aa6469a34c544f54f Mon Sep 17 00:00:00 2001 From: MochaMind Date: Fri, 19 Sep 2025 00:37:44 +0530 Subject: [PATCH 28/30] fix: Norwegian Bokmal translations --- frappe/locale/nb.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/locale/nb.po b/frappe/locale/nb.po index 0403353f4f..4346c51726 100644 --- a/frappe/locale/nb.po +++ b/frappe/locale/nb.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-14 09:32+0000\n" -"PO-Revision-Date: 2025-09-17 18:30\n" +"PO-Revision-Date: 2025-09-18 19:07\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Norwegian Bokmal\n" "MIME-Version: 1.0\n" @@ -8164,7 +8164,7 @@ msgstr "Last ned flere vCard" #: frappe/desk/page/setup_wizard/install_fixtures.py:46 msgid "Dr" -msgstr "Debet" +msgstr "Dr." #: frappe/public/js/frappe/model/indicator.js:73 #: frappe/public/js/frappe/ui/filters/filter.js:538 @@ -22719,7 +22719,7 @@ msgstr "Søk i en dokumenttype" #: frappe/public/js/frappe/ui/toolbar/navbar.html:29 msgid "Search or type a command ({0})" -msgstr "Søk eller skriv inn en kommando ({0})" +msgstr "Søk eller skriv en kommando ({0})" #: frappe/public/js/form_builder/components/SearchBox.vue:8 msgid "Search properties..." From f4f6e467b0afb8376e68bd8c7a61c07c7fff836c Mon Sep 17 00:00:00 2001 From: MochaMind Date: Fri, 19 Sep 2025 00:37:47 +0530 Subject: [PATCH 29/30] fix: Serbian (Latin) translations --- frappe/locale/sr_CS.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/locale/sr_CS.po b/frappe/locale/sr_CS.po index 105c78dbe7..c0eb5bedb6 100644 --- a/frappe/locale/sr_CS.po +++ b/frappe/locale/sr_CS.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-09-14 09:32+0000\n" -"PO-Revision-Date: 2025-09-15 18:13\n" +"PO-Revision-Date: 2025-09-18 19:07\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Serbian (Latin)\n" "MIME-Version: 1.0\n" @@ -5454,7 +5454,7 @@ msgstr "Kontakt" #: frappe/integrations/doctype/google_calendar/google_calendar.py:812 msgid "Contact / email not found. Did not add attendee for -
{0}" -msgstr "" +msgstr "Kontakt / imejl nije pronađen. Učesnik nije dodat za -
{0}" #. Label of the sb_01 (Section Break) field in DocType 'Contact' #: frappe/contacts/doctype/contact/contact.json @@ -13679,7 +13679,7 @@ msgstr "Primarno" #: frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py:43 msgid "Is Primary Address" -msgstr "" +msgstr "Primarna adresa" #. Label of the is_primary_contact (Check) field in DocType 'Contact' #: frappe/contacts/doctype/contact/contact.json @@ -26298,7 +26298,7 @@ msgstr "Ovaj mesec" #: frappe/core/doctype/file/file.py:394 msgid "This PDF cannot be uploaded as it contains unsafe content." -msgstr "" +msgstr "Ovaj PDF ne može biti otpremljen jer sadrži nebezbedan sadržaj." #: frappe/public/js/frappe/ui/filters/filter.js:670 msgid "This Quarter" From 27f2f49c56e1f3d4e3f0a1920f8780d184d2dcd7 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Fri, 19 Sep 2025 11:06:23 +0530 Subject: [PATCH 30/30] fix: include filters only if checked --- frappe/public/js/frappe/views/reports/print_grid.html | 2 +- frappe/public/js/frappe/views/reports/query_report.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/print_grid.html b/frappe/public/js/frappe/views/reports/print_grid.html index 1b9dae8d24..8e0f31280b 100644 --- a/frappe/public/js/frappe/views/reports/print_grid.html +++ b/frappe/public/js/frappe/views/reports/print_grid.html @@ -3,7 +3,7 @@

{{ __(title) }}


{% endif %} -{% if subtitle && print_settings.include_filters %} +{% if subtitle %} {{ subtitle }}
{% endif %} diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 9cffde3e82..18208f1492 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1495,7 +1495,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { frappe.render_grid({ template: print_settings.columns ? "print_grid" : custom_format, title: __(this.report_name), - subtitle: filters_html, + subtitle: print_settings?.include_filters ? filters_html : null, print_settings: print_settings, landscape: landscape, filters: this.get_filter_values(), @@ -1525,7 +1525,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { const template = print_settings.columns || !custom_format ? "print_grid" : custom_format; const content = frappe.render_template(template, { title: __(this.report_name), - subtitle: filters_html, + subtitle: print_settings?.include_filters ? filters_html : null, filters: applied_filters, data: data, original_data: this.data,