diff --git a/frappe/auth.py b/frappe/auth.py index 5a447a99af..1658930317 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -253,7 +253,11 @@ class LoginManager: ): return - clear_sessions(frappe.session.user, keep_current=True) + clear_sessions( + frappe.session.user, + keep_current=True, + force=frappe.session.user != "Administrator", + ) def authenticate(self, user: str | None = None, pwd: str | None = None): from frappe.core.doctype.user.user import User diff --git a/frappe/commands/test_commands.py b/frappe/commands/test_commands.py index bd9697d599..f3d9f3d0d9 100644 --- a/frappe/commands/test_commands.py +++ b/frappe/commands/test_commands.py @@ -1116,6 +1116,7 @@ class TestGunicornWorker(IntegrationTestCase): time.sleep(2) execute_in_shell("pgrep gunicorn | xargs -L1 kill -9") + @unittest.skip("Flaky test") def test_gunicorn_ping_sync(self): self.spawn_gunicorn() path = f"http://{self.TEST_SITE}:{self.port}/api/method/ping" @@ -1126,6 +1127,7 @@ class TestGunicornWorker(IntegrationTestCase): path = f"http://{self.TEST_SITE}:{self.port}/api/method/ping" self.assertEqual(requests.get(path).status_code, 200) + @unittest.skip("Flaky test") def test_gunicorn_idle_cpu_usage(self): def get_total_usage(): process = psutil.Process(self.handle.pid) diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 59aefc53bf..16dae174ec 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -419,7 +419,7 @@ class Communication(Document, CommunicationEmailMixin): # Skip timeline links if a "Sent" communication already exists # else will create duplicate timeline entries if self.sent_or_received == "Received" and self.find_one_by_filters( - message_id=self.message_id, sent_or_received="Sent" + message_id=self.message_id, email_account=self.email_account, sent_or_received="Sent" ): return diff --git a/frappe/core/doctype/file/file.js b/frappe/core/doctype/file/file.js index f3f1380855..2dd4bb48a2 100644 --- a/frappe/core/doctype/file/file.js +++ b/frappe/core/doctype/file/file.js @@ -43,25 +43,34 @@ frappe.ui.form.on("File", { if (!frappe.utils.can_upload_public_files() && frm.doc.is_private) { frm.set_df_property("is_private", "read_only", 1); } + + if (frm.doc.attached_to_name) { + const field = frm.get_field("attached_to_name"); + field.$input_wrapper + .find(".control-value") + .html(`${frappe.utils.get_form_link(frm.doctype, frm.docname, true)}`); + } }, preview_file: function (frm) { let $preview = ""; let file_extension = frm.doc.file_type.toLowerCase(); + const full_file_url = frm.doc.file_url + "?fid=" + frm.doc.name; + const src_url = frappe.utils.escape_html(full_file_url); - if (frappe.utils.is_image_file(frm.doc.file_url)) { + if (frappe.utils.is_image_file(full_file_url)) { $preview = $(`
`); - } else if (frappe.utils.is_video_file(frm.doc.file_url)) { + } else if (frappe.utils.is_video_file(full_file_url)) { $preview = $(`
`); @@ -72,14 +81,14 @@ frappe.ui.form.on("File", { style="background:#323639;" width="100%" height="1190" - src="${frappe.utils.escape_html(frm.doc.file_url)}" type="application/pdf" + src="${src_url}" type="application/pdf" > `); } else if (file_extension === "mp3") { $preview = $(`
`); diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index c1aa3fb0d7..75d1bd978a 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -248,7 +248,6 @@ }, { "default": "0", - "description": "Note: Multiple sessions will be allowed in case of mobile device", "fieldname": "deny_multiple_sessions", "fieldtype": "Check", "label": "Allow only one session per user" @@ -790,7 +789,7 @@ "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2026-01-02 18:13:45.430712", + "modified": "2026-02-24 14:27:04.763075", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 29d9cb6b0f..1424e2a3d8 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -76,6 +76,18 @@ class Workspace(Document): if self.public and not is_workspace_manager() and not disable_saving_as_public(): frappe.throw(_("You need to be Workspace Manager to edit this document")) + + if ( + not self.public + and self.for_user + and self.for_user != frappe.session.user + and not is_workspace_manager() + ): + frappe.throw( + _("You are not allowed to edit this workspace"), + frappe.PermissionError, + ) + if self.has_value_changed("title"): validate_route_conflict(self.doctype, self.title) else: diff --git a/frappe/desk/page/desktop/desktop.css b/frappe/desk/page/desktop/desktop.css index 27a128b60c..ef5e4e6062 100644 --- a/frappe/desk/page/desktop/desktop.css +++ b/frappe/desk/page/desktop/desktop.css @@ -2,7 +2,7 @@ --desktop-blur: blur(10.2px); --desktop-modal-width: 590px; --desktop-modal-height: 450px; - --folder-thumbnail-icon-height: 12px; + --folder-thumbnail-icon-height: 16px; --desktop-icon-dimension: 54px; --folder-icon-background-color: var(--surface-gray-1); --desktop-modal-radius: 30px; @@ -91,7 +91,7 @@ padding:0px; margin: 0px; height: 100%; - overflow: auto; + overflow: hidden; } .icons{ gap: 16px; @@ -109,6 +109,10 @@ gap: 12px; padding: 13px 16px 12px 16px; position: relative; + border-radius: 20px; + border-width: 1px; + border-style: dashed; + border-color: transparent; } .desktop-icon.desktop-edit-mode .hide-button { display: flex; @@ -129,6 +133,7 @@ .icon-container{ padding: 10px; border-radius: 16px; + overflow: hidden; display: flex; align-items: center; justify-content: center; @@ -254,13 +259,13 @@ position: absolute; } -.folder-icon{ - border-radius: 10px; - background-color: var(--folder-icon-background-color) !important; +.folder-icon { + border-radius: 16px; + background-color: var(--folder-icon-background-color) !important; box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.14); padding: 7px; align-items: normal; - box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.14); + /* box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.14); */ & .icons{ gap: 2.1px; margin-top: 0px; @@ -343,8 +348,7 @@ } .desktop-edit-mode{ - border: 1px dashed var(--outline-gray-2); - border-radius: 20px; + border-color: var(--outline-gray-2); } .edit-mode-buttons{ display: none; @@ -364,7 +368,7 @@ :root { --desktop-icon-dimension: 50px; --desktop-icon-container: 117px; - --folder-thumbnail-icon-height:17px; + --folder-thumbnail-icon-height:15px; } .desktop-container { @@ -443,6 +447,12 @@ } } +.icons-container { + > .icons-container { + padding: 0px; + } +} + .desktop-edit{ width: 36px; height: 36px; diff --git a/frappe/desk/page/desktop/desktop.js b/frappe/desk/page/desktop/desktop.js index 2690d5dd74..514af59374 100644 --- a/frappe/desk/page/desktop/desktop.js +++ b/frappe/desk/page/desktop/desktop.js @@ -286,7 +286,6 @@ class DesktopPage { this.setup_navbar(); this.setup_awesomebar(); this.handle_route_change(); - this.setup_edit_button(); } setup_edit_button() { if (this.edit_mode || frappe.is_mobile()) return; @@ -1087,11 +1086,6 @@ class DesktopIcon { this.folder_grid = new DesktopIconGrid({ wrapper: this.folder_wrapper, icons_data: this.child_icons, - row_size: 3, - page_size: { - row: 3, - col: 3, - }, in_folder: true, in_modal: false, no_dragging: true, diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py index b50dd69b34..810e87a424 100644 --- a/frappe/email/doctype/email_account/test_email_account.py +++ b/frappe/email/doctype/email_account/test_email_account.py @@ -521,12 +521,14 @@ class TestInboundMail(IntegrationTestCase): def test_mail_exist_validation(self): """Do not create communication record if the mail is already downloaded into the system.""" + email_account = frappe.get_doc("Email Account", "_Test Email Account 1") mail_content = self.get_test_mail(fname="incoming-1.raw") message_id = Email(mail_content).message_id # Create new communication record in DB - communication = self.new_communication(message_id=message_id, sent_or_received="Received") + communication = self.new_communication( + message_id=message_id, email_account=email_account.name, sent_or_received="Received" + ) - email_account = frappe.get_doc("Email Account", "_Test Email Account 1") inbound_mail = InboundMail(mail_content, email_account, 12345, 1) new_communication = inbound_mail.process() diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index b0576dfe38..ca05ec6278 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -514,7 +514,18 @@ def send_now(name: str | int, force_send: bool = False): @frappe.whitelist() def toggle_sending(enable: bool | int | str): frappe.only_for("System Manager") - frappe.db.set_default("suspend_email_queue", 0 if sbool(enable) else 1) + suspend_value = 0 if sbool(enable) else 1 + frappe.db.set_default("suspend_email_queue", suspend_value) + + action = "Resumed" if suspend_value == 0 else "Suspended" + frappe.get_doc( + { + "doctype": "Activity Log", + "user": frappe.session.user, + "status": "Success", + "subject": f"Email Queue sending {action.lower()}", + } + ).insert(ignore_permissions=True, ignore_links=True) def on_doctype_update(): diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 81e539d706..45d587a69a 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -751,7 +751,10 @@ class InboundMail(Email): return return Communication.find_one_by_filters( - message_id=self.message_id, sent_or_received="Received", order_by="creation DESC" + message_id=self.message_id, + email_account=self.email_account.name, + sent_or_received="Received", + order_by="creation DESC", ) def is_sender_same_as_receiver(self): diff --git a/frappe/locale/bs.po b/frappe/locale/bs.po index c0467055e8..f05ddd2f43 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: 2026-02-22 09:42+0000\n" -"PO-Revision-Date: 2026-02-23 22:07\n" +"PO-Revision-Date: 2026-02-25 23:15\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Bosnian\n" "MIME-Version: 1.0\n" @@ -2870,7 +2870,7 @@ msgstr "Dodjela je Završena" #. Label of the assignment_days (Table) field in DocType 'Assignment Rule' #: frappe/automation/doctype/assignment_rule/assignment_rule.json msgid "Assignment Days" -msgstr "Dani Dodjeljivanja" +msgstr "Dani Dodjele" #. Name of a DocType #. Label of the assignment_rule (Link) field in DocType 'ToDo' @@ -2888,7 +2888,7 @@ msgstr "Dan Dodjele Pravila" #. Name of a DocType #: frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json msgid "Assignment Rule User" -msgstr "Korisnik Dodjele Pravila" +msgstr "Korisnik Pravila Dodjele" #: frappe/automation/doctype/assignment_rule/assignment_rule.py:55 msgid "Assignment Rule is not allowed on document type {0}" diff --git a/frappe/locale/fa.po b/frappe/locale/fa.po index cd633e0b2d..8862e8e299 100644 --- a/frappe/locale/fa.po +++ b/frappe/locale/fa.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2026-02-22 09:42+0000\n" -"PO-Revision-Date: 2026-02-23 22:07\n" +"PO-Revision-Date: 2026-02-25 23:14\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Persian\n" "MIME-Version: 1.0\n" @@ -6314,7 +6314,7 @@ msgstr "سفارشی‌سازی" #: frappe/custom/doctype/customize_form/customize_form.js:89 msgid "Customize Child Table" -msgstr "سفارشی کردن جدول فرزند" +msgstr "سفارشی‌سازی جدول فرزند" #: frappe/public/js/frappe/views/dashboard/dashboard_view.js:38 msgid "Customize Dashboard" @@ -6339,7 +6339,7 @@ msgstr "سفارشی‌سازی فرم - {0}" #. Name of a DocType #: frappe/custom/doctype/customize_form_field/customize_form_field.json msgid "Customize Form Field" -msgstr "سفارشی کردن فیلد فرم" +msgstr "سفارشی‌سازی فیلد فرم" #: frappe/public/js/frappe/list/list_view.js:1994 msgctxt "Customize qucik filters of List View" @@ -18808,7 +18808,7 @@ msgstr "" #: frappe/core/doctype/doctype/doctype.py:1699 msgid "Options for Rating field can range from 3 to 10" -msgstr "گزینه‌های فیلد رتبه بندی می‌تواند از 3 تا 10 باشد" +msgstr "گزینه‌های فیلد رتبه‌بندی می‌تواند از 3 تا 10 باشد" #: frappe/custom/doctype/custom_field/custom_field.js:96 msgid "Options for select. Each option on a new line." diff --git a/frappe/locale/hr.po b/frappe/locale/hr.po index 9f534aaf36..8851d800e9 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: 2026-02-22 09:42+0000\n" -"PO-Revision-Date: 2026-02-23 22:07\n" +"PO-Revision-Date: 2026-02-25 23:14\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Croatian\n" "MIME-Version: 1.0\n" @@ -2870,7 +2870,7 @@ msgstr "Dodjela je Završena" #. Label of the assignment_days (Table) field in DocType 'Assignment Rule' #: frappe/automation/doctype/assignment_rule/assignment_rule.json msgid "Assignment Days" -msgstr "Dani Dodjeljivanja" +msgstr "Dani Dodjele" #. Name of a DocType #. Label of the assignment_rule (Link) field in DocType 'ToDo' diff --git a/frappe/locale/sv.po b/frappe/locale/sv.po index 0b77050332..7ad41225a0 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: 2026-02-22 09:42+0000\n" -"PO-Revision-Date: 2026-02-23 22:07\n" +"PO-Revision-Date: 2026-02-25 23:14\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Swedish\n" "MIME-Version: 1.0\n" @@ -2868,7 +2868,7 @@ msgstr "Tilldelning Klar" #. Label of the assignment_days (Table) field in DocType 'Assignment Rule' #: frappe/automation/doctype/assignment_rule/assignment_rule.json msgid "Assignment Days" -msgstr "Automation Dagar" +msgstr "Tilldelning Dagar" #. Name of a DocType #. Label of the assignment_rule (Link) field in DocType 'ToDo' @@ -2876,7 +2876,7 @@ msgstr "Automation Dagar" #: frappe/automation/doctype/assignment_rule/assignment_rule.json #: frappe/desk/doctype/todo/todo.json frappe/workspace_sidebar/automation.json msgid "Assignment Rule" -msgstr "Automation Regel" +msgstr "Tilldelning Regel" #. Name of a DocType #: frappe/automation/doctype/assignment_rule_day/assignment_rule_day.json @@ -2890,13 +2890,13 @@ msgstr "Automation Regel Användare" #: frappe/automation/doctype/assignment_rule/assignment_rule.py:55 msgid "Assignment Rule is not allowed on document type {0}" -msgstr "Automation Regel är ej tillåten på dokument typ {0}" +msgstr "Tilldelning Regel är ej tillåten på dokument typ {0}" #. Label of the assignment_rules_section (Section Break) field in DocType #. 'Assignment Rule' #: frappe/automation/doctype/assignment_rule/assignment_rule.json msgid "Assignment Rules" -msgstr "Automation Regler" +msgstr "Tilldelning Regler" #: frappe/desk/doctype/notification_log/notification_log.py:153 msgid "Assignment Update on {0}" diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 60df4c279a..858e5212d0 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -100,9 +100,6 @@ def delete_doc( else: return False - # delete passwords - delete_all_passwords_for(doctype, name) - doc = None if doctype == "DocType": if for_reload: @@ -200,6 +197,9 @@ def delete_doc( enqueue_after_commit=True, ) + # delete passwords + delete_all_passwords_for(doctype, name) + # clear cache for Document doc.clear_cache() # delete global search entry diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 9e5624252d..add084d38e 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -793,11 +793,21 @@ class Meta(Document): group.get("items").append(doctype) link.added = True + # Add fieldname to transaction group for external links + if not link.is_child_table: + if "fieldnames" not in group: + group["fieldnames"] = {} + group["fieldnames"][link.link_doctype] = link.link_fieldname + if not link.added: # group not found, make a new group - data.transactions.append( - dict(label=link.group, items=[link.parent_doctype or link.link_doctype]) - ) + new_group = dict(label=link.group, items=[link.parent_doctype or link.link_doctype]) + + # Add fieldname to new transaction group for external links + if not link.is_child_table: + new_group["fieldnames"] = {link.link_doctype: link.link_fieldname} + + data.transactions.append(new_group) if not data.fieldname and link.link_fieldname: data.fieldname = link.link_fieldname diff --git a/frappe/oauth.py b/frappe/oauth.py index 5e170db5d2..67595555f9 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -2,13 +2,14 @@ import base64 import datetime import hashlib import re -from http import cookies -from urllib.parse import unquote, urljoin, urlparse +from urllib.parse import urljoin, urlparse +from oauthlib.common import Request from oauthlib.openid import RequestValidator import frappe from frappe.auth import LoginManager +from frappe.integrations.doctype.oauth_client.oauth_client import OAuthClient from frappe.utils.data import cstr, get_system_timezone, now_datetime @@ -73,13 +74,11 @@ class OAuthWebRequestValidator(RequestValidator): # Post-authorization def save_authorization_code(self, client_id, code, request, *args, **kwargs): - cookie_dict = get_cookie_dict_from_headers(request) - oac = frappe.new_doc("OAuth Authorization Code") oac.scopes = get_url_delimiter().join(request.scopes) oac.redirect_uri_bound_to_authorization_code = request.redirect_uri oac.client = client_id - oac.user = unquote(cookie_dict["user_id"].value) + oac.user = frappe.session.user oac.authorization_code = code["code"] if request.nonce: @@ -92,43 +91,32 @@ class OAuthWebRequestValidator(RequestValidator): oac.save(ignore_permissions=True) frappe.db.commit() - def authenticate_client(self, request, *args, **kwargs): + def authenticate_client(self, request: Request, *args, **kwargs) -> bool | None: + """ + Loads the client based on request parameters and sets in oauth request. + Returns True on success, None on error. + """ # Get ClientID in URL if request.client_id: - oc = frappe.get_doc("OAuth Client", request.client_id) + client_name = request.client_id else: # Extract token, instantiate OAuth Bearer Token and use clientid from there. if "refresh_token" in frappe.form_dict: - oc = frappe.get_doc( - "OAuth Client", - frappe.db.get_value( - "OAuth Bearer Token", - {"refresh_token": frappe.form_dict["refresh_token"]}, - "client", - ), - ) + token_filters = {"refresh_token": frappe.form_dict["refresh_token"]} elif "token" in frappe.form_dict: - oc = frappe.get_doc( - "OAuth Client", - frappe.db.get_value("OAuth Bearer Token", frappe.form_dict["token"], "client"), - ) + token_filters = {"name": frappe.form_dict["token"]} else: - oc = frappe.get_doc( - "OAuth Client", - frappe.db.get_value( - "OAuth Bearer Token", - frappe.get_request_header("Authorization").split(" ")[1], - "client", - ), - ) + token_filters = {"name": frappe.get_request_header("Authorization").split(" ")[1]} + + client_name = frappe.db.get_value("OAuth Bearer Token", filters=token_filters, fieldname="client") + + oc: OAuthClient = frappe.get_doc("OAuth Client", client_name) try: request.client = request.client or oc.as_dict() except Exception as e: return generate_json_error_response(e) - cookie_dict = get_cookie_dict_from_headers(request) - user_id = unquote(cookie_dict.get("user_id").value) if "user_id" in cookie_dict else "Guest" - return frappe.session.user == user_id + return True def authenticate_client_id(self, client_id, request, *args, **kwargs): cli_id = frappe.db.get_value("OAuth Client", client_id, "name") @@ -506,13 +494,6 @@ class OAuthWebRequestValidator(RequestValidator): return True -def get_cookie_dict_from_headers(r): - cookie = cookies.BaseCookie() - if r.headers.get("Cookie"): - cookie.load(r.headers.get("Cookie")) - return cookie - - def calculate_at_hash(access_token, hash_alg): """Helper method for calculating an access token hash, as described in http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken diff --git a/frappe/printing/doctype/letter_head/letter_head.json b/frappe/printing/doctype/letter_head/letter_head.json index 9855c0ff32..4ddafc47db 100644 --- a/frappe/printing/doctype/letter_head/letter_head.json +++ b/frappe/printing/doctype/letter_head/letter_head.json @@ -98,6 +98,7 @@ "description": "Letter Head in HTML", "fieldname": "content", "fieldtype": "HTML Editor", + "ignore_xss_filter": 1, "label": "Header HTML", "oldfieldname": "content", "oldfieldtype": "Text Editor" @@ -113,6 +114,7 @@ "description": "Footer will display correctly only in PDF", "fieldname": "footer", "fieldtype": "HTML Editor", + "ignore_xss_filter": 1, "label": "Footer HTML" }, { @@ -184,6 +186,7 @@ { "collapsible": 1, "collapsible_depends_on": "eval: doc.header_script || doc.footer_script", + "depends_on": "eval: !doc.__islocal", "fieldname": "scripts_section", "fieldtype": "Section Break", "label": "Scripts" @@ -200,7 +203,7 @@ "links": [], "make_attachments_public": 1, "max_attachments": 3, - "modified": "2024-04-12 10:30:25.793932", + "modified": "2026-02-24 20:53:14.297567", "modified_by": "Administrator", "module": "Printing", "name": "Letter Head", @@ -223,8 +226,9 @@ "role": "Desk User" } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index f2f69e3aa3..0e5d68c165 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -706,7 +706,10 @@ frappe.ui.form.PrintView = class { return; } } else { - this.is_wkhtmltopdf_valid(); + let pdf_generator = this.get_pdf_generator(print_format?.pdf_generator); + if (pdf_generator === "wkhtmltopdf") { + this.is_wkhtmltopdf_valid(); + } this.render_page( "/api/method/frappe.utils.print_format.download_pdf?", false, @@ -738,9 +741,7 @@ frappe.ui.form.PrintView = class { encodeURIComponent(this.get_letterhead()) + "&settings=" + encodeURIComponent(JSON.stringify(this.additional_settings)) + - (this.lang_code ? "&_lang=" + this.lang_code : "") + - "&pdf_generator=" + - encodeURIComponent(pdf_generator) + (this.lang_code ? "&_lang=" + this.lang_code : "") ) ); if (!w) { diff --git a/frappe/public/js/billing.bundle.js b/frappe/public/js/billing.bundle.js index 00fd226e1a..79e0609c58 100644 --- a/frappe/public/js/billing.bundle.js +++ b/frappe/public/js/billing.bundle.js @@ -27,6 +27,14 @@ $(document).ready(function () { dismiss_key: `${frappe.boot.site_info.name}_trial_card_time`, dismiss_it_for: "day", }; + let visiblity_condition = + frappe.boot.is_fc_site && + !!frappe.boot.setup_complete && + !frappe.is_mobile() && + frappe.user.has_role("System Manager"); + if (visiblity_condition && isFCUser) { + addChatBubble(); + } if (isFCUser) { $.extend(card_args, { primary_action_label: "Upgrade", @@ -42,12 +50,7 @@ $(document).ready(function () { }); } $(document).on("desktop_screen", function (event, data) { - if ( - frappe.boot.is_fc_site && - !!frappe.boot.setup_complete && - !frappe.is_mobile() && - frappe.user.has_role("System Manager") - ) { + if (visiblity_condition) { if (site_info.trial_end_date && trial_end_date > new Date()) { card_args.parent = $(".icons-container").first(); let banner_card = new frappe.ui.SidebarCard(card_args); @@ -84,3 +87,25 @@ function openFrappeCloudDashboard() { "_blank" ); } + +function addChatBubble() { + const all_apps = frappe.utils.get_installed_apps(); + const desk_apps = ["erpnext", "hrms"]; + + const apps_allowed = frappe.utils.is_sub_array(all_apps, desk_apps); + if (checkBusinessHours && apps_allowed) { + let chat_banner = document.createElement("script"); + chat_banner.innerHTML = + '(function(d,t){var BASE_URL="https://chat.frappe.cloud";var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src=BASE_URL+"/packs/js/sdk.js";g.async=true;s.parentNode.insertBefore(g,s);g.onload=function(){window.chatwootSDK.run({websiteToken:"LdmfJzftdJGEcFjoTqk8CrSq",baseUrl:BASE_URL})}})(document,"script");'; + document.body.append(chat_banner); + const root = document.documentElement; + root.style.setProperty("--s-700", "var(--gray-500)"); + } +} + +function checkBusinessHours() { + let currentTime = new Date(); + const istTime = new Date(currentTime.toLocaleString("en-US", { timeZone: "Asia/Kolkata" })); + + return istTime.getHours() >= 11 && istTime.getHours() < 18; +} diff --git a/frappe/public/js/bootstrap-4-web.bundle.js b/frappe/public/js/bootstrap-4-web.bundle.js index 3845a7b185..0d37b745a7 100644 --- a/frappe/public/js/bootstrap-4-web.bundle.js +++ b/frappe/public/js/bootstrap-4-web.bundle.js @@ -25,7 +25,7 @@ frappe.get_modal = function (title, content) { {% for (let j=0; j < transactions[i].items.length; j++) { %} {% let doctype = transactions[i].items[j]; %} + {% let fieldname = (transactions[i].fieldnames && transactions[i].fieldnames[doctype]) || transactions[i].fieldname; %} @@ -296,8 +297,11 @@ function markReset(step) {
- - {{ step.action_label }} + + {{ __(step.action_label) }}
@@ -305,7 +309,7 @@ function markReset(step) { class="text-base onb-step-text" style="text-decoration-line: line-through" > - {{ step.action_label }} + {{ __(step.action_label) }}
diff --git a/frappe/public/js/frappe/ui/user_onboarding/user_onboarding.bundle.js b/frappe/public/js/frappe/ui/user_onboarding/user_onboarding.bundle.js index 86a994519e..3ed2215733 100644 --- a/frappe/public/js/frappe/ui/user_onboarding/user_onboarding.bundle.js +++ b/frappe/public/js/frappe/ui/user_onboarding/user_onboarding.bundle.js @@ -67,7 +67,7 @@ function addStyles() { position: fixed; right: 24px; bottom: 24px; - width: 380px; + width: 310px; max-height: 80vh; background: #fff; border-radius: 16px; @@ -215,6 +215,46 @@ function addStyles() { color: #6b7280; font-size: 14px; } + + [data-theme="dark"] .onb-panel { + background-color: #232323; + color: #e5e7eb; + box-shadow: 0 12px 40px rgba(0,0,0,0.6); + } + + [data-theme="dark"] .text-base { + color: #e5e7eb; + } + + [data-theme="dark"] .onb-skip { + color: #9ca3af; + } + + [data-theme="dark"] .onb-skip:hover { + color: #f3f4f6; + } + + [data-theme="dark"] .onb-title-steps, + [data-theme="dark"] .onb-progress-text { + color: #9ca3af; + } + + [data-theme="dark"] .onb-group:hover { + background: #374151; + color: #f3f4f6; + } + + [data-theme="dark"] .onb-progress-badge { + background: rgba(245,158,11,0.15); + color: #fbbf24; + } + + [data-theme="dark"] .onb-progress-badge-complete { + background: rgba(16,185,129,0.15); + color: #34d399; + } + + `; document.head.appendChild(style); diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index bfc1d0ad38..e61e0f3b78 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1575,7 +1575,8 @@ Object.assign(frappe.utils, { if (item.is_query_report) { route = "query-report/" + item.name; } else if (!item.is_query_report && item.report_ref_doctype) { - route = frappe.router.slug(item.report_ref_doctype) + "/view/report/"; + route = + frappe.router.slug(item.report_ref_doctype) + "/view/report/" + item.name; } else { route = "report/" + item.name; } @@ -1909,7 +1910,13 @@ Object.assign(frappe.utils, { process_filter_expression(filter) { let filters = []; - filters = filter ? new Function(`return ${filter}`)() : []; + if (filter) { + try { + filters = JSON.parse(filter); + } catch { + console.warn("Invalid JSON in filter expression", filter); + } + } return this.cleanup_filters(filters); }, cleanup_filters(filters) { @@ -2222,4 +2229,16 @@ Object.assign(frappe.utils, { } return value; }, + get_installed_apps() { + return frappe.boot.app_data.map((app) => { + return app.app_name; + }); + }, + is_sub_array(big, small) { + let i = 0; + for (let num of big) { + if (num === small[i]) i++; + } + return i === small.length; + }, }); diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index bfa1706f11..528b5f81a3 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -62,6 +62,7 @@ frappe.views.CommunicationComposer = class { { fieldtype: "Button", label: frappe.utils.icon("down", "xs"), + title: __("More Options"), fieldname: "option_toggle_button", click: () => { this.toggle_more_options(); @@ -496,7 +497,11 @@ frappe.views.CommunicationComposer = class { }, ]; - frappe.utils.add_select_group_button(clear_and_add_template, email_template_actions); + frappe.utils.add_select_group_button( + clear_and_add_template, + email_template_actions, + "btn-default" + ); $(fields.use_html.wrapper).addClass("mt-2 text-center").appendTo(clear_and_add_template); } diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js index 76e6691336..8ecde380be 100644 --- a/frappe/public/js/frappe/views/workspace/workspace.js +++ b/frappe/public/js/frappe/views/workspace/workspace.js @@ -493,6 +493,7 @@ frappe.views.Workspace = class Workspace { let blocks = [ { type: "header", + data: { text: values.title }, }, ]; @@ -666,7 +667,6 @@ frappe.views.Workspace = class Workspace { spacer: this.blocks["spacer"], HeaderSize: frappe.workspace_block.tunes["header_size"], }; - this.editor = new EditorJS({ data: { blocks: blocks || [], @@ -676,6 +676,26 @@ frappe.views.Workspace = class Workspace { readOnly: true, logLevel: "ERROR", }); + if (blocks.length == 0) { + let message = __("Welcome to the {0} workspace", [this.page.title]); + let default_block = [ + { + type: "header", + data: { text: message }, + }, + ]; + if (this.has_access) { + default_block.push({ + type: "paragraph", + data: { + text: __("Click on {0} to edit", [frappe.utils.icon("ellipsis")]), + }, + }); + } + this.editor.isReady.then(() => { + this.editor.render({ blocks: default_block }); + }); + } } save_page(page) { diff --git a/frappe/public/scss/common/modal.scss b/frappe/public/scss/common/modal.scss index 3cb388a3fd..9cd60935c2 100644 --- a/frappe/public/scss/common/modal.scss +++ b/frappe/public/scss/common/modal.scss @@ -243,10 +243,6 @@ body.modal-open[style^="padding-right"] { } .frappe-control:last-child { margin-left: 10px; - button { - // same as form-control input - height: calc(1.5em + 0.7rem); - } } } } @@ -268,7 +264,19 @@ body.modal-open[style^="padding-right"] { } .frappe-control:last-child { - margin-top: -14px; + margin-top: 10px; + } + } +} + +.modal .frappe-control[data-fieldname="option_toggle_button"] { + margin-top: 10px; + .form-group { + margin-bottom: 0; + + button { + width: 28px; + height: 28px; } } } @@ -299,6 +307,9 @@ body.modal-open[style^="padding-right"] { } .assignee { flex: 1; + display: flex; + gap: 8px; + align-items: center; } &:hover { .btn-group { @@ -306,9 +317,6 @@ body.modal-open[style^="padding-right"] { transition: opacity 0.1s ease-in-out; } } - .avatar { - margin-right: var(--margin-md); - } } // Stack minimized modals diff --git a/frappe/public/scss/desk/avatar.scss b/frappe/public/scss/desk/avatar.scss index 7fd2a321e2..059518c414 100644 --- a/frappe/public/scss/desk/avatar.scss +++ b/frappe/public/scss/desk/avatar.scss @@ -98,6 +98,16 @@ } } +.avatar-smaller { + width: 22px; + height: 22px; + text-align: center; + + .standard-image { + @include get_textstyle("xs", "regular"); + } +} + .avatar-medium { width: 28px; height: 28px; diff --git a/frappe/public/scss/desk/form_sidebar.scss b/frappe/public/scss/desk/form_sidebar.scss index fceffe52d7..02c7411027 100644 --- a/frappe/public/scss/desk/form_sidebar.scss +++ b/frappe/public/scss/desk/form_sidebar.scss @@ -20,6 +20,10 @@ flex-wrap: wrap; color: var(--text-light); + .icon { + stroke: var(--text-light); + } + .icon-btn { height: unset; } @@ -38,6 +42,82 @@ } } + .user-actions { + display: flex; + flex-direction: column; + gap: 8px; + padding: var(--padding-md); + + .user-actions-list { + display: flex; + flex-direction: column; + gap: 6px; + } + + .user-action-row { + margin: 0; + } + + .user-action-link { + display: flex; + align-items: center; + text-decoration: underline; + justify-content: space-between; + gap: var(--margin-sm); + width: 100%; + padding: 4px 8px; + margin-left: -6px; + margin-right: -8px; + border-radius: var(--border-radius-md); + transition: background-color 120ms ease; + + &:hover, + &:focus-visible { + background: var(--subtle-fg); + } + + &:focus-visible { + outline: none; + } + + .user-action-external-icon { + display: none; + line-height: 0; + + .icon { + margin: 0; + --icon-stroke: var(--text-muted); + } + } + + &[target="_blank"] .user-action-external-icon { + display: inline-flex; + align-items: center; + opacity: 0; + transform: translateX(-2px); + transition: opacity 120ms ease, transform 120ms ease; + } + + &[target="_blank"]:hover .user-action-external-icon, + &[target="_blank"]:focus-visible .user-action-external-icon { + opacity: 1; + transform: translateX(0); + } + } + + .user-action-label { + display: block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .sidebar-section.user-actions.border-bottom { + padding-bottom: 15px; + } + .form-tags { .tag-area { margin-top: -3px; @@ -141,8 +221,13 @@ } } + .form-title-text { + // to match the actions button height for center alignment + line-height: 28px; + } + .form-stats-likes { - gap: 8px; + gap: 2px; .form-print { button:hover { background: var(--btn-default-hover-bg); @@ -318,30 +403,56 @@ body[data-route^="Form"] { .attachment-row, .form-tag-row { - margin: var(--margin-xs) 0; - max-width: 100%; + margin: 4px 0; + .data-pill { @include get_textstyle("sm", "regular"); justify-content: space-between; box-shadow: none; + display: flex; + align-items: center; + height: 24px; + padding: 0px 6px !important; + + .pill-label { + color: inherit !important; + } + + .icon { + stroke: currentColor; + } } } .attachment-row { + margin-left: -6px; + margin-right: 0px; + .data-pill { + display: flex; + align-items: center; + height: 28px; + border-radius: var(--border-radius-md); + padding: 0px 6px !important; background-color: unset; box-shadow: none; - padding-left: 0px !important; width: 100%; + &:hover, + &:focus-within { + background-color: var(--subtle-fg); + } + &:active { background-color: transparent !important; box-shadow: none !important; } + > div { + gap: 8px; + } + .attachment-file-label { display: block; - margin-left: var(--margin-xs); - padding-right: var(--padding-xs); text-align: left; } .attachment-icon { @@ -377,13 +488,58 @@ body[data-route^="Form"] { .form-attachments, .form-tags, .form-shared { - padding: 8px; + padding: var(--padding-sm) var(--padding-md); } + +.form-attachments { + // to add gap between attachment section label and attachments + // without affecting empty state + .attachments-actions + .attachment-row { + margin-top: 8px; + } +} + +.form-tags { + // to add gap between tag section label and tags + // without affecting empty state + :not(.form-tag-row) + .form-tag-row { + margin-top: 8px; + } +} + .form-assignments, .form-shared { .assignments, .shares { - margin: var(--margin-xs) 0px; + margin-top: 8px; + + .dialog-assignment-row { + display: flex; + align-items: center; + height: 28px; + border-radius: var(--border-radius-md); + padding: 0px 6px; + margin-left: -8px; + margin-right: 0px; + + &:hover, + &:focus-within { + background-color: var(--subtle-fg); + } + + &:not(:last-child) { + margin-bottom: 4px; + } + + .btn-group { + margin-right: -4px; + } + } + + .view-all-assignment { + display: block; + margin-top: var(--padding-xs); + } } } .add-assignment-btn, @@ -415,17 +571,43 @@ body[data-route^="Form"] { } } +.liked-by { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; +} + .liked-by-popover { + max-width: 240px; + border-radius: var(--border-radius-md); + box-shadow: var(--shadow-md); + overflow: hidden; + .popover-body { - min-height: 30px; padding: 0px; + .liked-by-popover-summary { + padding: 4px 10px; + margin: 0; + color: var(--text-muted); + border-bottom: 1px solid var(--subtle-accent); + @include get_textstyle("sm", "regular"); + } + ul.list-unstyled { margin-bottom: 0px; + padding: 4px; li { - padding: var(--padding-xs) var(--padding-sm); - margin: 2px; + display: flex; + align-items: center; + gap: var(--padding-xs); + padding: var(--padding-xs); + margin: 0; + border-radius: var(--border-radius-sm); + cursor: pointer; &:hover { background-color: var(--fg-hover-color); diff --git a/frappe/public/scss/desk/sidebar.scss b/frappe/public/scss/desk/sidebar.scss index 739e2c3b16..5f8c479268 100644 --- a/frappe/public/scss/desk/sidebar.scss +++ b/frappe/public/scss/desk/sidebar.scss @@ -127,7 +127,15 @@ } } - .onboarding-sidebar { + .promotional-banners { + display: flex; + flex-direction: column; + gap: 4px; + margin: var(--margin-sm) 0; + } + + .onboarding-sidebar, + .promotional-banner { text-decoration: none; font-size: var(--text-sm); display: flex; @@ -287,9 +295,8 @@ width: auto; } } - .collapse-sidebar-link { - display: none; - } + .promotional-banners, + .collapse-sidebar-link, .dropdown-navbar-user { display: none; } diff --git a/frappe/public/scss/desk/timeline.scss b/frappe/public/scss/desk/timeline.scss index e5de39caf7..20d6f98e13 100644 --- a/frappe/public/scss/desk/timeline.scss +++ b/frappe/public/scss/desk/timeline.scss @@ -96,6 +96,11 @@ $threshold: 34; max-width: var(--timeline-content-max-width); padding: var(--padding-sm); margin-left: var(--margin-md); + + > .ql-editor { + display: inline-flex; + } + &.frappe-card { color: var(--text-neutral); background-color: var(--bg-color); diff --git a/frappe/website/js/bootstrap-4.js b/frappe/website/js/bootstrap-4.js index e525d423b6..b95d42ac6a 100644 --- a/frappe/website/js/bootstrap-4.js +++ b/frappe/website/js/bootstrap-4.js @@ -27,7 +27,7 @@ frappe.get_modal = function (title, content) { diff --git a/frappe/www/printview.py b/frappe/www/printview.py index b6b7d9aa8d..773a8611ae 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -92,6 +92,7 @@ def get_context(context) -> PrintContext: # Include selected print format name in access log print_format_name = getattr(print_format, "name", "Standard") + pdf_generator = getattr(print_format, "pdf_generator", "wkhtmltopdf") make_access_log( doctype=frappe.form_dict.doctype, @@ -114,7 +115,7 @@ def get_context(context) -> PrintContext: "print_format": print_format_name, "letterhead": letterhead, "no_letterhead": frappe.form_dict.no_letterhead, - "pdf_generator": frappe.form_dict.get("pdf_generator", "wkhtmltopdf"), + "pdf_generator": frappe.form_dict.get("pdf_generator", pdf_generator), } @@ -342,7 +343,8 @@ def get_html_and_style( if isinstance(name, str): document = frappe.get_lazy_doc(doc, name, check_permission=True) else: - document = frappe.get_doc(json.loads(doc), check_permission=True) + details = json.loads(doc) + document = frappe.get_cached_doc(details["doctype"], details["name"], check_permission=True) print_format = get_print_format_doc(print_format, meta=document.meta) set_link_titles(document) diff --git a/pyproject.toml b/pyproject.toml index d0bf7066b3..306aab4d3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ # We depend on internal attributes, # do NOT add loose requirements on PyMySQL versions. "PyMySQL==1.1.2", - "pypdf==6.7.1", + "pypdf==6.7.2", "PyPika @ git+https://github.com/frappe/pypika@2c50e6142b2d61d2d243e466fdd5dc03b3d918f2", "mysqlclient==2.2.7", "PyQRCode~=1.2.1", diff --git a/yarn.lock b/yarn.lock index 9dd12ad0e0..689fa38fba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2055,9 +2055,9 @@ mime@^1.4.1: integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + version "3.1.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624" + integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA== dependencies: brace-expansion "^1.1.7"