diff --git a/.github/workflows/build-and-commit-assets.yml b/.github/workflows/build-and-commit-assets.yml
new file mode 100644
index 000000000000..c57953cfd0d0
--- /dev/null
+++ b/.github/workflows/build-and-commit-assets.yml
@@ -0,0 +1,60 @@
+name: Build and Upload Assets
+
+on:
+ push:
+ branches:
+ - develop
+ - 'version-*'
+
+concurrency:
+ group: build-assets-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: write
+
+jobs:
+ build-assets:
+ name: Build JS/CSS and upload to release
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ path: apps/frappe
+
+ - name: Create bench structure
+ run: |
+ mkdir -p sites
+ echo "frappe" > sites/apps.txt
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: yarn
+ cache-dependency-path: apps/frappe/yarn.lock
+
+ - name: Install JS dependencies
+ working-directory: apps/frappe
+ run: yarn install --frozen-lockfile
+
+ - name: Link node_modules into public/
+ working-directory: apps/frappe
+ run: ln -s "$PWD/node_modules" frappe/public/node_modules
+
+ - name: Build assets (production)
+ working-directory: apps/frappe
+ run: yarn run production
+
+ - name: Package assets
+ working-directory: apps/frappe
+ run: tar czf frappe-assets.tar.gz -C ../../sites/assets/frappe dist
+
+ - name: Upload to rolling release
+ working-directory: apps/frappe
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ TAG="assets-${GITHUB_REF_NAME//\//-}"
+ gh release create "$TAG" --prerelease --title "Assets: $GITHUB_REF_NAME" --notes "" 2>/dev/null || true
+ gh release upload "$TAG" frappe-assets.tar.gz --clobber
diff --git a/.github/workflows/publish-assets-develop.yml b/.github/workflows/publish-assets-develop.yml
deleted file mode 100644
index 2d60df6c70cb..000000000000
--- a/.github/workflows/publish-assets-develop.yml
+++ /dev/null
@@ -1,45 +0,0 @@
-name: 'Frappe Assets'
-
-on:
- workflow_dispatch:
- push:
- branches: [ develop ]
-
-jobs:
- build-dev-and-publish:
- name: 'Build and Publish Assets for Development'
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v6
- with:
- path: 'frappe'
- - uses: actions/setup-node@v6
- with:
- node-version: 24
- - uses: actions/setup-python@v6
- with:
- python-version: '3.14'
- - name: Set up bench and build assets
- run: |
- npm install -g yarn
- pip3 install -U frappe-bench
- bench -v init frappe-bench --no-procfile --no-backups --skip-assets --skip-redis-config-generation --python $(which python) --frappe-path $GITHUB_WORKSPACE/frappe
- cd frappe-bench && bench build
-
- - name: Package assets
- run: |
- mkdir -p $GITHUB_WORKSPACE/build
- tar -cvpzf $GITHUB_WORKSPACE/build/$GITHUB_SHA.tar.gz ./frappe-bench/sites/assets/frappe/dist
-
- - name: Publish assets to S3
- uses: jakejarvis/s3-sync-action@master
- with:
- args: --acl public-read
- env:
- AWS_S3_BUCKET: 'assets.frappeframework.com'
- AWS_ACCESS_KEY_ID: ${{ secrets.S3_ASSETS_ACCESS_KEY_ID }}
- AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_ASSETS_SECRET_ACCESS_KEY }}
- AWS_S3_ENDPOINT: 'http://s3.fr-par.scw.cloud'
- AWS_REGION: 'fr-par'
- SOURCE_DIR: '$GITHUB_WORKSPACE/build'
diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml
index 71d3a359dd7c..22bd23c6492d 100644
--- a/.github/workflows/server-tests.yml
+++ b/.github/workflows/server-tests.yml
@@ -65,8 +65,8 @@ jobs:
coverage:
name: Coverage Wrap Up
needs: [test, checkrun]
+ if: ${{ needs.checkrun.outputs.build == 'strawberry' && github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
- if: ${{ github.event_name != 'pull_request' }}
steps:
- name: Clone
uses: actions/checkout@v6
diff --git a/cypress/integration/global_search_settings.js b/cypress/integration/global_search_settings.js
new file mode 100644
index 000000000000..3da3f98b037f
--- /dev/null
+++ b/cypress/integration/global_search_settings.js
@@ -0,0 +1,129 @@
+context("Global Search Settings — configure search fields", () => {
+ const GS_GRID = '.frappe-control[data-fieldname="allowed_in_global_search"]';
+
+ /** Editable-grid Link fields only mount after the row is active (toggle_editable_row → make_control). */
+ function ensure_first_priority_row() {
+ cy.get(`${GS_GRID} .grid-body`).then(($body) => {
+ if ($body.find(".grid-row").length === 0) {
+ cy.get(`${GS_GRID} .grid-add-row`).click();
+ }
+ });
+ cy.get(`${GS_GRID} .grid-body .grid-row[data-idx="1"]`).should("exist");
+ }
+
+ function activate_document_type_cell(rowAlias = "@row") {
+ cy.get(rowAlias).find('[data-fieldname="document_type"]').click();
+ cy.get(rowAlias).find('[data-fieldname="document_type"] input').should("exist");
+ }
+
+ /** Awesomplete dropdown is attached via `aria-owns` (often outside the row); items are `div[role="option"]`, not `li`. */
+ function select_document_type_link(rowAlias, label) {
+ cy.get(rowAlias).find('[data-fieldname="document_type"] input').as("docTypeInput");
+ cy.get("@docTypeInput").clear().focus().type(label, { delay: 100 });
+ cy.get("@docTypeInput").invoke("attr", "aria-owns").should("match", /\w+/);
+ cy.get("@docTypeInput")
+ .invoke("attr", "aria-owns")
+ .then((ownsId) => {
+ const sel = `#${CSS.escape(ownsId)}`;
+ cy.get(sel).should("be.visible");
+ cy.get(sel)
+ .find('[role="option"]')
+ .filter((_, el) => {
+ const $el = Cypress.$(el);
+ const primary =
+ $el.find("strong").first().text().trim() ||
+ $el.text().trim().split("\n")[0];
+ return primary === label;
+ })
+ .should("have.length", 1)
+ .scrollIntoView()
+ .click({ force: true });
+ });
+ cy.get("@docTypeInput").blur();
+ cy.get("@docTypeInput").should("have.value", label);
+ }
+
+ beforeEach(() => {
+ cy.login("Administrator", Cypress.env("adminPassword") || "admin");
+ cy.visit("/desk/global-search-settings");
+ cy.get("body").should("have.attr", "data-ajax-state", "complete");
+ cy.get(`${GS_GRID}`).should("exist");
+ });
+
+ it("shows a message when Configure is clicked without Document Type", () => {
+ cy.get('.frappe-control[data-fieldname="allowed_in_global_search"]')
+ .find(".grid-add-row")
+ .click();
+ cy.get(
+ '.frappe-control[data-fieldname="allowed_in_global_search"] .grid-body .grid-row:last'
+ )
+ .find('[data-fieldname="configure"] button')
+ .click();
+ cy.get(".msgprint").should("contain", "Please select Document Type first");
+ });
+
+ it("opens configure dialog with MultiCheck field options and filter search", () => {
+ ensure_first_priority_row();
+ cy.get(`${GS_GRID} .grid-body .grid-row[data-idx="1"]`).as("row");
+ activate_document_type_cell();
+ select_document_type_link("@row", "ToDo");
+
+ cy.get("@row").find('[data-fieldname="configure"] button').click();
+
+ cy.get_open_dialog().find(".modal-title").should("contain", "Configure search fields");
+ cy.get_open_dialog().should("contain", "ToDo");
+ cy.get_open_dialog()
+ .find('.checkbox-options input[type="checkbox"][data-unit="name"]')
+ .should("exist");
+
+ const unlikely = "xyz-nonmatching-global-search-filter-12345";
+ cy.get_open_dialog().find('[data-element="search"]').clear().type(unlikely);
+ cy.get_open_dialog()
+ .find(".checkbox-options .unit-checkbox:visible")
+ .should("have.length", 0);
+ cy.get_open_dialog().find('[data-element="search"]').clear();
+ cy.get_open_dialog()
+ .find(".checkbox-options .unit-checkbox:visible")
+ .should((els) => {
+ expect(els.length).to.be.greaterThan(0);
+ });
+
+ cy.get_open_dialog().find(".btn-modal-close").click();
+ cy.get(".modal:visible").should("not.exist");
+ });
+
+ it("saves selected fields and shows success toast", () => {
+ cy.intercept(
+ "POST",
+ "/api/method/frappe.desk.doctype.global_search_settings.global_search_settings.update_global_search_fields"
+ ).as("update_global_search_fields");
+
+ ensure_first_priority_row();
+ cy.get(`${GS_GRID} .grid-body .grid-row[data-idx="1"]`).as("row");
+ activate_document_type_cell();
+ // Must not be a Core module DocType — API rejects those (no toast; server error).
+ select_document_type_link("@row", "ToDo");
+
+ cy.get("@row").find('[data-fieldname="configure"] button').click();
+
+ cy.get_open_dialog()
+ .find('.checkbox-options input[type="checkbox"][data-unit="name"]')
+ .should("exist");
+
+ cy.get_open_dialog()
+ .find(".modal-footer .standard-actions .btn-primary")
+ .contains("Save")
+ .click({ force: true });
+
+ cy.wait("@update_global_search_fields").then(({ response }) => {
+ expect(response.statusCode).to.eq(200);
+ expect(response.body.exc, JSON.stringify(response.body)).to.be.undefined;
+ expect(response.body.message?.success).to.eq(true);
+ });
+
+ cy.get('[role="alert"].desk-alert .alert-message', { timeout: 25000 }).should(
+ "contain",
+ "Search fields updated."
+ );
+ });
+});
diff --git a/cypress/integration/grid.js b/cypress/integration/grid.js
index 607db6746eae..0e951edd0402 100644
--- a/cypress/integration/grid.js
+++ b/cypress/integration/grid.js
@@ -111,4 +111,90 @@ context("Grid", () => {
cy.get("@table-form").find(".grid-footer-toolbar").click();
});
});
+
+ it("shows edit button only when child table allow_bulk_edit is enabled", () => {
+ cy.visit("/desk/contact/Test Contact");
+ cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
+
+ cy.window()
+ .its("cur_frm")
+ .then((frm) => {
+ const grid = frm.get_field("phone_nos").grid;
+ grid.meta.allow_bulk_edit = false;
+ grid.refresh_edit_rows_button();
+ });
+
+ cy.get("@table").find('.grid-row[data-idx="1"] .grid-row-check').click({ force: true });
+ cy.get("@table").find(".grid-edit-rows").should("have.class", "hidden");
+
+ cy.window()
+ .its("cur_frm")
+ .then((frm) => {
+ const grid = frm.get_field("phone_nos").grid;
+ grid.meta.allow_bulk_edit = true;
+ grid.refresh_edit_rows_button();
+ });
+
+ cy.get("@table").find(".grid-edit-rows").should("not.have.class", "hidden");
+ });
+
+ it("bulk edit updates only selected child rows", () => {
+ const updated_phone = `99999${Date.now().toString().slice(-5)}`;
+
+ cy.visit("/desk/contact/Test Contact");
+ cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
+
+ cy.window()
+ .its("cur_frm")
+ .then((frm) => {
+ const grid = frm.get_field("phone_nos").grid;
+ grid.meta.allow_bulk_edit = true;
+ grid.refresh_edit_rows_button();
+
+ expect(frm.doc.phone_nos.length).to.be.greaterThan(1);
+ const phone_df = grid.docfields.find((df) => df.fieldname === "phone");
+ expect(phone_df).to.exist;
+ cy.wrap(phone_df.label).as("phoneFieldLabel");
+ cy.wrap(frm.doc.phone_nos[1].phone || "").as("secondRowPhoneBefore");
+ });
+
+ cy.get("@table").find('.grid-row[data-idx="1"] .grid-row-check').click({ force: true });
+ cy.get("@table").find(".grid-edit-rows").click({ force: true });
+
+ cy.window()
+ .its("cur_dialog")
+ .then((dialog) => {
+ cy.get("@phoneFieldLabel").then((phoneFieldLabel) => {
+ return dialog
+ .set_value("field", phoneFieldLabel)
+ .then(() => dialog.set_value("value", updated_phone))
+ .then(() => {
+ dialog.get_primary_btn().click();
+ });
+ });
+ });
+
+ cy.window().its("cur_frm.doc.phone_nos.0.phone").should("eq", updated_phone);
+ cy.window()
+ .its("cur_frm")
+ .then((frm) => {
+ cy.get("@secondRowPhoneBefore").then((secondRowPhoneBefore) => {
+ expect(frm.doc.phone_nos[1].phone || "").to.equal(secondRowPhoneBefore);
+ });
+ });
+ });
+
+ it("hides add-row and add-multiple-rows buttons when rows are selected", () => {
+ cy.visit("/desk/contact/Test Contact");
+ cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
+
+ cy.get("@table").find('.grid-row[data-idx="1"] .grid-row-check').click({ force: true });
+
+ cy.get("@table").find(".grid-add-row").should("have.class", "hidden");
+ cy.get("@table").find(".grid-add-multiple-rows").should("have.class", "hidden");
+
+ cy.get("@table").find('.grid-row[data-idx="1"] .grid-row-check').click({ force: true });
+
+ cy.get("@table").find(".grid-add-row").should("not.have.class", "hidden");
+ });
});
diff --git a/cypress/integration/grid_row_form_tabs.js b/cypress/integration/grid_row_form_tabs.js
index c9952a304250..74560cc9252b 100644
--- a/cypress/integration/grid_row_form_tabs.js
+++ b/cypress/integration/grid_row_form_tabs.js
@@ -112,6 +112,29 @@ context("Grid Row Form Tabs", () => {
cy.get("@table-form2").find(".form-tabs .nav-link").first().should("have.class", "active");
});
+ it("should jump to a field inside the grid row form", () => {
+ cy.new_form(parent_doctype_name);
+ cy.fill_field("title", "Test Jump To Field");
+
+ // Add a row and open the grid row form
+ cy.get('.frappe-control[data-fieldname="items"]').as("table");
+ cy.get("@table").findByRole("button", { name: "Add row" }).click();
+ cy.get("@table").find('[data-idx="1"]').find(".btn-open-row").click();
+ cy.get(".grid-row-open").as("table-form");
+
+ // Jump to a field that lives on a different tab (Details > Notes)
+ cy.get("body").type("{esc}").type("{ctrl+j}");
+ cy.get(".modal input[type='text']").first().focus();
+ cy.get("body").type("Notes").wait(1000).type("{enter}").wait(200);
+ cy.findByRole("button", { name: "Go" }).click().wait(500);
+
+ // Grid row form stays open and the target field is focused
+ cy.get(".grid-row-open").should("exist");
+ cy.get("@table-form")
+ .find('.frappe-control[data-fieldname="notes"] input')
+ .should("be.focused");
+ });
+
it("should allow data entry in fields across different tabs", () => {
cy.new_form(parent_doctype_name);
cy.fill_field("title", "Test Data Entry");
diff --git a/cypress/integration/query_report.js b/cypress/integration/query_report.js
index 8df908683cc7..62af660d645d 100644
--- a/cypress/integration/query_report.js
+++ b/cypress/integration/query_report.js
@@ -39,8 +39,17 @@ context("Query Report", () => {
.click({ force: true });
cy.get_open_dialog().get(".modal-title").should("contain", "Add Column");
cy.get('select[data-fieldname="doctype"]').select("Role (Name)", { force: true });
- cy.get('select[data-fieldname="field"]').select("Role Name", { force: true });
- cy.get('select[data-fieldname="insert_after"]').select("Name", { force: true });
+ cy.wait(500);
+ cy.get_open_dialog()
+ .find('.control-input > .awesomplete > input[data-fieldname="field"]')
+ .should("be.visible")
+ .clear({ force: true })
+ .type("Role Name{enter}", { delay: 150, force: true });
+ cy.get_open_dialog()
+ .find('.control-input > .awesomplete > input[data-fieldname="insert_after"]')
+ .should("be.visible")
+ .clear({ force: true })
+ .type("Name{enter}", { delay: 150, force: true });
cy.get_open_dialog()
.findByRole("button", { name: "Submit" })
.click({ force: true });
diff --git a/cypress/integration/rounding.js b/cypress/integration/rounding.js
index 89e305b1f61e..fc7cd50ead0f 100644
--- a/cypress/integration/rounding.js
+++ b/cypress/integration/rounding.js
@@ -99,6 +99,17 @@ context("Rounding behaviour", () => {
expect(flt(-1.15, 1, null, rounding_method)).eq(-1.2);
expect(flt(-2.25, 1, null, rounding_method)).eq(-2.2);
expect(flt(-3.35, 1, null, rounding_method)).eq(-3.4);
+
+ // Sign-symmetry regression
+ for (const [value, expected] of [
+ [647.325, 647.32],
+ [647.315, 647.32],
+ [0.125, 0.12],
+ [0.135, 0.14],
+ ]) {
+ expect(flt(value, 2, null, rounding_method)).eq(expected);
+ expect(flt(-value, 2, null, rounding_method)).eq(-expected);
+ }
});
});
});
diff --git a/cypress/integration/web_form.js b/cypress/integration/web_form.js
index 5f1c4474a5fb..36f65a80bc79 100644
--- a/cypress/integration/web_form.js
+++ b/cypress/integration/web_form.js
@@ -72,10 +72,8 @@ context("Web Form", () => {
cy.call("logout");
- cy.visit("/note");
- cy.get_open_dialog()
- .get(".modal-message")
- .contains("You are not permitted to access this page without login.");
+ cy.visit("/note", { failOnStatusCode: false });
+ cy.contains("You must be logged in to use this form.");
});
it("Show List", () => {
diff --git a/frappe/__init__.py b/frappe/__init__.py
index fd0d2d59907f..2b936fdcb7e0 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -55,7 +55,7 @@
render_template,
)
-__version__ = "16.18.2"
+__version__ = "16.22.0"
__title__ = "Frappe Framework"
if TYPE_CHECKING: # pragma: no cover
@@ -196,7 +196,8 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force: bool =
local.new_doc_templates = {}
local.request_cache = defaultdict(dict)
- local.jenv = None
+ local.jenv_restricted = None
+ local.jenv_unrestricted = None
local.jloader = None
local.cache = {}
local.form_dict = _dict()
@@ -371,7 +372,8 @@ def set_user(username: str):
local.session.sid = username
local.cache = {}
local.form_dict = _dict()
- local.jenv = None
+ local.jenv_restricted = None
+ local.jenv_unrestricted = None
local.session.data = _dict()
local.role_permissions = {}
local.new_doc_templates = {}
diff --git a/frappe/app.py b/frappe/app.py
index 7f4e4bcfacc3..5d319a34cbf5 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -478,6 +478,7 @@ def sync_database():
auto_enabling_integrations=False,
default_integrations=False,
integrations=integrations,
+ include_local_variables=False,
_experiments=experiments,
**kwargs,
)
diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py
index 1a26a6d77ca9..4516d1e9a9fc 100644
--- a/frappe/automation/doctype/assignment_rule/assignment_rule.py
+++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py
@@ -83,7 +83,7 @@ def do_assignment(self, doc):
if not user or not frappe.db.exists("User", user):
return False
- assign_to.add(
+ assign_to._add(
dict(
assign_to=[user],
doctype=doc.get("doctype"),
diff --git a/frappe/boot.py b/frappe/boot.py
index 2ee990ed2a8d..0ae8a64664bc 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -13,6 +13,7 @@
get_setup_wizard_completed_apps,
)
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings
+from frappe.desk.desk_views import DeskViews
from frappe.desk.doctype.changelog_feed.changelog_feed import get_changelog_feed_items
from frappe.desk.doctype.desktop_icon.desktop_icon import get_desktop_icons
from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours
@@ -21,11 +22,7 @@
from frappe.email.inbox import get_email_accounts
from frappe.integrations.frappe_providers.frappecloud_billing import current_site_info, is_fc_site
from frappe.model.base_document import get_controller
-from frappe.permissions import has_permission
-from frappe.query_builder import DocType
-from frappe.query_builder.functions import Count
-from frappe.query_builder.terms import ParameterizedValueWrapper, SubQuery
-from frappe.utils import add_user_info, cstr, get_system_timezone
+from frappe.utils import add_user_info, get_system_timezone
from frappe.utils.caching import redis_cache
from frappe.utils.change_log import get_versions
from frappe.utils.frappecloud import on_frappecloud
@@ -57,6 +54,9 @@ def get_bootinfo():
bootinfo.modules = {}
bootinfo.module_list = []
+ desk_views = DeskViews()
+ desk_views.build_entities()
+ desk_views.add_to_boot(bootinfo)
load_desktop_data(bootinfo)
bootinfo.desktop_icons = get_desktop_icons(bootinfo=bootinfo)
bootinfo.letter_heads = get_letter_heads()
@@ -69,7 +69,6 @@ def get_bootinfo():
bootinfo.nested_set_doctypes = frappe.get_all("DocField", {"fieldname": "lft"}, pluck="parent")
bootinfo.tree_view_doctypes = get_tree_view_doctypes()
add_home_page(bootinfo, doclist)
- bootinfo.page_info = get_allowed_pages()
load_translations(bootinfo)
add_timezone_info(bootinfo)
load_conf_settings(bootinfo)
@@ -160,13 +159,9 @@ def load_conf_settings(bootinfo):
def load_desktop_data(bootinfo):
- from frappe.desk.desktop import get_workspace_sidebar_items
-
- bootinfo.workspaces = get_workspace_sidebar_items()
allowed_pages = [d.name for d in bootinfo.workspaces.get("pages")]
bootinfo.workspace_sidebar_item = get_sidebar_items(allowed_pages)
bootinfo.module_wise_workspaces = get_controller("Workspace").get_module_wise_workspaces()
- bootinfo.dashboards = frappe.get_all("Dashboard")
bootinfo.app_data = []
Workspace = frappe.qb.DocType("Workspace")
@@ -222,124 +217,6 @@ def load_desktop_data(bootinfo):
)
-def get_allowed_pages(cache=False):
- return get_user_pages_or_reports("Page", cache=cache)
-
-
-def get_allowed_reports(cache=False):
- return get_user_pages_or_reports("Report", cache=cache)
-
-
-def get_allowed_report_names(cache=False) -> set[str]:
- return {cstr(report) for report in get_allowed_reports(cache).keys() if report}
-
-
-def get_user_pages_or_reports(parent, cache=False):
- if cache:
- has_role = frappe.cache.get_value("has_role:" + parent, user=frappe.session.user)
- if has_role:
- return has_role
-
- roles = frappe.get_roles()
- has_role = {}
-
- page = DocType("Page")
- report = DocType("Report")
-
- is_report = parent == "Report"
-
- if is_report:
- columns = (report.name.as_("title"), report.ref_doctype, report.report_type)
- else:
- columns = (page.title.as_("title"),)
-
- customRole = DocType("Custom Role")
- hasRole = DocType("Has Role")
- parentTable = DocType(parent)
-
- # get pages or reports set on custom role
- pages_with_custom_roles = (
- frappe.qb.from_(customRole)
- .from_(hasRole)
- .from_(parentTable)
- .select(customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns)
- .where(
- (hasRole.parent == customRole.name)
- & (parentTable.name == customRole[parent.lower()])
- & (customRole[parent.lower()].isnotnull())
- & (hasRole.role.isin(roles))
- )
- ).run(as_dict=True)
-
- for p in pages_with_custom_roles:
- has_role[p.name] = {"modified": p.modified, "title": p.title, "ref_doctype": p.ref_doctype}
-
- subq = (
- frappe.qb.from_(customRole)
- .select(customRole[parent.lower()])
- .where(customRole[parent.lower()].isnotnull())
- )
-
- pages_with_standard_roles = (
- frappe.qb.from_(hasRole)
- .from_(parentTable)
- .select(parentTable.name.as_("name"), parentTable.modified, *columns)
- .where(
- (hasRole.role.isin(roles)) & (hasRole.parent == parentTable.name) & (parentTable.name.notin(subq))
- )
- .distinct()
- )
-
- if is_report:
- pages_with_standard_roles = pages_with_standard_roles.where(report.disabled == 0)
-
- pages_with_standard_roles = pages_with_standard_roles.run(as_dict=True)
-
- for p in pages_with_standard_roles:
- if p.name not in has_role:
- has_role[p.name] = {"modified": p.modified, "title": p.title}
- if parent == "Report":
- has_role[p.name].update({"ref_doctype": p.ref_doctype})
-
- no_of_roles = SubQuery(
- frappe.qb.from_(hasRole).select(Count("*")).where(hasRole.parent == parentTable.name)
- )
-
- # pages and reports with no role are allowed
- rows_with_no_roles = (
- frappe.qb.from_(parentTable)
- .select(parentTable.name, parentTable.modified, *columns)
- .where(no_of_roles == 0)
- ).run(as_dict=True)
-
- for r in rows_with_no_roles:
- if r.name not in has_role:
- has_role[r.name] = {"modified": r.modified, "title": r.title}
- if is_report:
- has_role[r.name] |= {"ref_doctype": r.ref_doctype}
-
- if is_report:
- if not has_permission("Report", print_logs=False):
- return {}
-
- reports = frappe.get_list(
- "Report",
- fields=["name", "report_type"],
- filters={"name": ("in", has_role.keys())},
- ignore_ifnull=True,
- )
- for report in reports:
- has_role[report.name]["report_type"] = report.report_type
-
- non_permitted_reports = set(has_role.keys()) - {r.name for r in reports}
- for r in non_permitted_reports:
- has_role.pop(r, None)
-
- # Expire every six hours
- frappe.cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600)
- return has_role
-
-
def load_translations(bootinfo):
from frappe.translate import get_messages_for_boot
@@ -559,56 +436,60 @@ def get_sidebar_items(allowed_workspaces):
else:
sidebar_title = sidebar.title
sidebar_doc = sidebar
- if (
- frappe.session.user == "Administrator"
- or sidebar_title == "My Workspaces"
- or not sidebar_doc.module
- or sidebar_doc.module in sidebar_doc.user.allow_modules
- ):
- sidebar_items[sidebar_title.lower()] = {
- "label": sidebar_title,
- "items": [],
- "header_icon": sidebar.get("header_icon"),
- "module_onboarding": sidebar.get("module_onboarding"),
- "module": sidebar_doc.module,
- "app": sidebar_doc.app,
+ is_my_workspaces = "My Workspaces" in sidebar_title
+ items = []
+ for item in sidebar_doc.items:
+ workspace_sidebar = {
+ "label": _(item.label),
+ "link_to": item.link_to,
+ "link_type": item.link_type,
+ "type": item.type,
+ "icon": item.icon,
+ "child": item.child,
+ "collapsible": item.collapsible,
+ "indent": item.indent,
+ "keep_closed": item.keep_closed,
+ "url": item.url,
+ "show_arrow": item.show_arrow,
+ "filters": item.filters,
+ "route_options": item.route_options,
+ "tab": item.navigate_to_tab,
}
- for item in sidebar_doc.items:
- workspace_sidebar = {
- "label": _(item.label),
- "link_to": item.link_to,
- "link_type": item.link_type,
- "type": item.type,
- "icon": item.icon,
- "child": item.child,
- "collapsible": item.collapsible,
- "indent": item.indent,
- "keep_closed": item.keep_closed,
- "url": item.url,
- "show_arrow": item.show_arrow,
- "filters": item.filters,
- "route_options": item.route_options,
- "tab": item.navigate_to_tab,
+ if (
+ item.link_type == "Report"
+ and item.link_to
+ and frappe.db.exists("Report", item.link_to)
+ and not frappe.db.get_value("Report", item.link_to, "disabled")
+ ):
+ report_type, ref_doctype = frappe.db.get_value(
+ "Report", item.link_to, ["report_type", "ref_doctype"]
+ )
+ workspace_sidebar["report"] = {
+ "report_type": report_type,
+ "ref_doctype": ref_doctype,
}
- if (
- item.link_type == "Report"
- and item.link_to
- and frappe.db.exists("Report", item.link_to)
- and not frappe.db.get_value("Report", item.link_to, "disabled")
- ):
- report_type, ref_doctype = frappe.db.get_value(
- "Report", item.link_to, ["report_type", "ref_doctype"]
- )
- workspace_sidebar["report"] = {
- "report_type": report_type,
- "ref_doctype": ref_doctype,
- }
- if (
- "My Workspaces" in sidebar_title
- or item.type == "Section Break"
- or sidebar_doc.is_item_allowed(item.link_to, item.link_type, allowed_workspaces)
- ):
- sidebar_items[sidebar_title.lower()]["items"].append(workspace_sidebar)
+ if (
+ is_my_workspaces
+ or item.type == "Section Break"
+ or sidebar_doc.is_item_allowed(item.link_to, item.link_type, allowed_workspaces)
+ ):
+ items.append(workspace_sidebar)
+
+ # A sidebar (and its desktop icon) is shown only if the user can see at least one
+ # real item in it, i.e. a non-Section-Break item survived the per-item filter above.
+ # This is the single source of truth for sidebar permissions and mirrors
+ # Desktop Icon.is_permitted. "My Workspaces" is always shown.
+ if not is_my_workspaces and not any(item["type"] != "Section Break" for item in items):
+ continue
+
+ sidebar_items[sidebar_title.lower()] = {
+ "label": sidebar_title,
+ "items": items,
+ "header_icon": sidebar.get("header_icon"),
+ "module_onboarding": sidebar.get("module_onboarding"),
+ "module": sidebar_doc.module,
+ "app": sidebar_doc.app,
+ }
add_user_specific_sidebar(sidebar_items)
return sidebar_items
diff --git a/frappe/commands/test_commands.py b/frappe/commands/test_commands.py
index cab51b7034d6..a4e09764e1b0 100644
--- a/frappe/commands/test_commands.py
+++ b/frappe/commands/test_commands.py
@@ -940,7 +940,7 @@ def test_create_user(self):
class TestBenchBuild(IntegrationTestCase):
def test_build_assets_size_check(self):
- CURRENT_SIZE = 3.41 # MB
+ CURRENT_SIZE = 3.45 # MB
JS_ASSET_THRESHOLD = 0.01
hooks = frappe.get_hooks()
diff --git a/frappe/contacts/doctype/address/address.json b/frappe/contacts/doctype/address/address.json
index 3e147cc20595..118385d4edd5 100644
--- a/frappe/contacts/doctype/address/address.json
+++ b/frappe/contacts/doctype/address/address.json
@@ -92,6 +92,7 @@
{
"fieldname": "pincode",
"fieldtype": "Data",
+ "hidden": 1,
"label": "Postal Code",
"search_index": 1
},
@@ -103,6 +104,7 @@
{
"fieldname": "email_id",
"fieldtype": "Data",
+ "hidden": 1,
"label": "Email Address",
"options": "Email"
},
@@ -115,6 +117,7 @@
{
"fieldname": "fax",
"fieldtype": "Data",
+ "hidden": 1,
"label": "Fax"
},
{
@@ -151,7 +154,7 @@
"icon": "fa fa-map-marker",
"idx": 5,
"links": [],
- "modified": "2024-03-23 16:01:27.068028",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Address",
@@ -167,16 +170,6 @@
"share": 1,
"write": 1
},
- {
- "create": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Purchase User",
- "share": 1,
- "write": 1
- },
{
"create": 1,
"email": 1,
@@ -212,9 +205,7 @@
},
{
"create": 1,
- "export": 1,
"if_owner": 1,
- "print": 1,
"read": 1,
"role": "All",
"write": 1
diff --git a/frappe/contacts/doctype/contact/contact.json b/frappe/contacts/doctype/contact/contact.json
index 8011d5f4e85f..31b855b1801e 100644
--- a/frappe/contacts/doctype/contact/contact.json
+++ b/frappe/contacts/doctype/contact/contact.json
@@ -20,6 +20,7 @@
"cb00",
"status",
"salutation",
+ "department",
"designation",
"gender",
"phone",
@@ -38,7 +39,6 @@
"links",
"is_primary_contact",
"more_info",
- "department",
"unsubscribed"
],
"fields": [
@@ -166,6 +166,7 @@
"default": "0",
"fieldname": "unsubscribed",
"fieldtype": "Check",
+ "hidden": 1,
"label": "Unsubscribed"
},
{
@@ -216,6 +217,7 @@
"default": "0",
"fieldname": "sync_with_google_contacts",
"fieldtype": "Check",
+ "hidden": 1,
"label": "Sync with Google Contacts"
},
{
@@ -257,7 +259,7 @@
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-03-23 16:01:30.937045",
+ "modified": "2026-06-08",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Contact",
@@ -350,16 +352,6 @@
"share": 1,
"write": 1
},
- {
- "create": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Purchase User",
- "share": 1,
- "write": 1
- },
{
"create": 1,
"email": 1,
@@ -382,10 +374,8 @@
},
{
"create": 1,
- "delete": 1,
"if_owner": 1,
"read": 1,
- "report": 1,
"role": "All",
"write": 1
}
diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py
index 3ad5e54c62b5..65088b0ee2da 100644
--- a/frappe/contacts/doctype/contact/contact.py
+++ b/frappe/contacts/doctype/contact/contact.py
@@ -348,8 +348,8 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters):
if not frappe.get_meta(doctype).get_field(searchfield) and searchfield not in frappe.db.DEFAULT_COLUMNS:
return []
- link_doctype = filters.pop("link_doctype")
- link_name = filters.pop("link_name")
+ link_doctype = filters.pop("link_doctype", None)
+ link_name = filters.pop("link_name", None)
return frappe.db.sql(
f"""select
diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json
index 296e285445d6..5e5a17dc6e56 100644
--- a/frappe/core/doctype/communication/communication.json
+++ b/frappe/core/doctype/communication/communication.json
@@ -372,7 +372,7 @@
"idx": 1,
"links": [],
"make_attachments_public": 1,
- "modified": "2025-12-25 19:19:29.427081",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication",
@@ -418,13 +418,6 @@
"print": 1,
"read": 1,
"role": "Inbox User"
- },
- {
- "delete": 1,
- "email": 1,
- "if_owner": 1,
- "read": 1,
- "role": "All"
}
],
"row_format": "Compressed",
diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py
index 16dae174ec0f..450853044054 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -447,11 +447,19 @@ def deduplicate_timeline_links(self):
self.add_link(doctype, name)
def add_link(self, link_doctype, link_name, autosave=False):
+ title_field = frappe.get_meta(link_doctype).get_title_field()
+ link_title = (
+ frappe.db.get_value(link_doctype, link_name, title_field, cache=True, order_by=None)
+ if title_field != "name"
+ else None
+ )
+
self.append(
"timeline_links",
{
"link_doctype": link_doctype,
"link_name": link_name,
+ "link_title": link_title or link_name,
"communication_date": self.communication_date,
},
)
diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js
index af8e44e6de3d..766c73566d9a 100644
--- a/frappe/core/doctype/data_import/data_import.js
+++ b/frappe/core/doctype/data_import/data_import.js
@@ -406,7 +406,9 @@ frappe.ui.form.on("Data Import", {
let column_number = `${__("Column {0}", [
warning.col,
])} `;
- let column_header = columns[warning.col].header_title;
+ let column_header = frappe.utils.escape_html(
+ columns[warning.col].header_title
+ );
header = `${column_number} (${column_header})`;
}
return `
diff --git a/frappe/core/doctype/data_import/data_import.json b/frappe/core/doctype/data_import/data_import.json
index 5a96c8546632..7b1b23e21d66 100644
--- a/frappe/core/doctype/data_import/data_import.json
+++ b/frappe/core/doctype/data_import/data_import.json
@@ -1,7 +1,6 @@
{
"actions": [],
"autoname": "format:{reference_doctype} Import on {creation}",
- "beta": 1,
"creation": "2019-08-04 14:16:08.318714",
"doctype": "DocType",
"editable_grid": 1,
@@ -135,6 +134,7 @@
"default": "1",
"fieldname": "mute_emails",
"fieldtype": "Check",
+ "hidden": 1,
"label": "Don't Send Emails",
"set_only_once": 1
},
@@ -195,11 +195,11 @@
],
"hide_toolbar": 1,
"links": [],
- "modified": "2025-01-14 17:36:22.389195",
+ "modified": "2026-05-30",
"modified_by": "Administrator",
"module": "Core",
"name": "Data Import",
- "naming_rule": "Expression",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -215,8 +215,9 @@
"write": 1
}
],
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/data_import/exporter.py b/frappe/core/doctype/data_import/exporter.py
index 4cb63651e3e3..67a82807e279 100644
--- a/frappe/core/doctype/data_import/exporter.py
+++ b/frappe/core/doctype/data_import/exporter.py
@@ -124,14 +124,14 @@ def get_data_to_export(self):
raise frappe.PermissionError(
_("You are not allowed to export {} doctype").format(self.doctype)
)
-
for doc in data:
rows = []
rows = self.add_data_row(self.doctype, None, doc, rows, 0)
if table_fields:
# add child table data
for f in table_fields:
- for i, child_row in enumerate(doc.get(f, [])):
+ table_data = doc.get(f, []) or []
+ for i, child_row in enumerate(table_data):
table_df = self.meta.get_field(f)
child_doctype = table_df.options
rows = self.add_data_row(child_doctype, child_row.parentfield, child_row, rows, i)
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index 9db4273d6eda..4183693ea1c7 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -615,7 +615,7 @@ def read_content(self, content, extension):
if extension == "csv":
data = read_csv_content(content, use_sniffer=self.use_sniffer)
elif extension == "xlsx":
- data = read_xlsx_file_from_attached_file(fcontent=content)
+ data = read_xlsx_file_from_attached_file(fcontent=content, read_only=True)
elif extension == "xls":
data = read_xls_file_from_attached_file(content)
return data
@@ -926,7 +926,8 @@ def parse(self):
self.warnings.append(
{
"message": _("Mapping column {0} to field {1}").format(
- frappe.bold(header_title or "Untitled Column "), frappe.bold(df.label)
+ frappe.bold(escape_html(header_title) or "Untitled Column "),
+ frappe.bold(df.label),
),
"type": "info",
}
@@ -953,7 +954,9 @@ def parse(self):
self.warnings.append(
{
"col": column_number,
- "message": _("Skipping Duplicate Column {0}").format(frappe.bold(header_title)),
+ "message": _("Skipping Duplicate Column {0}").format(
+ frappe.bold(escape_html(header_title))
+ ),
"type": "info",
}
)
@@ -964,7 +967,7 @@ def parse(self):
self.warnings.append(
{
"col": column_number,
- "message": _("Skipping column {0}").format(frappe.bold(header_title)),
+ "message": _("Skipping column {0}").format(frappe.bold(escape_html(header_title))),
"type": "info",
}
)
@@ -972,7 +975,9 @@ def parse(self):
self.warnings.append(
{
"col": column_number,
- "message": _("Cannot match column {0} with any field").format(frappe.bold(header_title)),
+ "message": _("Cannot match column {0} with any field").format(
+ frappe.bold(escape_html(header_title))
+ ),
"type": "info",
}
)
@@ -1017,7 +1022,7 @@ def guess_date_format(d):
{
"col": self.column_number,
"message": message.format(
- frappe.bold(self.header_title),
+ frappe.bold(escape_html(self.header_title)),
len(unique_date_formats),
frappe.bold(user_date_format),
),
@@ -1040,13 +1045,14 @@ def validate_values(self):
if self.df.fieldtype == "Link":
# find all values that dont exist
transform = (lambda v: cstr(v).lower()) if frappe.db.db_type == "mariadb" else cstr
- values = list({transform(v) for v in self.column_values if v})
+ original_values = {transform(v): cstr(v) for v in self.column_values if v}
+ values = list(original_values.keys())
exists = [
transform(d.name) for d in frappe.get_all(self.df.options, filters={"name": ("in", values)})
]
not_exists = list(set(values) - set(exists))
if not_exists:
- missing_values = ", ".join(escape_html(v) for v in not_exists)
+ missing_values = ", ".join(escape_html(original_values[v]) for v in not_exists)
message = _("The following values do not exist for {0}: {1}")
self.warnings.append(
{
diff --git a/frappe/core/doctype/deleted_document/deleted_document.py b/frappe/core/doctype/deleted_document/deleted_document.py
index ef4578f9c975..15dc267ccbc8 100644
--- a/frappe/core/doctype/deleted_document/deleted_document.py
+++ b/frappe/core/doctype/deleted_document/deleted_document.py
@@ -38,13 +38,28 @@ def clear_old_logs(days=180):
@frappe.whitelist()
-def restore(name, alert=True):
+def restore(name: str | int, alert: bool = True):
+ frappe.only_for("System Manager")
deleted = frappe.get_doc("Deleted Document", name)
if deleted.restored:
frappe.throw(_("Document {0} Already Restored").format(name), exc=frappe.DocumentAlreadyRestored)
doc = frappe.get_doc(json.loads(deleted.data))
+
+ if not frappe.has_permission(doc.doctype, "create"):
+ frappe.throw(
+ _("You do not have permission to create or restore documents of type {0}.").format(doc.doctype),
+ frappe.PermissionError,
+ )
+
+ if not frappe.has_permission(doc.doctype, "read", doc=doc):
+ frappe.throw(_("You do not have permission to restore this document."), frappe.PermissionError)
+
+ original_owner = doc.get("owner")
+ original_creation = doc.get("creation")
+ original_modified = doc.get("modified")
+ original_modified_by = doc.get("modified_by")
doc.flags.from_restore = True
try:
doc.insert()
@@ -58,6 +73,19 @@ def restore(name, alert=True):
doc.set(workflow_state_fieldname, None)
doc.insert()
+ # retain original metadata
+ frappe.db.set_value(
+ doc.doctype,
+ doc.name,
+ {
+ "owner": original_owner,
+ "creation": original_creation,
+ "modified": original_modified,
+ "modified_by": original_modified_by,
+ },
+ update_modified=False,
+ )
+
doc.add_comment("Edit", _("restored {0} as {1}").format(deleted.deleted_name, doc.name))
deleted.new_name = doc.name
@@ -69,7 +97,8 @@ def restore(name, alert=True):
@frappe.whitelist()
-def bulk_restore(docnames):
+def bulk_restore(docnames: str | list[str]):
+ frappe.only_for("System Manager")
docnames = frappe.parse_json(docnames)
message = _("Restoring Deleted Document")
restored, invalid, failed = [], [], []
diff --git a/frappe/core/doctype/deleted_document/test_deleted_document.py b/frappe/core/doctype/deleted_document/test_deleted_document.py
index 522d6aa996a5..102166aac8fd 100644
--- a/frappe/core/doctype/deleted_document/test_deleted_document.py
+++ b/frappe/core/doctype/deleted_document/test_deleted_document.py
@@ -1,7 +1,30 @@
# Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE
+import frappe
+from frappe.core.doctype.deleted_document.deleted_document import restore
from frappe.tests import IntegrationTestCase
class TestDeletedDocument(IntegrationTestCase):
- pass
+ def test_metadata_retention(self):
+ frappe.set_user("Administrator")
+ doc = frappe.get_doc({"doctype": "Note", "title": "Test Note", "content": "Test Content"}).insert()
+ orig_owner = doc.owner
+ orig_creation = doc.creation
+ orig_modified = doc.modified
+ orig_modified_by = doc.modified_by
+
+ frappe.delete_doc("Note", doc.name, force=True)
+ self.assertFalse(frappe.db.exists("Note", doc.name))
+
+ log_name = frappe.db.get_value("Deleted Document", {"deleted_name": doc.name, "restored": 0})
+ restore(log_name, alert=False)
+
+ new_restored_name = frappe.db.get_value("Deleted Document", log_name, "new_name")
+
+ restored_doc = frappe.get_doc("Note", new_restored_name)
+
+ self.assertEqual(restored_doc.owner, orig_owner)
+ self.assertEqual(str(restored_doc.creation), str(orig_creation))
+ self.assertEqual(str(restored_doc.modified), str(orig_modified))
+ self.assertEqual(restored_doc.modified_by, orig_modified_by)
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index f77190d18d9f..7f6b9a01432c 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_bulk_edit": 1,
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2013-02-18 13:36:19",
@@ -34,6 +35,7 @@
"quick_entry",
"grid_page_length",
"rows_threshold_for_grid_search",
+ "allow_bulk_edit",
"cb01",
"track_changes",
"track_seen",
@@ -715,6 +717,14 @@
"fieldname": "recipient_account_field",
"fieldtype": "Data",
"label": "Recipient Account Field"
+ },
+ {
+ "default": "1",
+ "depends_on": "istable",
+ "description": "Enable bulk update of this field across child table rows.",
+ "fieldname": "allow_bulk_edit",
+ "fieldtype": "Check",
+ "label": "Allow Bulk Edit"
}
],
"grid_page_length": 50,
@@ -824,7 +834,6 @@
],
"route": "doctype",
"row_format": "Dynamic",
- "rows_threshold_for_grid_search": 20,
"search_fields": "module",
"show_name_in_global_search": 1,
"sort_field": "creation",
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index c4643524c087..b8f821d7f3e3 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -101,6 +101,7 @@ class DocType(Document):
actions: DF.Table[DocTypeAction]
allow_auto_repeat: DF.Check
+ allow_bulk_edit: DF.Check
allow_copy: DF.Check
allow_events_in_timeline: DF.Check
allow_guest_to_view: DF.Check
@@ -1763,7 +1764,6 @@ def check_decimal_config(docfield):
def get_fields_not_allowed_in_list_view(meta) -> list[str]:
not_allowed_in_list_view = list(copy.copy(no_value_fields))
- not_allowed_in_list_view.append("Attach Image")
if meta.istable:
not_allowed_in_list_view.remove("Button")
not_allowed_in_list_view.remove("HTML")
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index 28ba53275f2d..1cf1ec85a4c1 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -763,10 +763,9 @@ def test_not_in_list_view_for_not_allowed_mandatory_field(self):
doctype = new_doctype(
fields=[
{
- "fieldname": "cover_image",
- "fieldtype": "Attach Image",
- "label": "Cover Image",
- "reqd": 1, # mandatory
+ "fieldname": "btn",
+ "fieldtype": "Button",
+ "label": "Btn",
},
{
"fieldname": "book_name",
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index 4fe1e5b869d3..f6b0b2313963 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -302,8 +302,8 @@ def validate_file_url(self):
if self.is_remote_file or not self.file_url:
return
- if not self.file_url.startswith(("/files/", "/private/files/", "/api/method/")):
- # Probably an invalid URL since it doesn't start with http and isn't an internal URL either
+ if not self.file_url.startswith(("/files/", "/private/files/")):
+ # Probably an invalid URL since it doesn't start with http either
frappe.throw(
_("URL must start with http:// or https://"),
title=_("Invalid URL"),
diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index 7f2b6c1703b1..4afc1586999f 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -642,6 +642,110 @@ def test_symlinked_files_folder(self):
file.save().reload()
self.assertIn("42", file.get_content())
+ @IntegrationTestCase.change_settings(
+ "System Settings", {"allow_guests_to_upload_files": 1, "allowed_doctypes_for_guest_uploads": "ToDo"}
+ )
+ def test_guest_upload_to_non_allowed_doctype(self):
+ """Verify Guest cannot upload to a restricted DocType."""
+ from werkzeug.test import EnvironBuilder
+ from werkzeug.wrappers import Request
+
+ from frappe.handler import upload_file
+
+ builder = EnvironBuilder(path="/", base_url="http://localhost")
+ frappe.local.request = Request(builder.get_environ())
+
+ frappe.set_user("Guest")
+ frappe.form_dict.doctype = "User"
+ frappe.form_dict.docname = "Administrator"
+
+ try:
+ self.assertRaises(frappe.PermissionError, upload_file)
+ finally:
+ frappe.set_user("Administrator")
+ frappe.form_dict.pop("doctype", None)
+ frappe.form_dict.pop("docname", None)
+ if hasattr(frappe.local, "request"):
+ del frappe.local.request
+
+ @IntegrationTestCase.change_settings(
+ "System Settings",
+ {"allow_guests_to_upload_files": 1, "allowed_doctypes_for_guest_uploads": "User\nToDo"},
+ )
+ def test_guest_upload_to_allowed_doctype(self):
+ """Verify Guest can upload to an explicitly whitelisted DocType."""
+ from werkzeug.test import EnvironBuilder
+ from werkzeug.wrappers import Request
+
+ from frappe.handler import upload_file
+
+ builder = EnvironBuilder(path="/", base_url="http://localhost")
+ frappe.local.request = Request(builder.get_environ())
+
+ frappe.set_user("Administrator")
+ todo = frappe.get_doc({"doctype": "ToDo", "description": "Test Target"}).insert()
+
+ frappe.set_user("Guest")
+ frappe.form_dict.doctype = "ToDo"
+ frappe.form_dict.docname = todo.name
+ frappe.form_dict.file_url = "https://frappe.io/assets/img/logo.png"
+ frappe.form_dict.file_name = "guest_logo.png"
+
+ file_doc = None
+ try:
+ file_doc = upload_file()
+ self.assertEqual(file_doc.attached_to_name, todo.name)
+ finally:
+ frappe.set_user("Administrator")
+
+ if file_doc:
+ file_doc.delete()
+ todo.delete()
+
+ frappe.form_dict.pop("doctype", None)
+ frappe.form_dict.pop("docname", None)
+ frappe.form_dict.pop("file_url", None)
+ frappe.form_dict.pop("file_name", None)
+
+ if hasattr(frappe.local, "request"):
+ del frappe.local.request
+
+ @IntegrationTestCase.change_settings(
+ "System Settings", {"allow_guests_to_upload_files": 1, "allowed_doctypes_for_guest_uploads": ""}
+ )
+ def test_guest_upload_for_empty_whitelist(self):
+ """Verify Guest can upload anywhere if the configuration whitelist string is left completely empty."""
+ from werkzeug.test import EnvironBuilder
+ from werkzeug.wrappers import Request
+
+ from frappe.handler import upload_file
+
+ builder = EnvironBuilder(path="/", base_url="http://localhost")
+ frappe.local.request = Request(builder.get_environ())
+
+ frappe.set_user("Guest")
+ frappe.form_dict.doctype = "User"
+ frappe.form_dict.docname = "Administrator"
+ frappe.form_dict.file_url = "https://frappe.io/assets/img/logo.png"
+ frappe.form_dict.file_name = "guest_fallback.png"
+
+ file_doc = None
+ try:
+ file_doc = upload_file()
+ self.assertEqual(file_doc.attached_to_name, "Administrator")
+ finally:
+ frappe.set_user("Administrator")
+ if file_doc:
+ file_doc.delete()
+
+ frappe.form_dict.pop("doctype", None)
+ frappe.form_dict.pop("docname", None)
+ frappe.form_dict.pop("file_url", None)
+ frappe.form_dict.pop("file_name", None)
+
+ if hasattr(frappe.local, "request"):
+ del frappe.local.request
+
@contextmanager
def convert_to_symlink(directory):
diff --git a/frappe/core/doctype/file/utils.py b/frappe/core/doctype/file/utils.py
index a4952e710c7f..3f038c3df45f 100644
--- a/frappe/core/doctype/file/utils.py
+++ b/frappe/core/doctype/file/utils.py
@@ -463,3 +463,20 @@ def find_file_by_url(path: str, name: str | None = None) -> "File" | None:
file: File = frappe.get_doc(doctype="File", **file_data)
if file.is_downloadable():
return file
+
+
+def get_safe_file_name(file_name: str) -> str:
+ return re.sub(r"[/\\%?#]", "_", file_name)
+
+
+def check_path_safety(base_path: str, requested_path: str) -> bool:
+ """Util to check path safety by ensuring sandboxing and logging unsuccessful attempts"""
+ base_path = os.path.realpath(base_path)
+ requested_path = os.path.realpath(requested_path)
+ if os.path.commonpath([base_path, requested_path]) != base_path:
+ frappe.log_error(
+ title="Attempted Unauthorized File Access",
+ message=f"Blocked access to: {requested_path}",
+ )
+ return False
+ return True
diff --git a/frappe/core/doctype/module_def/module_def.json b/frappe/core/doctype/module_def/module_def.json
index 00b6852cd5b0..5609c11da794 100644
--- a/frappe/core/doctype/module_def/module_def.json
+++ b/frappe/core/doctype/module_def/module_def.json
@@ -139,7 +139,7 @@
"link_fieldname": "module"
}
],
- "modified": "2024-09-18 12:39:19.512528",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Core",
"name": "Module Def",
@@ -169,9 +169,7 @@
},
{
"read": 1,
- "report": 1,
- "role": "All",
- "select": 1
+ "role": "All"
}
],
"show_name_in_global_search": 1,
diff --git a/frappe/core/doctype/page/page.py b/frappe/core/doctype/page/page.py
index 23dea8ecc67a..f7bef48732d3 100644
--- a/frappe/core/doctype/page/page.py
+++ b/frappe/core/doctype/page/page.py
@@ -110,7 +110,8 @@ def on_trash(self):
frappe.throw(_("Deletion of this document is only permitted in developer mode."))
delete_custom_role("page", self.name)
- frappe.db.after_commit(self.delete_folder_with_contents)
+ if frappe.conf.developer_mode:
+ frappe.db.after_commit(self.delete_folder_with_contents)
def delete_folder_with_contents(self):
try:
diff --git a/frappe/core/doctype/permission_inspector/permission_inspector.json b/frappe/core/doctype/permission_inspector/permission_inspector.json
index 690f3b61a2d5..643ec45d20f0 100644
--- a/frappe/core/doctype/permission_inspector/permission_inspector.json
+++ b/frappe/core/doctype/permission_inspector/permission_inspector.json
@@ -1,7 +1,6 @@
{
"actions": [],
"allow_rename": 1,
- "beta": 1,
"creation": "2024-01-03 17:43:27.257317",
"doctype": "DocType",
"engine": "InnoDB",
@@ -72,7 +71,7 @@
"is_virtual": 1,
"issingle": 1,
"links": [],
- "modified": "2024-03-23 16:03:34.140177",
+ "modified": "2026-05-30 23:16:00.919759",
"modified_by": "Administrator",
"module": "Core",
"name": "Permission Inspector",
@@ -84,7 +83,8 @@
"write": 1
}
],
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/report/report.json b/frappe/core/doctype/report/report.json
index 519184d57ba7..bb217dc7b96a 100644
--- a/frappe/core/doctype/report/report.json
+++ b/frappe/core/doctype/report/report.json
@@ -17,6 +17,7 @@
"add_total_row",
"disabled",
"prepared_report",
+ "disable_prepared_report_automation",
"add_translate_data",
"timeout",
"filters_section",
@@ -202,12 +203,20 @@
"fieldname": "add_translate_data",
"fieldtype": "Check",
"label": "Add Translate Data"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.report_type === \"Script Report\"",
+ "description": "If checked, Prepared Report will not be enabled automatically on slow runs.",
+ "fieldname": "disable_prepared_report_automation",
+ "fieldtype": "Check",
+ "label": "Disable Prepared Report Automation"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2025-08-28 18:28:32.510719",
+ "modified": "2026-06-04",
"modified_by": "Administrator",
"module": "Core",
"name": "Report",
@@ -248,10 +257,7 @@
"write": 1
},
{
- "email": 1,
- "print": 1,
"read": 1,
- "report": 1,
"role": "Desk User"
}
],
diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py
index cbc3f2ece9ea..088603369505 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -33,6 +33,7 @@ class Report(Document):
add_total_row: DF.Check
add_translate_data: DF.Check
columns: DF.Table[ReportColumn]
+ disable_prepared_report_automation: DF.Check
disabled: DF.Check
filters: DF.Table[ReportFilter]
is_standard: DF.Literal["No", "Yes"]
@@ -184,7 +185,7 @@ def execute_script_report(self, filters):
start_time = datetime.datetime.now()
prepared_report_watcher = None
- if not self.prepared_report:
+ if not self.prepared_report and not self.disable_prepared_report_automation:
prepared_report_watcher = threading.Timer(
interval=threshold,
function=enable_prepared_report,
@@ -370,28 +371,39 @@ def get_standard_report_order_by(self, params):
return order_by, group_by, group_by_args
def build_standard_report_columns(self, columns, group_by_args):
- _columns = []
+ from frappe.model.meta import get_default_df
+
+ report_columns = []
for fieldname, doctype in columns:
meta = frappe.get_meta(doctype)
- if meta.get_field(fieldname):
- field = meta.get_field(fieldname)
+ if meta_df := meta.get_field(fieldname):
+ column = meta_df.as_dict()
+ elif default_df := get_default_df(fieldname):
+ column = default_df.copy()
+
+ if not column.get("label"):
+ column.label = meta.get_label(fieldname)
else:
- if fieldname == "_aggregate_column":
- label = get_group_by_column_label(group_by_args, meta)
- else:
- label = meta.get_label(fieldname)
+ label = (
+ get_group_by_column_label(group_by_args, meta)
+ if fieldname == "_aggregate_column"
+ else meta.get_label(fieldname)
+ )
- field = frappe._dict(fieldname=fieldname, label=label)
+ column = frappe._dict(
+ {
+ "fieldname": fieldname,
+ "label": label,
+ "fieldtype": "Link" if fieldname == "name" else "Data",
+ "options": doctype if fieldname == "name" else None,
+ }
+ )
- # since name is the primary key for a document, it will always be a Link datatype
- if fieldname == "name":
- field.fieldtype = "Link"
- field.options = doctype
+ report_columns.append(column)
- _columns.append(field)
- return _columns
+ return report_columns
def build_data_dict(self, result, columns):
data = []
diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py
index 6a9ff688095d..1ed50d0c0492 100644
--- a/frappe/core/doctype/report/test_report.py
+++ b/frappe/core/doctype/report/test_report.py
@@ -407,36 +407,6 @@ def test_add_total_row_for_tree_reports(self):
self.assertEqual(result[-1][1], 200)
self.assertEqual(result[-1][2], 150.50)
- def test_cte_in_query_report(self):
- cte_query = textwrap.dedent(
- """
- with enabled_users as (
- select name
- from `tabUser`
- where enabled = 1
- )
- select * from enabled_users;
- """
- )
-
- report = frappe.get_doc(
- {
- "doctype": "Report",
- "ref_doctype": "User",
- "report_name": "Enabled Users List",
- "report_type": "Query Report",
- "is_standard": "No",
- "query": cte_query,
- }
- ).insert()
-
- if frappe.db.db_type == "mariadb":
- col, rows = report.execute_query_report(filters={})
- self.assertEqual(col[0], "name")
- self.assertGreaterEqual(len(rows), 1)
- elif frappe.db.db_type == "postgres":
- self.assertRaises(frappe.PermissionError, report.execute_query_report, filters={})
-
def test_report_cache_invalidation(self):
import frappe.sessions
from frappe.utils import set_request
diff --git a/frappe/core/doctype/sms_settings/sms_settings.py b/frappe/core/doctype/sms_settings/sms_settings.py
index 6d9207db8870..fa948316d472 100644
--- a/frappe/core/doctype/sms_settings/sms_settings.py
+++ b/frappe/core/doctype/sms_settings/sms_settings.py
@@ -151,6 +151,10 @@ def send_request(gateway_url, params, headers=None, use_post=False, use_json=Fal
# Create SMS Log
# =========================================================
def create_sms_log(args, sent_to):
+ # SMS Log doctype was removed; skip silently if it isn't available
+ # (apps that still ship it will continue to log).
+ if not frappe.db.exists("DocType", "SMS Log"):
+ return
sl = frappe.new_doc("SMS Log")
sl.sent_on = nowdate()
sl.message = args["message"].decode("utf-8")
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index d2a1b501503d..535a0a8eb5da 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -89,6 +89,7 @@
"delete_background_exported_reports_after",
"column_break_uqma",
"allowed_file_extensions",
+ "allowed_doctypes_for_guest_uploads",
"app_tab",
"default_app",
"updates_tab",
@@ -797,12 +798,19 @@
"fieldname": "allow_clearing_link_fields",
"fieldtype": "Check",
"label": "Allow Clearing Link Fields"
+ },
+ {
+ "depends_on": "eval:doc.allow_guests_to_upload_files == 1",
+ "description": "Provide a list of allowed Doctypes for file uploads. Each line should contain one allowed Doctype. If unset, uploads to all doctypes are allowed. Example: User Item Quotation",
+ "fieldname": "allowed_doctypes_for_guest_uploads",
+ "fieldtype": "Small Text",
+ "label": "Allowed Doctypes for Guest Uploads"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2026-04-14 16:26:19.634212",
+ "modified": "2026-05-21 10:32:53.594355",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py
index 2f26dad6d2bb..436a3c984e34 100644
--- a/frappe/core/doctype/system_settings/system_settings.py
+++ b/frappe/core/doctype/system_settings/system_settings.py
@@ -25,6 +25,7 @@ class SystemSettings(Document):
allow_login_after_fail: DF.Int
allow_login_using_mobile_number: DF.Check
allow_login_using_user_name: DF.Check
+ allowed_doctypes_for_guest_uploads: DF.SmallText | None
allowed_file_extensions: DF.SmallText | None
app_name: DF.Data | None
apply_strict_user_permissions: DF.Check
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 7e40e3ccd46e..2074c91d1dee 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -224,7 +224,7 @@ frappe.ui.form.on("User", {
if (
cint(frappe.boot.sysdefaults.enable_two_factor_auth) &&
- (frappe.session.user == doc.name || frappe.user.has_role("System Manager"))
+ frappe.user.has_role("System Manager")
) {
frm.add_custom_button(
__("Reset OTP Secret"),
@@ -374,8 +374,8 @@ frappe.ui.form.on("User", {
},
setup_impersonation: function (frm) {
if (
- frappe.session.user === "Administrator" &&
- frm.doc.name != "Administrator" &&
+ (frappe.session.user === "Administrator" || frm.has_perm("impersonate")) &&
+ frm.doc.name !== frappe.session.user &&
!frm.is_new()
) {
frm.add_custom_button(__("Impersonate"), () => {
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index f358a30a4394..cc0a8d147ffa 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -34,6 +34,8 @@
"modules_html",
"block_modules",
"home_settings",
+ "allowed_accounts_section",
+ "allowed_accounts",
"short_bio",
"gender",
"birth_date",
@@ -148,6 +150,7 @@
"label": "First Name",
"oldfieldname": "first_name",
"oldfieldtype": "Data",
+ "permlevel": 1,
"reqd": 1
},
{
@@ -155,7 +158,8 @@
"fieldtype": "Data",
"label": "Middle Name",
"oldfieldname": "middle_name",
- "oldfieldtype": "Data"
+ "oldfieldtype": "Data",
+ "permlevel": 1
},
{
"bold": 1,
@@ -163,7 +167,8 @@
"fieldtype": "Data",
"label": "Last Name",
"oldfieldname": "last_name",
- "oldfieldtype": "Data"
+ "oldfieldtype": "Data",
+ "permlevel": 1
},
{
"fieldname": "full_name",
@@ -179,7 +184,8 @@
"depends_on": "eval:doc.__islocal",
"fieldname": "send_welcome_email",
"fieldtype": "Check",
- "label": "Send Welcome Email"
+ "label": "Send Welcome Email",
+ "permlevel": 1
},
{
"default": "0",
@@ -202,6 +208,7 @@
"in_global_search": 1,
"in_standard_filter": 1,
"label": "Username",
+ "permlevel": 1,
"unique": 1
},
{
@@ -269,19 +276,22 @@
"label": "Gender",
"oldfieldname": "gender",
"oldfieldtype": "Select",
- "options": "Gender"
+ "options": "Gender",
+ "permlevel": 1
},
{
"fieldname": "phone",
"fieldtype": "Data",
"label": "Phone",
- "options": "Phone"
+ "options": "Phone",
+ "permlevel": 1
},
{
"fieldname": "mobile_no",
"fieldtype": "Data",
"label": "Mobile No",
"options": "Phone",
+ "permlevel": 1,
"unique": 1
},
{
@@ -290,13 +300,15 @@
"label": "Birth Date",
"no_copy": 1,
"oldfieldname": "birth_date",
- "oldfieldtype": "Date"
+ "oldfieldtype": "Date",
+ "permlevel": 1
},
{
"fieldname": "location",
"fieldtype": "Data",
"label": "Location",
- "no_copy": 1
+ "no_copy": 1,
+ "permlevel": 1
},
{
"fieldname": "banner_image",
@@ -310,13 +322,15 @@
{
"fieldname": "interest",
"fieldtype": "Small Text",
- "label": "Interests"
+ "label": "Interests",
+ "permlevel": 1
},
{
"fieldname": "bio",
"fieldtype": "Small Text",
"label": "Bio",
- "no_copy": 1
+ "no_copy": 1,
+ "permlevel": 1
},
{
"default": "0",
@@ -335,13 +349,15 @@
"fieldname": "new_password",
"fieldtype": "Password",
"label": "Set New Password",
- "no_copy": 1
+ "no_copy": 1,
+ "permlevel": 1
},
{
"default": "1",
"fieldname": "logout_all_sessions",
"fieldtype": "Check",
- "label": "Logout From All Devices After Changing Password"
+ "label": "Logout From All Devices After Changing Password",
+ "permlevel": 1
},
{
"fieldname": "reset_password_key",
@@ -378,7 +394,8 @@
"default": "0",
"fieldname": "document_follow_notify",
"fieldtype": "Check",
- "label": "Send Notifications For Documents Followed By Me"
+ "label": "Send Notifications For Documents Followed By Me",
+ "permlevel": 1
},
{
"default": "Daily",
@@ -386,7 +403,8 @@
"fieldname": "document_follow_frequency",
"fieldtype": "Select",
"label": "Frequency",
- "options": "Hourly\nDaily\nWeekly"
+ "options": "Hourly\nDaily\nWeekly",
+ "permlevel": 1
},
{
"collapsible": 1,
@@ -399,25 +417,29 @@
"default": "1",
"fieldname": "thread_notify",
"fieldtype": "Check",
- "label": "Send Notifications For Email Threads"
+ "label": "Send Notifications For Email Threads",
+ "permlevel": 1
},
{
"default": "0",
"fieldname": "send_me_a_copy",
"fieldtype": "Check",
- "label": "Send Me A Copy of Outgoing Emails"
+ "label": "Send Me A Copy of Outgoing Emails",
+ "permlevel": 1
},
{
"default": "1",
"fieldname": "allowed_in_mentions",
"fieldtype": "Check",
- "label": "Allowed In Mentions"
+ "label": "Allowed In Mentions",
+ "permlevel": 1
},
{
"fieldname": "email_signature",
"fieldtype": "Text Editor",
"label": "Email Signature",
- "no_copy": 1
+ "no_copy": 1,
+ "permlevel": 1
},
{
"fieldname": "user_emails",
@@ -487,7 +509,8 @@
"default": "2",
"fieldname": "simultaneous_sessions",
"fieldtype": "Int",
- "label": "Simultaneous Sessions"
+ "label": "Simultaneous Sessions",
+ "permlevel": 1
},
{
"bold": 1,
@@ -530,7 +553,8 @@
"description": "If enabled, user can login from any IP Address using Two Factor Auth, this can also be set for all users in System Settings",
"fieldname": "bypass_restrict_ip_check_if_2fa_enabled",
"fieldtype": "Check",
- "label": "Bypass Restricted IP Address Check If Two Factor Auth Enabled"
+ "label": "Bypass Restricted IP Address Check If Two Factor Auth Enabled",
+ "permlevel": 1
},
{
"fieldname": "column_break1",
@@ -585,7 +609,8 @@
"fieldname": "social_logins",
"fieldtype": "Table",
"label": "Social Logins",
- "options": "User Social Login"
+ "options": "User Social Login",
+ "permlevel": 1
},
{
"collapsible": 1,
@@ -604,7 +629,6 @@
"unique": 1
},
{
- "description": "\n Click here to learn about token-based authentication\n ",
"fieldname": "generate_keys",
"fieldtype": "Button",
"label": "Generate Keys",
@@ -643,7 +667,8 @@
"fieldname": "module_profile",
"fieldtype": "Link",
"label": "Module Profile",
- "options": "Module Profile"
+ "options": "Module Profile",
+ "permlevel": 1
},
{
"description": "Stores the datetime when the last reset password key was generated.",
@@ -663,35 +688,40 @@
"depends_on": "eval:(doc.document_follow_notify== 1)",
"fieldname": "follow_created_documents",
"fieldtype": "Check",
- "label": "Auto follow documents that you create"
+ "label": "Auto follow documents that you create",
+ "permlevel": 1
},
{
"default": "0",
"depends_on": "eval:(doc.document_follow_notify== 1)",
"fieldname": "follow_commented_documents",
"fieldtype": "Check",
- "label": "Auto follow documents that you comment on"
+ "label": "Auto follow documents that you comment on",
+ "permlevel": 1
},
{
"default": "0",
"depends_on": "eval:(doc.document_follow_notify== 1)",
"fieldname": "follow_liked_documents",
"fieldtype": "Check",
- "label": "Auto follow documents that you Like"
+ "label": "Auto follow documents that you Like",
+ "permlevel": 1
},
{
"default": "0",
"depends_on": "eval:(doc.document_follow_notify== 1)",
"fieldname": "follow_shared_documents",
"fieldtype": "Check",
- "label": "Auto follow documents that are shared with you"
+ "label": "Auto follow documents that are shared with you",
+ "permlevel": 1
},
{
"default": "0",
"depends_on": "eval:(doc.document_follow_notify== 1)",
"fieldname": "follow_assigned_documents",
"fieldtype": "Check",
- "label": "Auto follow documents that are assigned to you"
+ "label": "Auto follow documents that are assigned to you",
+ "permlevel": 1
},
{
"fieldname": "user_details_tab",
@@ -740,7 +770,8 @@
"fieldname": "default_workspace",
"fieldtype": "Link",
"label": "Default Workspace",
- "options": "Workspace"
+ "options": "Workspace",
+ "permlevel": 1
},
{
"collapsible": 1,
@@ -765,7 +796,8 @@
"description": "Redirect to the selected app after login",
"fieldname": "default_app",
"fieldtype": "Select",
- "label": "Default App"
+ "label": "Default App",
+ "permlevel": 1
},
{
"collapsible": 1,
@@ -777,13 +809,15 @@
"default": "1",
"fieldname": "search_bar",
"fieldtype": "Check",
- "label": "Search Bar"
+ "label": "Search Bar",
+ "permlevel": 1
},
{
"default": "1",
"fieldname": "notifications",
"fieldtype": "Check",
- "label": "Notifications"
+ "label": "Notifications",
+ "permlevel": 1
},
{
"collapsible": 1,
@@ -795,19 +829,22 @@
"default": "1",
"fieldname": "list_sidebar",
"fieldtype": "Check",
- "label": "Sidebar"
+ "label": "Sidebar",
+ "permlevel": 1
},
{
"default": "1",
"fieldname": "bulk_actions",
"fieldtype": "Check",
- "label": "Bulk Actions"
+ "label": "Bulk Actions",
+ "permlevel": 1
},
{
"default": "1",
"fieldname": "view_switcher",
"fieldtype": "Check",
- "label": "View Switcher"
+ "label": "View Switcher",
+ "permlevel": 1
},
{
"collapsible": 1,
@@ -819,19 +856,22 @@
"default": "1",
"fieldname": "form_sidebar",
"fieldtype": "Check",
- "label": "Sidebar"
+ "label": "Sidebar",
+ "permlevel": 1
},
{
"default": "1",
"fieldname": "timeline",
"fieldtype": "Check",
- "label": "Timeline"
+ "label": "Timeline",
+ "permlevel": 1
},
{
"default": "1",
"fieldname": "dashboard",
"fieldtype": "Check",
- "label": "Dashboard"
+ "label": "Dashboard",
+ "permlevel": 1
},
{
"default": "0",
@@ -857,6 +897,17 @@
"fieldname": "form_navigation_buttons",
"fieldtype": "Check",
"label": "Navigation Buttons"
+ },
+ {
+ "fieldname": "allowed_accounts_section",
+ "fieldtype": "Section Break",
+ "label": "Allowed Accounts"
+ },
+ {
+ "fieldname": "allowed_accounts",
+ "fieldtype": "Table",
+ "options": "User Account",
+ "permlevel": 1
}
],
"icon": "fa fa-user",
@@ -910,7 +961,7 @@
}
],
"make_attachments_public": 1,
- "modified": "2026-04-28 21:59:59.160099",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
@@ -938,6 +989,11 @@
{
"role": "Desk User",
"select": 1
+ },
+ {
+ "permlevel": 1,
+ "read": 1,
+ "role": "Desk User"
}
],
"quick_entry": 1,
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 5349e64c8fc1..db9374424df1 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -65,6 +65,7 @@ class User(Document):
from frappe.core.doctype.block_module.block_module import BlockModule
from frappe.core.doctype.defaultvalue.defaultvalue import DefaultValue
from frappe.core.doctype.has_role.has_role import HasRole
+ from frappe.core.doctype.user_account.user_account import UserAccount
from frappe.core.doctype.user_email.user_email import UserEmail
from frappe.core.doctype.user_role_profile.user_role_profile import UserRoleProfile
from frappe.core.doctype.user_session_display.user_session_display import UserSessionDisplay
@@ -72,6 +73,7 @@ class User(Document):
from frappe.types import DF
active_sessions: DF.Table[UserSessionDisplay]
+ allowed_accounts: DF.Table[UserAccount]
allowed_in_mentions: DF.Check
api_key: DF.Data | None
api_secret: DF.Password | None
diff --git a/frappe/core/doctype/user_account/__init__.py b/frappe/core/doctype/user_account/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/frappe/core/doctype/user_account/user_account.json b/frappe/core/doctype/user_account/user_account.json
new file mode 100644
index 000000000000..677fd4b4e8ca
--- /dev/null
+++ b/frappe/core/doctype/user_account/user_account.json
@@ -0,0 +1,46 @@
+{
+ "actions": [],
+ "creation": "2025-10-14",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": ["access_type", "account", "mode_of_payment"],
+ "fields": [
+ {
+ "fieldname": "access_type",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Access Type",
+ "options": "Payment\nRead",
+ "reqd": 1
+ },
+ {
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Account",
+ "options": "Account",
+ "reqd": 1
+ },
+ {
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Default Mode of Payment",
+ "options": "Mode of Payment"
+ }
+ ],
+ "grid_page_length": 50,
+ "istable": 1,
+ "links": [],
+ "modified": "2026-05-25",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "User Account",
+ "owner": "Administrator",
+ "permissions": [],
+ "row_format": "Dynamic",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
diff --git a/frappe/core/doctype/user_account/user_account.py b/frappe/core/doctype/user_account/user_account.py
new file mode 100644
index 000000000000..7c693558269b
--- /dev/null
+++ b/frappe/core/doctype/user_account/user_account.py
@@ -0,0 +1,21 @@
+from frappe.model.document import Document
+
+
+class UserAccount(Document):
+ # begin: auto-generated types
+ # This code is auto-generated. Do not modify anything in this block.
+
+ from typing import TYPE_CHECKING
+
+ if TYPE_CHECKING:
+ from frappe.types import DF
+
+ access_type: DF.Literal["Payment", "Read"]
+ account: DF.Link
+ mode_of_payment: DF.Link | None
+ parent: DF.Data
+ parentfield: DF.Data
+ parenttype: DF.Data
+ # end: auto-generated types
+
+ pass
diff --git a/frappe/core/doctype/user_group/user_group.json b/frappe/core/doctype/user_group/user_group.json
index fd49ac1266f5..9ea8a9ea94b7 100644
--- a/frappe/core/doctype/user_group/user_group.json
+++ b/frappe/core/doctype/user_group/user_group.json
@@ -19,7 +19,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-03-23 16:04:00.597980",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Core",
"name": "User Group",
@@ -37,10 +37,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "read": 1,
- "role": "Desk User"
}
],
"sort_field": "creation",
diff --git a/frappe/core/page/dashboard_view/dashboard_view.js b/frappe/core/page/dashboard_view/dashboard_view.js
index c9d8342f2c86..2b7e0a53ff79 100644
--- a/frappe/core/page/dashboard_view/dashboard_view.js
+++ b/frappe/core/page/dashboard_view/dashboard_view.js
@@ -88,10 +88,7 @@ class Dashboard {
"frappe.desk.doctype.dashboard.dashboard.get_permitted_charts"
).then((charts) => {
if (!charts.length) {
- frappe.msgprint(
- __("No Permitted Charts on this Dashboard"),
- __("No Permitted Charts")
- );
+ return;
}
frappe.dashboard_utils.get_dashboard_settings().then((settings) => {
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index 5cb6e9139856..f04563a1cbfc 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -24,6 +24,7 @@
"track_views",
"allow_auto_repeat",
"allow_import",
+ "allow_bulk_edit",
"queue_in_background",
"naming_section",
"naming_rule",
@@ -222,6 +223,14 @@
"fieldtype": "Check",
"label": "Allow Import (via Data Import Tool)"
},
+ {
+ "default": "1",
+ "depends_on": "istable",
+ "description": "Enable bulk edit for child table fields in Form view.",
+ "fieldname": "allow_bulk_edit",
+ "fieldtype": "Check",
+ "label": "Allow Bulk Edit"
+ },
{
"depends_on": "email_append_to",
"fieldname": "subject_field",
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 92b8b34a8b74..07deb438a742 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -42,6 +42,7 @@ class CustomizeForm(Document):
actions: DF.Table[DocTypeAction]
allow_auto_repeat: DF.Check
+ allow_bulk_edit: DF.Check
allow_copy: DF.Check
allow_import: DF.Check
autoname: DF.Data | None
@@ -744,6 +745,7 @@ def get_link_filters_from_doc_without_customisations(doctype, fieldname):
"track_views": "Check",
"allow_auto_repeat": "Check",
"allow_import": "Check",
+ "allow_bulk_edit": "Check",
"show_name_in_global_search": "Check",
"show_preview_popup": "Check",
"default_email_template": "Data",
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.json b/frappe/custom/doctype/doctype_layout/doctype_layout.json
index 131924bc776a..365b1ac8b6a4 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.json
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.json
@@ -43,7 +43,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-03-23 16:03:22.020755",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Custom",
"name": "DocType Layout",
@@ -61,10 +61,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "read": 1,
- "role": "Desk User"
}
],
"route": "doctype-layout",
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index c9c57b1c00cc..0c5b2c11a056 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -26,19 +26,19 @@ class MariaDBExceptionUtil:
@staticmethod
def is_deadlocked(e: pymysql.Error) -> bool:
# Snapshot isolation is also treated as deadlock from User POV
- return e.args[0] in (ER.LOCK_DEADLOCK, ER.CHECKREAD)
+ return e.args and e.args[0] in (ER.LOCK_DEADLOCK, ER.CHECKREAD)
@staticmethod
def is_timedout(e: pymysql.Error) -> bool:
- return e.args[0] == ER.LOCK_WAIT_TIMEOUT
+ return e.args and e.args[0] == ER.LOCK_WAIT_TIMEOUT
@staticmethod
def is_read_only_mode_error(e: pymysql.Error) -> bool:
- return e.args[0] == 1792
+ return e.args and e.args[0] == 1792
@staticmethod
def is_table_missing(e: pymysql.Error) -> bool:
- return e.args[0] == ER.NO_SUCH_TABLE
+ return e.args and e.args[0] == ER.NO_SUCH_TABLE
@staticmethod
def is_missing_table(e: pymysql.Error) -> bool:
@@ -46,39 +46,39 @@ def is_missing_table(e: pymysql.Error) -> bool:
@staticmethod
def is_missing_column(e: pymysql.Error) -> bool:
- return e.args[0] == ER.BAD_FIELD_ERROR
+ return e.args and e.args[0] == ER.BAD_FIELD_ERROR
@staticmethod
def is_duplicate_fieldname(e: pymysql.Error) -> bool:
- return e.args[0] == ER.DUP_FIELDNAME
+ return e.args and e.args[0] == ER.DUP_FIELDNAME
@staticmethod
def is_duplicate_entry(e: pymysql.Error) -> bool:
- return e.args[0] == ER.DUP_ENTRY
+ return e.args and e.args[0] == ER.DUP_ENTRY
@staticmethod
def is_access_denied(e: pymysql.Error) -> bool:
- return e.args[0] == ER.ACCESS_DENIED_ERROR
+ return e.args and e.args[0] == ER.ACCESS_DENIED_ERROR
@staticmethod
def cant_drop_field_or_key(e: pymysql.Error) -> bool:
- return e.args[0] == ER.CANT_DROP_FIELD_OR_KEY
+ return e.args and e.args[0] == ER.CANT_DROP_FIELD_OR_KEY
@staticmethod
def is_syntax_error(e: pymysql.Error) -> bool:
- return e.args[0] == ER.PARSE_ERROR
+ return e.args and e.args[0] == ER.PARSE_ERROR
@staticmethod
def is_statement_timeout(e: pymysql.Error) -> bool:
- return e.args[0] == 1969
+ return e.args and e.args[0] == 1969
@staticmethod
def is_data_too_long(e: pymysql.Error) -> bool:
- return e.args[0] == ER.DATA_TOO_LONG
+ return e.args and e.args[0] == ER.DATA_TOO_LONG
@staticmethod
def is_db_table_size_limit(e: pymysql.Error) -> bool:
- return e.args[0] == ER.TOO_BIG_ROWSIZE
+ return e.args and e.args[0] == ER.TOO_BIG_ROWSIZE
@staticmethod
def is_primary_key_violation(e: pymysql.Error) -> bool:
diff --git a/frappe/database/mariadb/mysqlclient.py b/frappe/database/mariadb/mysqlclient.py
index 2ccc5f079d4e..5c38dede77a6 100644
--- a/frappe/database/mariadb/mysqlclient.py
+++ b/frappe/database/mariadb/mysqlclient.py
@@ -29,19 +29,19 @@ class MariaDBExceptionUtil:
@staticmethod
def is_deadlocked(e: MySQLdb.Error) -> bool:
# Snapshot isolation is also treated as deadlock from User POV
- return e.args[0] in (ER.LOCK_DEADLOCK, ER.CHECKREAD)
+ return e.args and e.args[0] in (ER.LOCK_DEADLOCK, ER.CHECKREAD)
@staticmethod
def is_timedout(e: MySQLdb.Error) -> bool:
- return e.args[0] == ER.LOCK_WAIT_TIMEOUT
+ return e.args and e.args[0] == ER.LOCK_WAIT_TIMEOUT
@staticmethod
def is_read_only_mode_error(e: MySQLdb.Error) -> bool:
- return e.args[0] == ER.CANT_EXECUTE_IN_READ_ONLY_TRANSACTION
+ return e.args and e.args[0] == ER.CANT_EXECUTE_IN_READ_ONLY_TRANSACTION
@staticmethod
def is_table_missing(e: MySQLdb.Error) -> bool:
- return e.args[0] == ER.NO_SUCH_TABLE
+ return e.args and e.args[0] == ER.NO_SUCH_TABLE
@staticmethod
def is_missing_table(e: MySQLdb.Error) -> bool:
@@ -49,39 +49,39 @@ def is_missing_table(e: MySQLdb.Error) -> bool:
@staticmethod
def is_missing_column(e: MySQLdb.Error) -> bool:
- return e.args[0] == ER.BAD_FIELD_ERROR
+ return e.args and e.args[0] == ER.BAD_FIELD_ERROR
@staticmethod
def is_duplicate_fieldname(e: MySQLdb.Error) -> bool:
- return e.args[0] == ER.DUP_FIELDNAME
+ return e.args and e.args[0] == ER.DUP_FIELDNAME
@staticmethod
def is_duplicate_entry(e: MySQLdb.Error) -> bool:
- return e.args[0] == ER.DUP_ENTRY
+ return e.args and e.args[0] == ER.DUP_ENTRY
@staticmethod
def is_access_denied(e: MySQLdb.Error) -> bool:
- return e.args[0] == ER.ACCESS_DENIED_ERROR
+ return e.args and e.args[0] == ER.ACCESS_DENIED_ERROR
@staticmethod
def cant_drop_field_or_key(e: MySQLdb.Error) -> bool:
- return e.args[0] == ER.CANT_DROP_FIELD_OR_KEY
+ return e.args and e.args[0] == ER.CANT_DROP_FIELD_OR_KEY
@staticmethod
def is_syntax_error(e: MySQLdb.Error) -> bool:
- return e.args[0] == ER.PARSE_ERROR
+ return e.args and e.args[0] == ER.PARSE_ERROR
@staticmethod
def is_statement_timeout(e: MySQLdb.Error) -> bool:
- return e.args[0] == ER_STATEMENT_TIMEOUT
+ return e.args and e.args[0] == ER_STATEMENT_TIMEOUT
@staticmethod
def is_data_too_long(e: MySQLdb.Error) -> bool:
- return e.args[0] == ER.DATA_TOO_LONG
+ return e.args and e.args[0] == ER.DATA_TOO_LONG
@staticmethod
def is_db_table_size_limit(e: MySQLdb.Error) -> bool:
- return e.args[0] == ER.TOO_BIG_ROWSIZE
+ return e.args and e.args[0] == ER.TOO_BIG_ROWSIZE
@staticmethod
def is_primary_key_violation(e: MySQLdb.Error) -> bool:
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index 9039ff3356b7..2b703e8abda8 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -1,5 +1,3 @@
-from pymysql.constants.ER import DUP_ENTRY
-
import frappe
from frappe import _
from frappe.database.schema import DbColumn, DBTable
@@ -166,7 +164,7 @@ def alter(self):
if query := locals().get("query"): # this weirdness is to avoid potentially unbounded vars
print(f"Failed to alter schema using query: {query}")
- if e.args[0] == DUP_ENTRY:
+ if frappe.db.is_duplicate_entry(e):
fieldname = str(e).split("'")[-2]
frappe.throw(
_(
diff --git a/frappe/database/query.py b/frappe/database/query.py
index 3e4a7bad035f..7fbf2548d7ea 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -23,7 +23,7 @@
from frappe.model import OPTIONAL_FIELDS, get_permitted_fields
from frappe.model.base_document import DOCTYPES_FOR_DOCTYPE
from frappe.model.document import Document
-from frappe.query_builder import Criterion, Field, Order, functions
+from frappe.query_builder import Criterion, CustomFunction, Field, Order, functions
from frappe.query_builder.custom import Month, MonthName, Quarter
CORE_DOCTYPES = DOCTYPES_FOR_DOCTYPE | frozenset(
@@ -193,6 +193,9 @@ def _apply_datetime_field_filter_conversion(between_values: tuple | list, doctyp
"MONTHNAME": MonthName,
"QUARTER": Quarter,
"MONTH": Month,
+ "DATE": functions.Date,
+ "VALUEWRAPPER": ValueWrapper,
+ "TIME": CustomFunction("TIME", ["time"]),
}
# Functions that accept '*' as an argument (e.g., COUNT(*))
@@ -615,15 +618,17 @@ def _build_criterion_for_simple_filter(
# If _field is from a dynamic field, its name might be just the target fieldname.
# We need the original string ('link.target') or the fieldname from the main doctype.
original_field_name = field if isinstance(field, str) else _field.name
- # Check if the original field name exists in the *main* doctype meta
- main_meta = frappe.get_meta(self.doctype)
- if main_meta.has_field(original_field_name):
- _df = main_meta.get_field(original_field_name)
- ref_doctype = _df.options if _df else self.doctype
+ # When the filter targets a child table, resolve the field against
+ # the child doctype rather than the parent.
+ lookup_doctype = doctype or self.doctype
+ lookup_meta = frappe.get_meta(lookup_doctype)
+ if lookup_meta.has_field(original_field_name):
+ _df = lookup_meta.get_field(original_field_name)
+ ref_doctype = _df.options if _df else lookup_doctype
else:
- # If not in main doctype, assume it's a standard field like 'name' or refers to the main doctype itself
+ # If not in lookup doctype, assume it's a standard field like 'name' or refers to the lookup doctype itself
# This part might need refinement if nested set operators are used with dynamic fields.
- ref_doctype = self.doctype
+ ref_doctype = lookup_doctype
nodes = get_nested_set_hierarchy_result(ref_doctype, docname, hierarchy)
operator_fn = (
diff --git a/frappe/database/schema.py b/frappe/database/schema.py
index d29bff1615da..5922c2823107 100644
--- a/frappe/database/schema.py
+++ b/frappe/database/schema.py
@@ -146,7 +146,7 @@ def validate(self):
# case when the field is no longer a varchar
continue
current_length = current_length[0]
- if cint(current_length) != cint(new_length):
+ if cint(current_length) > cint(new_length):
try:
# check for truncation
max_length = frappe.db.sql(
diff --git a/frappe/database/sequence.py b/frappe/database/sequence.py
index f68ff7c0e607..6c515ca15705 100644
--- a/frappe/database/sequence.py
+++ b/frappe/database/sequence.py
@@ -97,3 +97,50 @@ def set_next_val(
"mariadb": f"SELECT SETVAL(`{scrub(doctype_name + slug)}`, {next_val}, {is_val_used})",
}
)
+
+
+def _get_existing_sequences() -> set[str]:
+ if db.db_type == "postgres":
+ rows = db.sql(
+ """SELECT sequence_name FROM information_schema.sequences
+ WHERE sequence_schema = 'public'"""
+ )
+ else:
+ rows = db.sql(
+ """SELECT TABLE_NAME FROM information_schema.TABLES
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'SEQUENCE'"""
+ )
+ return {r[0] for r in rows}
+
+
+def create_missing_sequences() -> list[str]:
+ """Recreate sequences for autoincrement doctypes whose sequence object is missing."""
+ import frappe
+ from frappe.query_builder.functions import Max
+
+ if db.db_type == "sqlite":
+ return []
+
+ doctypes = frappe.get_all(
+ "DocType",
+ filters={"autoname": "autoincrement", "issingle": 0, "is_virtual": 0},
+ pluck="name",
+ )
+ if not doctypes:
+ return []
+
+ existing = _get_existing_sequences()
+ created = []
+
+ for doctype in doctypes:
+ if scrub(f"{doctype}_id_seq") in existing:
+ continue
+
+ # align past existing rows to avoid name collisions; empty tables fall
+ # back to the default start (1), same as normal sequence creation
+ table = frappe.qb.DocType(doctype)
+ max_name = frappe.qb.from_(table).select(Max(table["name"])).run()[0][0]
+ create_sequence(doctype, check_not_exists=True, start_value=int(max_name) + 1 if max_name else 0)
+ created.append(doctype)
+
+ return created
diff --git a/frappe/desk/desk_views.py b/frappe/desk/desk_views.py
new file mode 100644
index 000000000000..67d9286c4581
--- /dev/null
+++ b/frappe/desk/desk_views.py
@@ -0,0 +1,258 @@
+# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
+# License: MIT. See LICENSE
+
+from functools import cached_property
+
+import frappe
+from frappe.permissions import has_permission
+from frappe.query_builder import DocType
+from frappe.query_builder.functions import Count
+from frappe.query_builder.terms import SubQuery
+from frappe.utils.data import cstr
+
+
+class DeskViews:
+ """Builds the desk views (workspaces, dashboards, pages and reports) for the boot payload."""
+
+ # allowed-entity caches refresh every six hours
+ CACHE_EXPIRY = 6 * 60 * 60
+
+ def __init__(self):
+ self.pages = {}
+ self.reports = {}
+ self.workspaces = {}
+ self.dashboards = []
+
+ def build_entities(self):
+ from frappe.desk.desktop import get_workspaces
+
+ self.pages = self.get_allowed_pages()
+ self.reports = self.get_allowed_reports()
+ self.workspaces = get_workspaces()
+ self.dashboards = self.get_allowed_dashboards(cache=True)
+ return self
+
+ def add_to_boot(self, bootinfo):
+ bootinfo.page_info = self.pages
+ bootinfo.allowed_reports = self.reports
+ bootinfo.workspaces = self.workspaces
+ bootinfo.dashboards = self.dashboards
+
+ # The properties below are the per-user view-permission data read by `is_item_allowed`.
+ # They load lazily and cache on the instance, so consumers don't need to populate them.
+
+ @cached_property
+ def allowed_pages(self):
+ return self.get_allowed_pages(cache=True)
+
+ @cached_property
+ def allowed_reports(self):
+ return self.get_allowed_reports(cache=True)
+
+ @cached_property
+ def allowed_dashboards(self):
+ return {d["name"] for d in self.get_allowed_dashboards(cache=True)}
+
+ @cached_property
+ def restricted_doctypes(self):
+ from frappe.cache_manager import build_domain_restricted_doctype_cache
+
+ return frappe.cache.get_value("domain_restricted_doctypes") or build_domain_restricted_doctype_cache()
+
+ @cached_property
+ def restricted_pages(self):
+ from frappe.cache_manager import build_domain_restricted_page_cache
+
+ return frappe.cache.get_value("domain_restricted_pages") or build_domain_restricted_page_cache()
+
+ def is_item_allowed(self, name, item_type, allowed_workspaces=None):
+ """Return whether the user may see a sidebar/workspace item.
+
+ Relies on the consumer setting `can_read`, `allowed_pages`, `allowed_reports`,
+ `allowed_dashboards`, `restricted_doctypes` and `restricted_pages` on the instance.
+ """
+ if frappe.session.user == "Administrator":
+ return True
+
+ item_type = item_type.lower()
+
+ if item_type == "doctype":
+ return (
+ name in (self.can_read or [])
+ and name in (self.restricted_doctypes or [])
+ and frappe.has_permission(name)
+ )
+ if item_type == "page":
+ return name in self.allowed_pages and name in self.restricted_pages
+ if item_type == "report":
+ return not frappe.db.get_value("Report", name, "disabled") and name in self.allowed_reports
+ if item_type == "dashboard":
+ return name in (self.allowed_dashboards or [])
+ if item_type in ("help", "url"):
+ return True
+ if item_type == "workspace":
+ return name in (allowed_workspaces or [])
+
+ return False
+
+ @classmethod
+ def get_allowed_pages(cls, cache=False, user: str | None = None):
+ return cls.get_user_pages_or_reports("Page", cache=cache, user=user)
+
+ @classmethod
+ def get_allowed_reports(cls, cache=False, user: str | None = None):
+ return cls.get_user_pages_or_reports("Report", cache=cache, user=user)
+
+ @classmethod
+ def get_allowed_report_names(cls, cache=False, user: str | None = None) -> set[str]:
+ return {cstr(report) for report in cls.get_allowed_reports(cache=cache, user=user).keys() if report}
+
+ @classmethod
+ def get_allowed_dashboards(cls, cache=False):
+ """Return dashboards the user is allowed to see.
+
+ A dashboard is permitted when the user can access at least one of its charts or cards.
+ Evaluated for the current session user and cached like pages and reports.
+ """
+ from frappe.desk.doctype.dashboard.dashboard import get_permitted_cards, get_permitted_charts
+
+ def build():
+ return [
+ {"name": name}
+ for name in frappe.get_all("Dashboard", pluck="name")
+ if get_permitted_charts(name) or get_permitted_cards(name)
+ ]
+
+ return cls._allowed_entity_cache("allowed_dashboards", frappe.session.user, build, cache=cache)
+
+ @classmethod
+ def _allowed_entity_cache(cls, key, user, builder, cache=False):
+ """Return the user's allowed entities for `key`, rebuilding and re-caching on a miss.
+
+ Pass `cache=True` to return a previously cached value instead of rebuilding. The result
+ is stored per-user and expires after `CACHE_EXPIRY` seconds.
+ """
+ if cache:
+ cached = frappe.cache.get_value(key, user=user)
+ if cached:
+ return cached
+
+ value = builder()
+ frappe.cache.set_value(key, value, user, cls.CACHE_EXPIRY)
+ return value
+
+ @classmethod
+ def get_user_pages_or_reports(cls, parent, cache=False, user: str | None = None):
+ if user is None:
+ user = frappe.session.user
+
+ return cls._allowed_entity_cache(
+ "has_role:" + parent,
+ user,
+ lambda: cls._build_user_pages_or_reports(parent, user),
+ cache=cache,
+ )
+
+ @classmethod
+ def _build_user_pages_or_reports(cls, parent, user):
+ roles = frappe.get_roles(user)
+ has_role = {}
+
+ page = DocType("Page")
+ report = DocType("Report")
+
+ is_report = parent == "Report"
+
+ if is_report:
+ columns = (report.name.as_("title"), report.ref_doctype, report.report_type)
+ else:
+ columns = (page.title.as_("title"),)
+
+ customRole = DocType("Custom Role")
+ hasRole = DocType("Has Role")
+ parentTable = DocType(parent)
+
+ # get pages or reports set on custom role
+ pages_with_custom_roles = (
+ frappe.qb.from_(customRole)
+ .from_(hasRole)
+ .from_(parentTable)
+ .select(
+ customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns
+ )
+ .where(
+ (hasRole.parent == customRole.name)
+ & (parentTable.name == customRole[parent.lower()])
+ & (customRole[parent.lower()].isnotnull())
+ & (hasRole.role.isin(roles))
+ )
+ ).run(as_dict=True)
+
+ for p in pages_with_custom_roles:
+ has_role[p.name] = {"modified": p.modified, "title": p.title, "ref_doctype": p.ref_doctype}
+
+ subq = (
+ frappe.qb.from_(customRole)
+ .select(customRole[parent.lower()])
+ .where(customRole[parent.lower()].isnotnull())
+ )
+
+ pages_with_standard_roles = (
+ frappe.qb.from_(hasRole)
+ .from_(parentTable)
+ .select(parentTable.name.as_("name"), parentTable.modified, *columns)
+ .where(
+ (hasRole.role.isin(roles))
+ & (hasRole.parent == parentTable.name)
+ & (parentTable.name.notin(subq))
+ )
+ .distinct()
+ )
+
+ if is_report:
+ pages_with_standard_roles = pages_with_standard_roles.where(report.disabled == 0)
+
+ pages_with_standard_roles = pages_with_standard_roles.run(as_dict=True)
+
+ for p in pages_with_standard_roles:
+ if p.name not in has_role:
+ has_role[p.name] = {"modified": p.modified, "title": p.title}
+ if parent == "Report":
+ has_role[p.name].update({"ref_doctype": p.ref_doctype})
+
+ no_of_roles = SubQuery(
+ frappe.qb.from_(hasRole).select(Count("*")).where(hasRole.parent == parentTable.name)
+ )
+
+ # pages and reports with no role are allowed
+ rows_with_no_roles = (
+ frappe.qb.from_(parentTable)
+ .select(parentTable.name, parentTable.modified, *columns)
+ .where(no_of_roles == 0)
+ ).run(as_dict=True)
+
+ for r in rows_with_no_roles:
+ if r.name not in has_role:
+ has_role[r.name] = {"modified": r.modified, "title": r.title}
+ if is_report:
+ has_role[r.name] |= {"ref_doctype": r.ref_doctype}
+
+ if is_report:
+ if not has_permission("Report", user=user, print_logs=False):
+ return {}
+
+ reports = frappe.get_list(
+ "Report",
+ fields=["name", "report_type"],
+ filters={"name": ("in", has_role.keys())},
+ ignore_ifnull=True,
+ user=user,
+ )
+ for report in reports:
+ has_role[report.name]["report_type"] = report.report_type
+
+ non_permitted_reports = set(has_role.keys()) - {r.name for r in reports}
+ for r in non_permitted_reports:
+ has_role.pop(r, None)
+
+ return has_role
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index fb1d2018802e..85793902255b 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -7,13 +7,9 @@
import frappe
from frappe import DoesNotExistError, ValidationError, _, _dict
-from frappe.boot import get_allowed_pages, get_allowed_reports
-from frappe.cache_manager import (
- build_domain_restricted_doctype_cache,
- build_domain_restricted_page_cache,
- build_table_count_cache,
-)
+from frappe.cache_manager import build_table_count_cache
from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles
+from frappe.desk.desk_views import DeskViews
def handle_not_exist(fn):
@@ -28,7 +24,7 @@ def wrapper(*args, **kwargs):
return wrapper
-class Workspace:
+class Workspace(DeskViews):
def __init__(self, page, minimal=False):
self.page_name = page.get("name")
self.page_title = page.get("title")
@@ -49,8 +45,8 @@ def __init__(self, page, minimal=False):
self.can_read = self.get_cached("user_perm_can_read", self.get_can_read_items)
- self.allowed_pages = get_allowed_pages(cache=True)
- self.allowed_reports = get_allowed_reports(cache=True)
+ self.allowed_pages = DeskViews.get_allowed_pages(cache=True)
+ self.allowed_reports = DeskViews.get_allowed_reports(cache=True)
if not minimal:
if self.doc.content:
@@ -59,12 +55,6 @@ def __init__(self, page, minimal=False):
]
self.table_counts = get_table_with_counts()
- self.restricted_doctypes = (
- frappe.cache.get_value("domain_restricted_doctypes") or build_domain_restricted_doctype_cache()
- )
- self.restricted_pages = (
- frappe.cache.get_value("domain_restricted_pages") or build_domain_restricted_page_cache()
- )
def is_permitted(self):
"""Return true if `Has Role` is not set or the user is allowed."""
@@ -131,24 +121,6 @@ def get_onboarding_doc(self, onboarding):
return doc
- def is_item_allowed(self, name, item_type):
- item_type = item_type.lower()
-
- if item_type == "doctype":
- return name in (self.can_read or []) and name in (self.restricted_doctypes or [])
- if item_type == "page":
- return name in self.allowed_pages and name in self.restricted_pages
- if item_type == "report":
- return not frappe.db.get_value("Report", name, "disabled") and name in self.allowed_reports
- if item_type == "help":
- return True
- if item_type == "dashboard":
- return True
- if item_type == "url":
- return True
-
- return False
-
def build_workspace(self):
self.cards = {"items": self.get_links()}
self.charts = {"items": self.get_charts()}
@@ -375,7 +347,7 @@ def get_desktop_page(page):
@frappe.whitelist()
-def get_workspace_sidebar_items():
+def get_workspaces():
"""Get list of sidebar items for desk"""
from frappe.modules.utils import get_module_app
diff --git a/frappe/desk/doctype/calendar_view/calendar_view.json b/frappe/desk/doctype/calendar_view/calendar_view.json
index 445c46a8e1bb..fa5d2853b35d 100644
--- a/frappe/desk/doctype/calendar_view/calendar_view.json
+++ b/frappe/desk/doctype/calendar_view/calendar_view.json
@@ -53,7 +53,7 @@
}
],
"links": [],
- "modified": "2024-03-23 16:01:29.668268",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Calendar View",
@@ -71,10 +71,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "read": 1,
- "role": "Desk User"
}
],
"sort_field": "creation",
diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.json b/frappe/desk/doctype/custom_html_block/custom_html_block.json
index 6d8aadfbc48c..5f6cf9ede092 100644
--- a/frappe/desk/doctype/custom_html_block/custom_html_block.json
+++ b/frappe/desk/doctype/custom_html_block/custom_html_block.json
@@ -93,7 +93,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-03-23 16:02:15.380899",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Custom HTML Block",
@@ -101,26 +101,8 @@
"owner": "Administrator",
"permissions": [
{
- "email": 1,
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
- "role": "Desk User",
- "share": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Desk User",
- "share": 1,
- "write": 1
+ "role": "Desk User"
},
{
"create": 1,
diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json
index 294abb64c3a4..0e3d023b671c 100644
--- a/frappe/desk/doctype/dashboard/dashboard.json
+++ b/frappe/desk/doctype/dashboard/dashboard.json
@@ -67,7 +67,7 @@
}
],
"links": [],
- "modified": "2024-03-23 16:02:16.071715",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",
@@ -98,13 +98,8 @@
"write": 1
},
{
- "email": 1,
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
- "role": "Desk User",
- "share": 1
+ "role": "Desk User"
}
],
"quick_entry": 1,
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index 0a84efa97a1e..97db8e325ab8 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -304,7 +304,7 @@
}
],
"links": [],
- "modified": "2025-06-08 22:49:08.587921",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
@@ -336,13 +336,8 @@
"write": 1
},
{
- "email": 1,
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
- "role": "Desk User",
- "share": 1
+ "role": "Desk User"
}
],
"sort_field": "creation",
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 2fe829b50bb5..b669911fd674 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -6,10 +6,11 @@
import frappe
from frappe import _
-from frappe.boot import get_allowed_report_names
+from frappe.desk.desk_views import DeskViews
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.utils import cint, flt, get_datetime, getdate, has_common, now_datetime, nowdate
from frappe.utils.dashboard import cache_source
from frappe.utils.data import format_date
@@ -38,9 +39,9 @@ def get_permission_query_conditions(user):
module_condition = False
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
- allowed_reports = [frappe.db.escape(report) for report in get_allowed_report_names()]
+ allowed_reports = [frappe.db.escape(report) for report in DeskViews.get_allowed_report_names(user=user)]
allowed_modules = [
- frappe.db.escape(module.get("module_name")) for module in get_modules_from_all_apps_for_user()
+ frappe.db.escape(module.get("module_name")) for module in get_modules_from_all_apps_for_user(user)
]
if allowed_doctypes:
@@ -76,10 +77,10 @@ def has_permission(doc, ptype, user):
if has_common(roles, allowed):
return True
elif doc.chart_type == "Report":
- if doc.report_name in get_allowed_report_names():
+ if doc.report_name in DeskViews.get_allowed_report_names(user=user):
return True
else:
- allowed_doctypes = frappe.permissions.get_doctypes_with_read()
+ allowed_doctypes = get_doctypes_with_read(user)
if doc.document_type in allowed_doctypes:
return True
@@ -284,14 +285,16 @@ def get_group_by_chart_config(chart, filters) -> dict | None:
) # get info about @group_by_field
if data and group_by_field_field.fieldtype == "Link": # if @group_by_field is link
- title_field = frappe.get_meta(group_by_field_field.options) # get title field
- if title_field.title_field: # if has title_field
- for item in data: # replace chart labels from name to title value
- item.name = frappe.get_value(group_by_field_field.options, item.name, title_field.title_field)
+ meta = frappe.get_meta(group_by_field_field.options) # get title field
+ for item in data: # replace chart labels from name to title value
+ if meta.title_field:
+ item.name = frappe.get_value(group_by_field_field.options, item.name, meta.title_field)
+ elif meta.translated_doctype:
+ item.name = _(item.get("name", "Not Specified"))
if data:
return {
- "labels": [item.get("name", "Not Specified") for item in data],
+ "labels": [item.name for item in data],
"datasets": [{"name": chart.name, "values": [item["count"] for item in data]}],
}
return None
diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.json b/frappe/desk/doctype/dashboard_settings/dashboard_settings.json
index 86b77187e3ab..219448eb60be 100644
--- a/frappe/desk/doctype/dashboard_settings/dashboard_settings.json
+++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.json
@@ -27,7 +27,7 @@
],
"in_create": 1,
"links": [],
- "modified": "2024-03-23 16:02:16.763875",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Settings",
@@ -35,13 +35,8 @@
"owner": "Administrator",
"permissions": [
{
- "email": 1,
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
"role": "Desk User",
- "share": 1,
"write": 1
}
],
diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.json b/frappe/desk/doctype/desktop_icon/desktop_icon.json
index b954aabe6043..1c0548506738 100644
--- a/frappe/desk/doctype/desktop_icon/desktop_icon.json
+++ b/frappe/desk/doctype/desktop_icon/desktop_icon.json
@@ -151,7 +151,7 @@
}
],
"links": [],
- "modified": "2026-02-04 13:59:30.578370",
+ "modified": "2026-05-29",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desktop Icon",
@@ -169,18 +169,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Desk User",
- "share": 1,
- "write": 1
}
],
"quick_entry": 1,
diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py
index 9ab50fb45a95..bc76a89c5181 100644
--- a/frappe/desk/doctype/desktop_icon/desktop_icon.py
+++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py
@@ -85,63 +85,6 @@ def delete_desktop_icon_file(self):
if os.path.exists(file_path):
os.remove(file_path)
- def is_permitted(self, bootinfo):
- icon_module = None
- if self.icon_type == "Link" and self.link_to:
- icon_module = frappe.db.get_value("Workspace", self.link_to, "module")
- # module permission check
- if icon_module:
- blocked_modules = frappe.get_cached_doc("User", frappe.session.user).get_blocked_modules()
- if icon_module in blocked_modules:
- return False
- # perform a permission check based on roles table (desktop icons)
- allowed_roles = [d.role for d in self.get("roles") or []]
- if allowed_roles and not set(allowed_roles).intersection(frappe.get_roles()):
- return False
- if self.icon_type == "Folder":
- return True
- elif self.icon_type == "App":
- return self.check_app_permission()
- else:
- try:
- items = bootinfo.workspace_sidebar_item[self.label.lower()]["items"]
-
- if len(items) and all(item["type"] == "Section Break" for item in items):
- return False
- if len(items) == 0:
- return False
- return True
- except KeyError:
- return False
-
- def check_app_permission(self):
- for a in frappe.get_installed_apps():
- if frappe.get_hooks(app_name=a)["app_title"][0] == self.label or self.app == a:
- app_detail = frappe.get_hooks("add_to_apps_screen", app_name=a)
- if len(app_detail) != 0:
- permission_method = app_detail[0].get("has_permission", None)
- if permission_method:
- return frappe.call(permission_method)
- else:
- return True
- else:
- # App hooks.py doesn't have add_to_apps_screen
- return True
-
- # def is_permitted(self):
- # """Return True if `Has Role` is not set or the user is allowed."""
- # from frappe.utils import has_common
-
- # allowed = [d.role for d in frappe.get_all("Has Role", fields=["role"], filters={"parent": self.name})]
-
- # if not allowed:
- # return True
-
- # roles = frappe.get_roles()
-
- # if has_common(roles, allowed):
- # return True
-
def after_insert(self):
clear_desktop_icons_cache()
@@ -160,6 +103,21 @@ def get_workspace_names(workspaces):
return workspace_list
+def check_app_permission(label, app):
+ for a in frappe.get_installed_apps():
+ if frappe.get_hooks(app_name=a)["app_title"][0] == label or app == a:
+ app_detail = frappe.get_hooks("add_to_apps_screen", app_name=a)
+ if len(app_detail) != 0:
+ permission_method = app_detail[0].get("has_permission", None)
+ if permission_method:
+ return frappe.call(permission_method)
+ else:
+ return True
+ else:
+ # App hooks.py doesn't have add_to_apps_screen
+ return True
+
+
def get_desktop_icons(user=None, bootinfo=None):
"""Return desktop icons for user"""
if not user:
@@ -211,8 +169,17 @@ def get_desktop_icons(user=None, bootinfo=None):
permitted_parent_labels = set()
if bootinfo:
for s in user_icons:
- icon = frappe.get_doc("Desktop Icon", s.name)
- if icon.is_permitted(bootinfo):
+ if s.icon_type == "Folder":
+ permitted = True
+ elif s.icon_type == "App":
+ permitted = check_app_permission(s.label, s.app)
+ else:
+ # Workspace Sidebar link: present in the boot map ⇒ user can see at least
+ # one item in it (get_sidebar_items already enforces this).
+ sidebar = bootinfo.workspace_sidebar_item.get(s.label.lower())
+ permitted = bool(sidebar and sidebar["items"])
+
+ if permitted:
permitted_icons.append(s)
if not s.parent_icon:
@@ -325,15 +292,23 @@ def create_user_icons(user, data):
@frappe.whitelist()
def add_workspace_to_desktop(workspace: str):
- sidebar = frappe.new_doc("Workspace Sidebar")
- sidebar_item = frappe.new_doc("Workspace Sidebar Item")
- sidebar_item.label = workspace
- sidebar_item.type = "Link"
- sidebar_item.link_to = workspace
- sidebar_item.link_type = "Workspace"
- sidebar.title = workspace
- sidebar.append("items", sidebar_item)
- sidebar.save()
+ if frappe.db.exists("Workspace Sidebar", workspace):
+ sidebar = frappe.get_doc("Workspace Sidebar", workspace)
+ else:
+ sidebar = frappe.new_doc("Workspace Sidebar")
+ sidebar.title = workspace
+
+ if not any(item.link_to == workspace for item in sidebar.get("items", [])):
+ sidebar_item = frappe.new_doc("Workspace Sidebar Item")
+ sidebar_item.label = workspace
+ sidebar_item.type = "Link"
+ sidebar_item.link_to = workspace
+ sidebar_item.link_type = "Workspace"
+ sidebar.append("items", sidebar_item)
+ sidebar.save()
+
+ if frappe.db.exists("Desktop Icon", workspace):
+ return {"icon": frappe.get_doc("Desktop Icon", workspace).as_dict()}
new_icon = frappe.new_doc("Desktop Icon")
new_icon.label = workspace
diff --git a/frappe/desk/doctype/desktop_layout/desktop_layout.json b/frappe/desk/doctype/desktop_layout/desktop_layout.json
index 91e516df8647..35a7ec3269b6 100644
--- a/frappe/desk/doctype/desktop_layout/desktop_layout.json
+++ b/frappe/desk/doctype/desktop_layout/desktop_layout.json
@@ -27,7 +27,7 @@
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2026-01-25 15:30:12.805037",
+ "modified": "2026-06-03",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desktop Layout",
@@ -49,13 +49,8 @@
{
"create": 1,
"delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
"role": "Desk User",
- "share": 1,
"write": 1
}
],
diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json
index 5b792d8d526a..f87676ba9af4 100644
--- a/frappe/desk/doctype/event/event.json
+++ b/frappe/desk/doctype/event/event.json
@@ -364,23 +364,13 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
- "modified": "2025-10-07 08:53:24.732646",
+ "modified": "2026-05-28",
"modified_by": "shariq@frappe.io",
"module": "Desk",
"name": "Event",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
- {
- "create": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Desk User",
- "share": 1,
- "write": 1
- },
{
"create": 1,
"delete": 1,
@@ -393,15 +383,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "All",
- "share": 1
}
],
"read_only": 1,
diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py
index 11674434312f..72962c8cf4ac 100644
--- a/frappe/desk/doctype/event/event.py
+++ b/frappe/desk/doctype/event/event.py
@@ -4,6 +4,7 @@
import json
from datetime import date, datetime
+from typing import Any
import frappe
import frappe.share
@@ -338,8 +339,14 @@ def send_event_digest():
@frappe.whitelist()
@http_cache(max_age=5 * 60, stale_while_revalidate=60 * 60)
def get_events(
- start: date, end: date, user: str | None = None, for_reminder: bool = False, filters=None
+ start: str | date,
+ end: str | date,
+ user: str | None = None,
+ for_reminder: bool = False,
+ filters: str | list | dict[str, Any] | None = None,
) -> list[frappe._dict]:
+ start, end = getdate(start), getdate(end)
+
caller = frappe.session.user
target_user = user or caller
diff --git a/frappe/desk/doctype/event/test_event.py b/frappe/desk/doctype/event/test_event.py
index 76679a05b7b6..b68a3c7ca482 100644
--- a/frappe/desk/doctype/event/test_event.py
+++ b/frappe/desk/doctype/event/test_event.py
@@ -8,7 +8,7 @@
from frappe.core.utils import find
from frappe.desk.doctype.event.event import get_events
from frappe.tests import IntegrationTestCase
-from frappe.tests.utils import make_test_objects
+from frappe.tests.utils import make_test_objects, toggle_test_mode
class TestEvent(IntegrationTestCase):
@@ -50,6 +50,26 @@ def test_event_list(self):
self.assertFalse("_Test Event 3" in subjects)
self.assertFalse("_Test Event 2" in subjects)
+ def test_get_events_for_different_timezone(self):
+ frappe.set_user("Administrator")
+ frappe.get_doc(
+ {
+ "doctype": "Event",
+ "subject": "_Test Event Different Timezone",
+ "event_type": "Public",
+ "starts_on": "2025-05-10 10:00:00",
+ }
+ ).insert()
+
+ original_in_test = frappe.in_test
+ toggle_test_mode(True)
+ try:
+ events = get_events(start="2025-04-30 23:00:00", end="2025-06-10 23:00:00", user="Administrator")
+ finally:
+ toggle_test_mode(original_in_test)
+
+ self.assertIn("_Test Event Different Timezone", [event.subject for event in events])
+
def test_revert_logic(self):
ev = frappe.get_doc(self.globalTestRecords["Event"][0]).insert()
name = ev.name
diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json
index aba602ae76ad..3fb9769d535c 100644
--- a/frappe/desk/doctype/form_tour/form_tour.json
+++ b/frappe/desk/doctype/form_tour/form_tour.json
@@ -178,7 +178,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-11-17 13:06:48.120836",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour",
@@ -196,10 +196,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "read": 1,
- "role": "Desk User"
}
],
"sort_field": "creation",
diff --git a/frappe/desk/doctype/global_search_doctype/global_search_doctype.json b/frappe/desk/doctype/global_search_doctype/global_search_doctype.json
index fd380ab47bbb..c0558999446f 100644
--- a/frappe/desk/doctype/global_search_doctype/global_search_doctype.json
+++ b/frappe/desk/doctype/global_search_doctype/global_search_doctype.json
@@ -1,11 +1,13 @@
{
"actions": [],
+ "allow_bulk_edit": 0,
"creation": "2019-09-13 21:33:55.551941",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "document_type"
+ "document_type",
+ "configure"
],
"fields": [
{
@@ -14,17 +16,24 @@
"in_list_view": 1,
"label": "Document Type",
"options": "DocType"
+ },
+ {
+ "fieldname": "configure",
+ "fieldtype": "Button",
+ "in_list_view": 1,
+ "label": "Configure"
}
],
"istable": 1,
"links": [],
- "modified": "2024-03-23 16:03:26.489242",
+ "modified": "2026-04-27 21:41:43.954439",
"modified_by": "Administrator",
"module": "Desk",
"name": "Global Search DocType",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.js b/frappe/desk/doctype/global_search_settings/global_search_settings.js
index 147a72eef16a..011dc5324901 100644
--- a/frappe/desk/doctype/global_search_settings/global_search_settings.js
+++ b/frappe/desk/doctype/global_search_settings/global_search_settings.js
@@ -1,6 +1,81 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
+function show_configure_search_fields_dialog(doctype, frm) {
+ frappe.call({
+ method: "frappe.desk.doctype.global_search_settings.global_search_settings.get_global_search_field_options",
+ args: { doctype },
+ callback(r) {
+ const options = r.message || [];
+
+ const dialog = new frappe.ui.Dialog({
+ title: __("Configure search fields"),
+ fields: [
+ {
+ fieldtype: "HTML",
+ fieldname: "doctype_heading",
+ },
+ {
+ fieldtype: "HTML",
+ fieldname: "search_bar",
+ },
+ {
+ fieldname: "search_fields",
+ fieldtype: "MultiCheck",
+ columns: 2,
+ sort_options: false,
+ options,
+ },
+ ],
+
+ primary_action_label: __("Save"),
+ primary_action() {
+ const checked = dialog.get_field("search_fields").get_checked_options();
+ frappe.call({
+ method: "frappe.desk.doctype.global_search_settings.global_search_settings.update_global_search_fields",
+ args: { doctype, fields: checked },
+ freeze: true,
+ freeze_message: __("Updating search index"),
+ callback: function (r) {
+ if (r.exc) {
+ frappe.msgprint(r.exc);
+ } else {
+ dialog.hide();
+ frappe.show_alert({
+ message: __("Search fields updated."),
+ indicator: "green",
+ });
+ if (frm) {
+ frm.refresh();
+ }
+ }
+ },
+ });
+ },
+ });
+
+ dialog.get_field("doctype_heading").$wrapper.html(`
+
${frappe.utils.escape_html(__(doctype))}
+ `);
+
+ dialog.get_field("search_bar").$wrapper.html(`
+
+
+
+ `);
+
+ dialog.show();
+
+ frappe.utils.setup_search(dialog.$body, ".unit-checkbox", ".label-area");
+ },
+ });
+}
+
frappe.ui.form.on("Global Search Settings", {
refresh: function (frm) {
frappe.realtime.on("global_search_settings", (data) => {
@@ -30,3 +105,16 @@ frappe.ui.form.on("Global Search Settings", {
});
},
});
+
+frappe.ui.form.on("Global Search DocType", {
+ configure: function (frm, cdt, cdn) {
+ const row = frappe.get_doc(cdt, cdn);
+ if (!row.document_type) {
+ frappe.msgprint(__("Please select Document Type first."));
+ return;
+ }
+ frappe.model.with_doctype(row.document_type, () => {
+ show_configure_search_fields_dialog(row.document_type, frm);
+ });
+ },
+});
diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.py b/frappe/desk/doctype/global_search_settings/global_search_settings.py
index f84c5e510825..e8414256d94f 100644
--- a/frappe/desk/doctype/global_search_settings/global_search_settings.py
+++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py
@@ -3,6 +3,8 @@
import frappe
from frappe import _
+from frappe.custom.doctype.customize_form.customize_form import CustomizeForm
+from frappe.model import NO_VALUE_FIELDS
from frappe.model.document import Document
@@ -101,3 +103,109 @@ def show_message(progress, msg):
{"progress": progress, "total": 3, "msg": msg},
user=frappe.session.user,
)
+
+
+def _eligible_global_search_docfields(meta):
+ for df in sorted(meta.fields, key=lambda x: x.idx or 0):
+ if df.fieldtype in NO_VALUE_FIELDS:
+ continue
+ if getattr(df, "hidden", False):
+ continue
+ if getattr(df, "is_virtual", False):
+ continue
+ yield df
+
+
+@frappe.whitelist()
+def get_global_search_field_options(doctype: str | None = None):
+ """
+ Get the global search field options which is list of fields with checked status if that field is in global search
+ and name is also included if show_name_in_global_search is set
+ """
+ if not doctype:
+ frappe.throw(_("Document Type is required"))
+
+ frappe.only_for("System Manager")
+
+ meta = frappe.get_meta(doctype)
+
+ options = [
+ {
+ "label": _("Document Name (ID)"),
+ "value": "name",
+ "checked": bool(getattr(meta, "show_name_in_global_search", False)),
+ }
+ ]
+
+ for df in _eligible_global_search_docfields(meta):
+ options.append(
+ {
+ "label": _(df.label, context=df.parent),
+ "value": df.fieldname,
+ "checked": bool(df.in_global_search),
+ }
+ )
+
+ return options
+
+
+def _customize_form_stub(doctype: str) -> CustomizeForm:
+ """In-memory Customize Form — same PS helpers as desk Customize Form."""
+ cf = frappe.new_doc("Customize Form")
+ cf.doc_type = doctype
+ return cf
+
+
+def _set_global_search_property_setter(cf: CustomizeForm, fieldname: str, enabled: bool) -> None:
+ """Toggle DocType / DocField flags via `CustomizeForm.make_property_setter` (same as Customize Form desk save)."""
+ val = 1 if enabled else 0
+ if fieldname == "name":
+ cf.make_property_setter("show_name_in_global_search", val, "Check")
+ else:
+ cf.make_property_setter("in_global_search", val, "Check", fieldname=fieldname)
+
+
+@frappe.whitelist()
+def update_global_search_fields(doctype: str, fields: str):
+ """Apply global-search field selection via the same Property Setter path as Customize Form."""
+
+ frappe.only_for("System Manager")
+ if not doctype:
+ frappe.throw(_("Document Type is required"))
+ if frappe.get_meta(doctype).module == "Core":
+ frappe.throw(_("Cannot configure Core DocTypes for Global Search."))
+
+ fields = frappe.parse_json(fields)
+ meta = frappe.get_meta(doctype)
+
+ # Current set of global search fields which are in the database
+ current = {df.fieldname for df in _eligible_global_search_docfields(meta) if df.in_global_search}
+ if bool(getattr(meta, "show_name_in_global_search", False)):
+ current.add("name")
+
+ # Desired set of global search fields which are in the request
+ desired = set(fields)
+
+ # So basically we need to add the fields that are in the request and are not in the database
+ # and remove the fields that are in the database and are not in the request
+
+ # Create a Customize Form stub to apply property setters
+ cf = _customize_form_stub(doctype)
+
+ # Add the fields that are in the request and are not in the database
+ for fieldname in desired - current:
+ _set_global_search_property_setter(cf, fieldname, True)
+
+ # Remove the fields that are in the database and are not in the request
+ for fieldname in current - desired:
+ _set_global_search_property_setter(cf, fieldname, False)
+
+ # Clear the cache and enqueue the rebuild for the doctype
+ frappe.clear_cache(doctype=doctype)
+ frappe.enqueue(
+ "frappe.utils.global_search.rebuild_for_doctype",
+ doctype=doctype,
+ enqueue_after_commit=True,
+ )
+
+ return {"success": True}
diff --git a/frappe/desk/doctype/kanban_board/kanban_board.json b/frappe/desk/doctype/kanban_board/kanban_board.json
index f15b2e755b6e..0bf3f471f722 100644
--- a/frappe/desk/doctype/kanban_board/kanban_board.json
+++ b/frappe/desk/doctype/kanban_board/kanban_board.json
@@ -84,7 +84,7 @@
}
],
"links": [],
- "modified": "2024-03-23 16:03:28.240133",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Kanban Board",
@@ -92,16 +92,8 @@
"owner": "Administrator",
"permissions": [
{
- "read": 1,
- "role": "Desk User"
- },
- {
- "create": 1,
- "delete": 1,
- "if_owner": 1,
- "read": 1,
"role": "Desk User",
- "write": 1
+ "select": 1
},
{
"create": 1,
diff --git a/frappe/desk/doctype/list_filter/list_filter.json b/frappe/desk/doctype/list_filter/list_filter.json
index 2d029a3f6f01..3f6e7e2dd597 100644
--- a/frappe/desk/doctype/list_filter/list_filter.json
+++ b/frappe/desk/doctype/list_filter/list_filter.json
@@ -36,7 +36,7 @@
],
"in_create": 1,
"links": [],
- "modified": "2024-03-23 16:03:29.075357",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "List Filter",
@@ -45,13 +45,8 @@
{
"create": 1,
"delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
"role": "Desk User",
- "share": 1,
"write": 1
}
],
diff --git a/frappe/desk/doctype/module_onboarding/module_onboarding.json b/frappe/desk/doctype/module_onboarding/module_onboarding.json
index 099c75083e16..8a7c2bfc7fc2 100644
--- a/frappe/desk/doctype/module_onboarding/module_onboarding.json
+++ b/frappe/desk/doctype/module_onboarding/module_onboarding.json
@@ -61,7 +61,7 @@
}
],
"links": [],
- "modified": "2026-02-20 13:30:25.659490",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Module Onboarding",
@@ -78,15 +78,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Desk User",
- "share": 1
}
],
"read_only": 1,
diff --git a/frappe/desk/doctype/note/note.json b/frappe/desk/doctype/note/note.json
index 530e084c1948..9da2f20c11d4 100644
--- a/frappe/desk/doctype/note/note.json
+++ b/frappe/desk/doctype/note/note.json
@@ -87,7 +87,7 @@
"icon": "fa fa-file-text",
"idx": 1,
"links": [],
- "modified": "2025-03-26 18:19:45.079747",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Note",
@@ -119,19 +119,14 @@
"write": 1
},
{
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
"role": "Desk User"
},
{
"create": 1,
"delete": 1,
- "email": 1,
"if_owner": 1,
"role": "Desk User",
- "share": 1,
"write": 1
},
{
diff --git a/frappe/desk/doctype/note/note.py b/frappe/desk/doctype/note/note.py
index 179994e5a798..aab8b239e72c 100644
--- a/frappe/desk/doctype/note/note.py
+++ b/frappe/desk/doctype/note/note.py
@@ -56,6 +56,7 @@ def mark_seen_by(self, user: str) -> None:
@frappe.whitelist()
def mark_as_seen(note: str):
note: Note = frappe.get_doc("Note", note)
+ note.check_permission("read")
note.mark_seen_by(frappe.session.user)
note.save(ignore_permissions=True, ignore_version=True)
diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json
index 4411780a9e7d..87f5b31201f5 100644
--- a/frappe/desk/doctype/notification_log/notification_log.json
+++ b/frappe/desk/doctype/notification_log/notification_log.json
@@ -109,20 +109,15 @@
"hide_toolbar": 1,
"in_create": 1,
"links": [],
- "modified": "2025-05-30 20:17:44.969738",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Log",
"owner": "Administrator",
"permissions": [
{
- "email": 1,
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
- "role": "All",
- "share": 1
+ "role": "All"
}
],
"row_format": "Dynamic",
diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
index 5b4f49526c39..8eb5ab52c5d6 100644
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -199,7 +199,13 @@ def mark_as_read(docname: str):
return
if docname:
- frappe.db.set_value("Notification Log", str(docname), "read", 1, update_modified=False)
+ frappe.db.set_value(
+ "Notification Log",
+ {"name": str(docname), "for_user": frappe.session.user},
+ "read",
+ 1,
+ update_modified=False,
+ )
@frappe.whitelist()
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json
index 7f73c2bff44b..88521b19e046 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.json
+++ b/frappe/desk/doctype/notification_settings/notification_settings.json
@@ -37,7 +37,7 @@
"label": "Email Settings"
},
{
- "default": "1",
+ "default": "0",
"fieldname": "enable_email_notifications",
"fieldtype": "Check",
"label": "Enable Email Notifications"
@@ -98,7 +98,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2026-02-24 11:06:24.112935",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Settings",
@@ -107,13 +107,8 @@
"permissions": [
{
"create": 1,
- "email": 1,
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
"role": "All",
- "share": 1,
"write": 1
}
],
diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json
index 089786fbdcac..cd82452b7008 100644
--- a/frappe/desk/doctype/number_card/number_card.json
+++ b/frappe/desk/doctype/number_card/number_card.json
@@ -55,11 +55,11 @@
"options": "Count\nSum\nAverage\nMinimum\nMaximum"
},
{
- "depends_on": "eval: doc.type === 'Document Type' && doc.function !== 'Count'",
+ "depends_on": "eval: doc.type === 'Document Type' && doc.function !== 'Count'",
"fieldname": "aggregate_function_based_on",
"fieldtype": "Select",
"label": "Aggregate Function Based On",
- "mandatory_depends_on": "eval: doc.function !== 'Count'"
+ "mandatory_depends_on": "eval: doc.type === 'Document Type' && doc.function !== 'Count'"
},
{
"fieldname": "filters_json",
@@ -229,7 +229,7 @@
}
],
"links": [],
- "modified": "2025-09-17 21:00:11.351605",
+ "modified": "2026-05-29",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",
@@ -260,13 +260,8 @@
"write": 1
},
{
- "email": 1,
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
- "role": "Desk User",
- "share": 1
+ "role": "Desk User"
}
],
"row_format": "Dynamic",
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index 19b45ea589f5..20951f392dfa 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -3,7 +3,7 @@
import frappe
from frappe import _
-from frappe.boot import get_allowed_report_names
+from frappe.desk.desk_views import DeskViews
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
@@ -79,42 +79,40 @@ def on_update(self):
def get_permission_query_conditions(user=None):
- # 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
+ if not user:
+ user = frappe.session.user
- if "System Manager" in frappe.get_roles():
+ if user == "Administrator" or "System Manager" in frappe.get_roles(user):
return
- 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_reports = DeskViews.get_allowed_report_names(user=user)
+ allowed_doctypes = get_doctypes_with_read(user)
+ allowed_modules = [module.get("module_name") for module in get_modules_from_all_apps_for_user(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 == "")
+ ) & (nc.module.isin(allowed_modules) | nc.module.isnull() | (nc.module == ""))
return conditions.get_sql(quote_char="`")
def has_permission(doc, ptype, user):
- # 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 not user:
+ user = frappe.session.user
- if "System Manager" in frappe.get_roles():
+ if user == "Administrator" or "System Manager" in frappe.get_roles(user):
return True
- if doc.type == "Report" and doc.report_name in get_allowed_report_names():
+ if doc.type == "Report" and doc.report_name in DeskViews.get_allowed_report_names(user=user):
return True
- if doc.type == "Custom" and doc.document_type in get_doctypes_with_read():
+ if doc.type == "Custom" and doc.document_type in get_doctypes_with_read(user):
return True
- if doc.type == "Document Type" and doc.document_type in get_doctypes_with_read():
+ if doc.type == "Document Type" and doc.document_type in get_doctypes_with_read(user):
return True
return False
@@ -221,9 +219,14 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
filters=filters,
)
+ allowed_modules = {module.get("module_name") for module in get_modules_from_all_apps_for_user()}
+
return (
condition_query.select(numberCard.name, numberCard.label, numberCard.document_type)
.where((numberCard.owner == frappe.session.user) | (numberCard.is_public == 1))
+ .where(
+ numberCard.module.isin(allowed_modules) | numberCard.module.isnull() | (numberCard.module == "")
+ )
.where(Criterion.any(search_conditions))
).run()
diff --git a/frappe/desk/doctype/number_card/test_number_card.py b/frappe/desk/doctype/number_card/test_number_card.py
index ee78aeb7f66b..f6e465acd309 100644
--- a/frappe/desk/doctype/number_card/test_number_card.py
+++ b/frappe/desk/doctype/number_card/test_number_card.py
@@ -1,8 +1,137 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# License: MIT. See LICENSE
-# import frappe
+import frappe
+from frappe.desk.doctype.number_card.number_card import get_cards_for_user
from frappe.tests import IntegrationTestCase
class TestNumberCard(IntegrationTestCase):
- pass
+ def test_link_search_hides_cards_from_blocked_modules(self):
+ user = "test2@example.com"
+ blocked_module = "Contacts"
+ blocked_card_name = "Test Blocked Module Card"
+ allowed_card_name = "Test No Module Card"
+
+ frappe.set_user("Administrator")
+
+ admin_blocked = frappe.get_cached_doc("User", "Administrator").get_blocked_modules()
+ self.assertNotIn(blocked_module, admin_blocked, f"{blocked_module} globally blocked — test invalid")
+
+ for card_name in (blocked_card_name, allowed_card_name):
+ frappe.delete_doc("Number Card", card_name, ignore_missing=True, force=True)
+ self.addCleanup(
+ lambda c=card_name: frappe.delete_doc("Number Card", c, ignore_missing=True, force=True)
+ )
+
+ user_doc = frappe.get_doc("User", user)
+ if blocked_module not in {d.module for d in user_doc.block_modules}:
+ user_doc.append("block_modules", {"module": blocked_module})
+ user_doc.save(ignore_permissions=True)
+ frappe.clear_document_cache("User", user)
+
+ def restore_blocks():
+ doc = frappe.get_doc("User", user)
+ doc.block_modules = {d for d in doc.block_modules if d.module != blocked_module}
+ doc.save(ignore_permissions=True)
+ frappe.clear_document_cache("User", user)
+
+ self.addCleanup(restore_blocks)
+
+ frappe.get_doc(
+ {
+ "doctype": "Number Card",
+ "label": blocked_card_name,
+ "type": "Document Type",
+ "document_type": "ToDo",
+ "function": "Count",
+ "is_public": 1,
+ "module": blocked_module,
+ }
+ ).insert(ignore_permissions=True)
+
+ frappe.get_doc(
+ {
+ "doctype": "Number Card",
+ "label": allowed_card_name,
+ "type": "Document Type",
+ "document_type": "ToDo",
+ "function": "Count",
+ "is_public": 1,
+ }
+ ).insert(ignore_permissions=True)
+
+ frappe.set_user(user)
+ try:
+ blocked_results = get_cards_for_user(
+ doctype="Number Card",
+ txt=blocked_card_name,
+ searchfield="name",
+ start=0,
+ page_len=20,
+ filters={},
+ )
+ allowed_results = get_cards_for_user(
+ doctype="Number Card",
+ txt=allowed_card_name,
+ searchfield="name",
+ start=0,
+ page_len=20,
+ filters={},
+ )
+ finally:
+ frappe.set_user("Administrator")
+
+ self.assertEqual([row[0] for row in blocked_results], [])
+ self.assertEqual([row[0] for row in allowed_results], [allowed_card_name])
+
+ def test_report_card_hidden_when_report_is_not_allowed(self):
+ user = "test2@example.com"
+ report_name = "Test Restricted Number Card Report"
+ card_name = "Test Restricted Report Number Card"
+ baseline_role = "Desk User"
+ exclusive_role = "System Manager"
+
+ frappe.set_user("Administrator")
+ frappe.delete_doc("Number Card", card_name, ignore_missing=True, force=True)
+ frappe.delete_doc("Report", report_name, ignore_missing=True, force=True)
+ self.addCleanup(lambda: frappe.delete_doc("Number Card", card_name, ignore_missing=True, force=True))
+ self.addCleanup(lambda: frappe.delete_doc("Report", report_name, ignore_missing=True, force=True))
+
+ user_doc = frappe.get_doc("User", user)
+ had_baseline_role = baseline_role in frappe.get_roles(user)
+ if not had_baseline_role:
+ user_doc.add_roles(baseline_role)
+ self.addCleanup(lambda: user_doc.remove_roles(baseline_role))
+
+ had_exclusive_role = exclusive_role in frappe.get_roles(user)
+ if had_exclusive_role:
+ user_doc.remove_roles(exclusive_role)
+ self.addCleanup(lambda: user_doc.add_roles(exclusive_role))
+
+ report = frappe.get_doc(
+ {
+ "doctype": "Report",
+ "report_name": report_name,
+ "ref_doctype": "ToDo",
+ "report_type": "Report Builder",
+ "is_standard": "No",
+ "roles": [{"role": exclusive_role}],
+ }
+ ).insert(ignore_permissions=True)
+
+ card = frappe.get_doc(
+ {
+ "doctype": "Number Card",
+ "label": card_name,
+ "type": "Report",
+ "report_name": report.name,
+ "function": "Count",
+ "report_field": "name",
+ }
+ ).insert(ignore_permissions=True)
+
+ self.assertFalse(frappe.has_permission("Number Card", doc=card, user=user))
+ self.assertNotIn(
+ card.name,
+ frappe.get_list("Number Card", filters={"name": card.name}, pluck="name", user=user),
+ )
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.json b/frappe/desk/doctype/onboarding_step/onboarding_step.json
index 7037c334bf2d..f97a8f97ac32 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.json
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.json
@@ -228,7 +228,7 @@
}
],
"links": [],
- "modified": "2026-02-23 21:03:51.131292",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Step",
@@ -246,15 +246,6 @@
"role": "Administrator",
"share": 1,
"write": 1
- },
- {
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Desk User",
- "share": 1
}
],
"quick_entry": 1,
diff --git a/frappe/desk/doctype/sidebar_item_group/sidebar_item_group.py b/frappe/desk/doctype/sidebar_item_group/sidebar_item_group.py
index 7b72e2139f90..6759d1357c5e 100644
--- a/frappe/desk/doctype/sidebar_item_group/sidebar_item_group.py
+++ b/frappe/desk/doctype/sidebar_item_group/sidebar_item_group.py
@@ -4,7 +4,7 @@
import os
import frappe
-from frappe.boot import get_allowed_pages, get_allowed_reports
+from frappe.desk.desk_views import DeskViews
from frappe.model.document import Document
from frappe.modules.utils import create_directory_on_app_path, get_app_level_directory_path
@@ -48,11 +48,12 @@ def delete_file(self):
@frappe.whitelist()
-def get_reports(module_name=None):
+def get_reports(module_name: str | None = None):
reports_info = []
if module_name:
sidebar_group = frappe.get_doc("Sidebar Item Group", module_name)
for report_links in sidebar_group.links:
- if report_links.report in get_allowed_reports().keys():
- reports_info.append(get_allowed_reports()[report_links.report])
+ allowed_reports = DeskViews.get_allowed_reports()
+ if report_links.report in allowed_reports:
+ reports_info.append(allowed_reports[report_links.report])
return reports_info
diff --git a/frappe/desk/doctype/tag/tag.json b/frappe/desk/doctype/tag/tag.json
index 5385a89c72d7..adb897f5e19b 100644
--- a/frappe/desk/doctype/tag/tag.json
+++ b/frappe/desk/doctype/tag/tag.json
@@ -17,7 +17,7 @@
}
],
"links": [],
- "modified": "2024-03-23 16:03:39.682626",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Tag",
@@ -37,13 +37,8 @@
},
{
"create": 1,
- "email": 1,
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
"role": "All",
- "share": 1,
"write": 1
}
],
diff --git a/frappe/desk/doctype/tag_link/tag_link.json b/frappe/desk/doctype/tag_link/tag_link.json
index 9613c7d0ed64..7c6e77979867 100644
--- a/frappe/desk/doctype/tag_link/tag_link.json
+++ b/frappe/desk/doctype/tag_link/tag_link.json
@@ -46,7 +46,7 @@
}
],
"links": [],
- "modified": "2025-02-04 12:02:03.779623",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "Tag Link",
@@ -66,13 +66,8 @@
},
{
"create": 1,
- "email": 1,
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
"role": "All",
- "share": 1,
"write": 1
}
],
diff --git a/frappe/desk/doctype/todo/todo.json b/frappe/desk/doctype/todo/todo.json
index 3186a0a84609..c7b589a6e971 100644
--- a/frappe/desk/doctype/todo/todo.json
+++ b/frappe/desk/doctype/todo/todo.json
@@ -161,7 +161,7 @@
"icon": "fa fa-check",
"idx": 2,
"links": [],
- "modified": "2024-03-23 16:03:58.758787",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Desk",
"name": "ToDo",
@@ -170,12 +170,8 @@
{
"create": 1,
"delete": 1,
- "email": 1,
- "print": 1,
"read": 1,
- "report": 1,
"role": "All",
- "share": 1,
"write": 1
},
{
diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json
index f3422cd6eb99..16e4a2b9ef5b 100644
--- a/frappe/desk/doctype/workspace/workspace.json
+++ b/frappe/desk/doctype/workspace/workspace.json
@@ -2,7 +2,6 @@
"actions": [],
"allow_rename": 1,
"autoname": "field:label",
- "beta": 1,
"creation": "2020-01-23 13:45:59.470592",
"doctype": "DocType",
"editable_grid": 1,
@@ -262,7 +261,7 @@
],
"in_create": 1,
"links": [],
- "modified": "2024-09-26 15:47:24.710881",
+ "modified": "2026-05-30",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
@@ -289,13 +288,14 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "Desk User",
+ "role": "System Manager",
"share": 1,
"write": 1
}
],
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index cddafa1290c8..2030934d7f6f 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -7,7 +7,7 @@
import frappe
from frappe import _
from frappe.boot import get_sidebar_items
-from frappe.desk.desktop import get_workspace_sidebar_items, save_new_widget
+from frappe.desk.desktop import get_workspaces, save_new_widget
from frappe.desk.doctype.workspace_sidebar.workspace_sidebar import add_to_my_workspace
from frappe.desk.utils import validate_route_conflict
from frappe.model.document import Document
@@ -292,7 +292,7 @@ def get_report_type(report):
@frappe.whitelist()
-def new_page(new_page):
+def new_page(new_page: str):
if not loads(new_page):
return
@@ -331,7 +331,7 @@ def new_page(new_page):
# add to workspace sidebar items
if not doc.public:
add_to_my_workspace(doc)
- workspaces = get_workspace_sidebar_items()
+ workspaces = get_workspaces()
return {"workspace_pages": workspaces, "sidebar_items": get_sidebar_items(workspaces)}
@@ -340,8 +340,12 @@ def save_page(name, public, new_widgets, blocks):
public = frappe.parse_json(public)
doc = frappe.get_doc("Workspace", name)
- if not (is_workspace_manager() and doc.for_user == frappe.session.user):
- return
+ can_edit = is_workspace_manager() or (not doc.public and doc.for_user == frappe.session.user)
+ if not can_edit:
+ frappe.throw(
+ _("You need the Workspace Manager role to edit this workspace."),
+ frappe.PermissionError,
+ )
if not doc.type:
doc.type = "Workspace"
@@ -358,9 +362,11 @@ def update_page(name, title, icon, indicator_color, parent, public):
public = frappe.parse_json(public)
doc = frappe.get_doc("Workspace", name)
- if not doc.get("public") and doc.get("for_user") != frappe.session.user and not is_workspace_manager():
+ if doc.get("public") and not is_workspace_manager():
+ frappe.throw(_("Need Workspace Manager role to edit public workspaces."))
+ elif not doc.get("public") and doc.get("for_user") != frappe.session.user and not is_workspace_manager():
frappe.throw(
- _("Need Workspace Manager role to edit private workspace of other users"),
+ _("Need Workspace Manager role to edit private workspace of other users."),
frappe.PermissionError,
)
diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json
index 9b14ba8d4ad2..ebe8f77c2fde 100644
--- a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json
+++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json
@@ -79,7 +79,7 @@
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2026-02-20 15:19:27.520469",
+ "modified": "2026-05-29",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace Sidebar",
@@ -97,18 +97,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Desk User",
- "share": 1,
- "write": 1
}
],
"row_format": "Dynamic",
diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py
index a1fe322d71ac..534ad483cb0f 100644
--- a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py
+++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py
@@ -8,14 +8,14 @@
import frappe
from frappe import _
-from frappe.boot import get_allowed_pages, get_allowed_reports
+from frappe.desk.desk_views import DeskViews
from frappe.model.document import Document
from frappe.modules.export_file import strip_default_fields
from frappe.modules.utils import create_directory_on_app_path
from frappe.utils.caching import site_cache
-class WorkspaceSidebar(Document):
+class WorkspaceSidebar(Document, DeskViews):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -39,12 +39,6 @@ def __init__(self, *args, **kwargs):
if not frappe.flags.in_migrate:
self.user = frappe.get_user()
self.can_read = self.get_cached("user_perm_can_read", self.get_can_read_items)
- self.allowed_modules = self.get_cached("user_allowed_modules", self.get_allowed_modules)
-
- self.allowed_pages = get_allowed_pages(cache=True)
- self.allowed_reports = get_allowed_reports(cache=True)
- self.restricted_doctypes = frappe.cache.get_value("domain_restricted_doctypes")
- self.restricted_pages = frappe.cache.get_value("domain_restricted_pages")
def get_can_read_items(self):
if not self.user.can_read:
@@ -75,33 +69,9 @@ def on_trash(self):
frappe.throw(_("You need to be Workspace Manager to delete a public workspace."))
def after_rename(self, old, new, merge):
- delete_file(self.app, old)
- self.export_sidebar()
-
- def is_item_allowed(self, name, item_type, allowed_workspaces):
- if frappe.session.user == "Administrator":
- return True
-
- item_type = item_type.lower()
-
- if item_type == "doctype":
- return (
- name in (self.can_read or [])
- and name in (self.restricted_doctypes or [])
- and frappe.has_permission(name)
- )
- if item_type == "page":
- return name in self.allowed_pages and name in self.restricted_pages
- if item_type == "report":
- return name in self.allowed_reports
- if item_type == "help":
- return True
- if item_type == "dashboard":
- return True
- if item_type == "url":
- return True
- if item_type == "workspace":
- return name in allowed_workspaces
+ if self.standard:
+ delete_file(self.app, old)
+ self.export_sidebar()
def get_cached(self, cache_key, fallback_fn):
value = frappe.cache.get_value(cache_key, user=frappe.session.user)
@@ -133,12 +103,6 @@ def get_module_from_items(self):
if counts and counts.most_common(1)[0]:
return counts.most_common(1)[0][0]
- def get_allowed_modules(self):
- if not self.user.allow_modules:
- self.user.build_permissions()
-
- return self.user.allow_modules
-
def delete_file(app, title):
folder_path = create_directory_on_app_path("workspace_sidebar", app)
@@ -275,10 +239,7 @@ def auto_generate_sidebar_from_module():
"""Auto generate sidebar from module"""
sidebars = []
for module in frappe.get_all("Module Def", pluck="name"):
- if not (
- frappe.db.exists("Workspace Sidebar", {"module": module, "for_user": None})
- or frappe.db.exists("Workspace Sidebar", {"name": module, "for_user": None})
- ):
+ if not (frappe.db.exists("Workspace Sidebar", {"name": module, "for_user": None})):
module_info = get_module_info(module)
sidebar_items = create_sidebar_items(module_info)
sidebar = frappe.new_doc("Workspace Sidebar")
diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py
index bcebd4db0ed6..1ad9e2b830e2 100644
--- a/frappe/desk/form/assign_to.py
+++ b/frappe/desk/form/assign_to.py
@@ -40,7 +40,7 @@ def get(args=None):
@frappe.whitelist()
-def add(args=None, *, ignore_permissions=False):
+def add(args=None):
"""add in someone's to do list
args = {
"assign_to": [],
@@ -51,6 +51,10 @@ def add(args=None, *, ignore_permissions=False):
}
"""
+ return _add(args, ignore_permissions=False)
+
+
+def _add(args=None, *, ignore_permissions=False):
if not args:
args = frappe.local.form_dict
@@ -109,7 +113,7 @@ def add(args=None, *, ignore_permissions=False):
)
frappe.throw(msg, title=_("Missing Permission"))
else:
- frappe.share.add(doc.doctype, doc.name, assign_to)
+ frappe.share.add(doc.doctype, str(doc.name), assign_to)
shared_with_users.append(assign_to)
# make this document followed by assigned user
@@ -174,12 +178,16 @@ def close_all_assignments(doctype, name, ignore_permissions=False):
@frappe.whitelist()
-def remove(doctype, name, assign_to, ignore_permissions=False):
+def remove(doctype, name, assign_to):
+ return _remove(doctype, name, assign_to, ignore_permissions=False)
+
+
+def _remove(doctype, name, assign_to, ignore_permissions=False):
return set_status(doctype, name, "", assign_to, status="Cancelled", ignore_permissions=ignore_permissions)
@frappe.whitelist()
-def remove_multiple(doctype, names, ignore_permissions=False):
+def remove_multiple(doctype, names):
docname_list = json.loads(names)
for name in docname_list:
@@ -189,15 +197,15 @@ def remove_multiple(doctype, names, ignore_permissions=False):
continue
for assignment in assignments:
- remove(doctype, name, assignment.get("owner"), ignore_permissions)
+ remove(doctype, name, assignment.get("owner"))
@frappe.whitelist()
-def close(doctype: str, name: str, assign_to: str, ignore_permissions=False):
+def close(doctype, name, assign_to):
if assign_to != frappe.session.user:
frappe.throw(_("Only the assignee can complete this to-do."))
- return set_status(doctype, name, "", assign_to, status="Closed", ignore_permissions=ignore_permissions)
+ return set_status(doctype, name, "", assign_to, status="Closed", ignore_permissions=False)
def set_status(doctype, name, todo=None, assign_to=None, status="Cancelled", ignore_permissions=False):
diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py
index c51ac0bea1c5..675a556b2081 100644
--- a/frappe/desk/form/document_follow.py
+++ b/frappe/desk/form/document_follow.py
@@ -10,11 +10,14 @@
@frappe.whitelist()
-def update_follow(doctype: str, doc_name: str, following: bool):
+def update_follow(doctype: str, doc_name: str, following: bool | str):
+ following = frappe.utils.sbool(following)
if following:
- return (follow_document(doctype, doc_name, frappe.session.user) and True) or False
+ is_following = follow_document(doctype, doc_name, frappe.session.user)
+ return bool(is_following)
else:
- return unfollow_document(doctype, doc_name, frappe.session.user)
+ unfollow_document(doctype, doc_name, frappe.session.user)
+ return False
@frappe.whitelist()
@@ -83,7 +86,7 @@ def unfollow_document(doctype, doc_name, user):
if doc:
frappe.delete_doc("Document Follow", doc[0].name, force=True)
frappe.toast(_("Un-following document {0}").format(doc_name))
- return False
+ return True
return False
diff --git a/frappe/desk/form/test_form.py b/frappe/desk/form/test_form.py
index 1c7a8f011446..ed695625b4fe 100644
--- a/frappe/desk/form/test_form.py
+++ b/frappe/desk/form/test_form.py
@@ -3,6 +3,7 @@
import frappe
from frappe.desk.form.linked_with import get_linked_docs, get_linked_doctypes
+from frappe.desk.form.utils import _sort_field_fallback, get_next
from frappe.tests import IntegrationTestCase
@@ -11,3 +12,52 @@ def test_linked_with(self):
results = get_linked_docs("Role", "System Manager", linkinfo=get_linked_doctypes("Role"))
self.assertTrue("User" in results)
self.assertTrue("DocType" in results)
+
+ def test_sort_field_fallback(self):
+ self.assertIsNone(_sort_field_fallback("Note", "name"))
+ self.assertIsNone(_sort_field_fallback("Note", "creation"))
+ self.assertIsNone(_sort_field_fallback("Note", "public"))
+ self.assertEqual(_sort_field_fallback("Note", "expire_notification_on"), "0001-01-01")
+ self.assertEqual(_sort_field_fallback("Event Notifications", "time"), "00:00:00")
+ self.assertEqual(_sort_field_fallback("Note", "title"), "")
+ self.assertEqual(_sort_field_fallback("Note", "nonexistent_field_xyz"), "")
+
+ def test_get_next_with_null_sort_field(self):
+ notes = []
+ for i, expire in enumerate([None, "2099-01-01", None]):
+ note = frappe.get_doc(
+ {
+ "doctype": "Note",
+ "title": f"test_navigate_null_{frappe.generate_hash(length=8)}_{i}",
+ "expire_notification_on": expire,
+ }
+ ).insert(ignore_permissions=True)
+ notes.append(note.name)
+
+ try:
+ filters = {"name": ["in", notes]}
+
+ next_name = get_next(
+ "Note",
+ notes[0],
+ prev=0,
+ sort_field="expire_notification_on",
+ sort_order="asc",
+ filters=filters,
+ )
+ self.assertIsNotNone(next_name)
+ self.assertIn(next_name, notes)
+
+ prev_name = get_next(
+ "Note",
+ notes[1],
+ prev=1,
+ sort_field="expire_notification_on",
+ sort_order="asc",
+ filters=filters,
+ )
+ self.assertIsNotNone(prev_name)
+ self.assertIn(prev_name, [notes[0], notes[2]])
+ finally:
+ for name in notes:
+ frappe.delete_doc("Note", name, force=1, ignore_permissions=True)
diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py
index c0c665ed486c..665cc33086dc 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -10,6 +10,7 @@
from frappe import _
from frappe.core.doctype.file.utils import extract_images_from_html
from frappe.desk.form.document_follow import follow_document
+from frappe.query_builder.functions import IfNull
if TYPE_CHECKING:
from frappe.core.doctype.comment.comment import Comment
@@ -92,9 +93,15 @@ def get_next(
filters = json.loads(filters)
table = frappe.qb.DocType(doctype)
- sort_column = table[sort_field]
name_column = table.name
current_sort_value = frappe.db.get_value(doctype, value, sort_field)
+ fallback = _sort_field_fallback(doctype, sort_field)
+ if fallback is not None:
+ sort_column = IfNull(table[sort_field], fallback)
+ if current_sort_value is None:
+ current_sort_value = fallback
+ else:
+ sort_column = table[sort_field]
is_ascending = sort_order.lower() == "asc"
if prev == is_ascending:
@@ -123,5 +130,22 @@ def get_next(
return None
+def _sort_field_fallback(doctype: str, fieldname: str):
+ if fieldname in ("name", "modified", "creation", "modified_by", "owner", "idx", "docstatus"):
+ return None
+ df = frappe.get_meta(doctype).get_field(fieldname)
+ if df is None:
+ return ""
+ if df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"):
+ return None
+ if getattr(df, "not_nullable", False):
+ return None
+ if df.fieldtype in ("Date", "Datetime"):
+ return "0001-01-01"
+ if df.fieldtype == "Time":
+ return "00:00:00"
+ return ""
+
+
def get_pdf_link(doctype, docname, print_format="Standard", no_letterhead=0):
return f"/api/method/frappe.utils.print_format.download_pdf?doctype={doctype}&name={docname}&format={print_format}&no_letterhead={no_letterhead}"
diff --git a/frappe/desk/like.py b/frappe/desk/like.py
index 639967369164..16789f80c982 100644
--- a/frappe/desk/like.py
+++ b/frappe/desk/like.py
@@ -33,6 +33,8 @@ def _toggle_like(doctype, name, add, user=None):
if not user:
user = frappe.session.user
+ frappe.has_permission(doctype, "read", doc=name, throw=True)
+
try:
liked_by = frappe.db.get_value(doctype, name, "_liked_by")
diff --git a/frappe/desk/link_preview.py b/frappe/desk/link_preview.py
index c9143ef5f16a..4d79400b201d 100644
--- a/frappe/desk/link_preview.py
+++ b/frappe/desk/link_preview.py
@@ -28,7 +28,8 @@ def get_preview_data(doctype, docname):
image_field = meta.image_field
preview_fields.append(title_field)
- preview_fields.append(image_field)
+ if image_field:
+ preview_fields.append(image_field)
preview_fields.append("name")
preview_data = frappe.get_list(doctype, filters={"name": docname}, fields=preview_fields, limit=1)
diff --git a/frappe/desk/page/desktop/desktop.html b/frappe/desk/page/desktop/desktop.html
index 5d3126af1766..12d7baa7f70e 100644
--- a/frappe/desk/page/desktop/desktop.html
+++ b/frappe/desk/page/desktop/desktop.html
@@ -8,20 +8,21 @@
alt="{{ _("App Logo") |e }}"
>
-
-
-
-
- {{ _("Search") }}
-
-
-
-
-
+ {% if (show_search_bar) %}
+
+
+
+
+
+
+ {{ _("Search") }}
+
+
+
+
+
+ {% endif %}
diff --git a/frappe/desk/page/desktop/desktop.js b/frappe/desk/page/desktop/desktop.js
index d783326c5d31..a909dbe430a7 100644
--- a/frappe/desk/page/desktop/desktop.js
+++ b/frappe/desk/page/desktop/desktop.js
@@ -451,17 +451,11 @@ class DesktopPage {
{
icon: "info",
label: "About",
+ condition: "frappe.user.has_role('System Manager')",
onClick: function () {
return frappe.ui.toolbar.show_about();
},
},
- {
- icon: "support",
- label: "Frappe Support",
- onClick: function () {
- window.open("https://support.frappe.io/help", "_blank");
- },
- },
{
icon: "rotate-ccw",
label: "Reset Desktop Layout",
@@ -511,16 +505,6 @@ class DesktopPage {
let awesome_bar = new frappe.search.AwesomeBar();
awesome_bar.setup(".desktop-search-wrapper #desktop-navbar-modal-search");
- frappe.ui.keys.add_shortcut({
- shortcut: "ctrl+g",
- action: function (e) {
- $(".desktop-search-wrapper #desktop-navbar-modal-search").click();
- e.preventDefault();
- return false;
- },
- description: __("Open Awesomebar"),
- ignore_inputs: true,
- });
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+k",
action: function (e) {
diff --git a/frappe/desk/page/desktop/desktop.py b/frappe/desk/page/desktop/desktop.py
index 632783315bd9..4c676645f76e 100644
--- a/frappe/desk/page/desktop/desktop.py
+++ b/frappe/desk/page/desktop/desktop.py
@@ -18,4 +18,6 @@ def get_context(context):
except frappe.DoesNotExistError:
frappe.clear_last_message()
context.desktop_layout = {}
+
+ context.show_search_bar = frappe.get_cached_value("User", frappe.session.user, "search_bar")
return context
diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py
index 60c832b78ac6..7704c921abc5 100644
--- a/frappe/desk/page/setup_wizard/install_fixtures.py
+++ b/frappe/desk/page/setup_wizard/install_fixtures.py
@@ -27,11 +27,6 @@ def update_genders():
for gender in (
_("Male"),
_("Female"),
- _("Other"),
- _("Transgender"),
- _("Genderqueer"),
- _("Non-Conforming"),
- _("Prefer not to say"),
):
doc = frappe.new_doc("Gender")
doc.gender = gender
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py
index d06031375516..d2719024508c 100755
--- a/frappe/desk/page/setup_wizard/setup_wizard.py
+++ b/frappe/desk/page/setup_wizard/setup_wizard.py
@@ -11,6 +11,7 @@
from frappe.translate import send_translations, set_default_language
from frappe.utils import cint, now, strip
from frappe.utils.password import update_password
+from frappe.utils.synchronization import LockTimeoutError, filelock
from . import install_fixtures
@@ -53,19 +54,18 @@ def setup_complete(args):
and clears cache. If wizard breaks, calls `setup_wizard_exception` hook"""
# Setup complete: do not throw an exception, let the user continue to desk
- if frappe.is_setup_complete():
+ try:
+ with filelock("setup_wizard", timeout=0.5):
+ if frappe.is_setup_complete():
+ return {"status": "ok"}
+
+ kwargs = parse_args(sanitize_input(args))
+ stages = get_setup_stages(kwargs)
+ return process_setup_stages(stages, kwargs)
+ except LockTimeoutError:
+ # Duplicate request
return {"status": "ok"}
- kwargs = parse_args(sanitize_input(args))
- stages = get_setup_stages(kwargs)
- is_background_task = frappe.conf.get("trigger_site_setup_in_background")
-
- if is_background_task:
- process_setup_stages.enqueue(stages=stages, user_input=kwargs, is_background_task=True, at_front=True)
- return {"status": "registered"}
- else:
- return process_setup_stages(stages, kwargs)
-
@frappe.whitelist()
def initialize_system_settings_and_user(system_settings_data, user_data):
@@ -89,7 +89,6 @@ def initialize_system_settings_and_user(system_settings_data, user_data):
create_or_update_user(user_data)
-@frappe.task()
def process_setup_stages(stages, user_input, is_background_task=False):
from frappe.utils.telemetry import capture
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index c3396915f0fb..7ead316bb237 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -99,8 +99,13 @@ def generate_report_result(
result = normalize_result(result, columns)
if report.get("custom_columns"):
- # saved columns (with custom columns / with different column order)
- columns = report.custom_columns
+ # keep saved columns still returned by this run, plus user-added
+ # custom columns (`link_field`); drops columns stale after a filter change
+ columns = [
+ column
+ for column in report.custom_columns
+ if column.get("link_field") or column["fieldname"] in report_column_names
+ ]
# unsaved custom_columns
if custom_columns:
@@ -842,7 +847,7 @@ def save_report(reference_report, report_name, columns, filters):
def get_filtered_data(ref_doctype, columns, data, user):
result = []
linked_doctypes = get_linked_doctypes(columns, data)
- match_filters_per_doctype = get_user_match_filters(linked_doctypes, user=user)
+ match_filters_per_doctype = merge_nested_dicts(get_user_match_filters(linked_doctypes, user=user))
shared = frappe.share.get_shared(ref_doctype, user)
columns_dict = get_columns_dict(columns)
@@ -886,6 +891,16 @@ def get_filtered_data(ref_doctype, columns, data, user):
return result
+def merge_nested_dicts(data):
+ result = {}
+ for key, dict_list in data.items():
+ merged_dict = {}
+ for sub_dict in dict_list:
+ merged_dict.update(sub_dict)
+ result[key] = [merged_dict]
+ return result
+
+
def has_match(
row,
linked_doctypes,
diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py
index 3aa27f8d56ef..c46b70038fbb 100644
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -333,6 +333,13 @@ def save_report(name, doctype, report_settings):
if report.owner != frappe.session.user and not report.has_permission("write"):
frappe.throw(_("Insufficient Permissions for editing Report"), frappe.PermissionError)
else:
+ if not frappe.has_permission("Report", "create"):
+ frappe.throw(_("You don't have permission to create Report records."), frappe.PermissionError)
+ if not frappe.has_permission(doctype, "read"):
+ frappe.throw(
+ _("You don't have permission to create report for {0}").format(_(doctype)),
+ frappe.PermissionError,
+ )
report = frappe.new_doc("Report")
report.report_name = name
report.ref_doctype = doctype
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index ac5a26c60270..6ae65f9fe881 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -12,7 +12,7 @@
from frappe.database.schema import SPECIAL_CHAR_PATTERN
from frappe.model.db_query import get_order_by
from frappe.permissions import has_permission
-from frappe.utils import cint, cstr, escape_html, unique
+from frappe.utils import cint, cstr, escape_html, sbool, unique
from frappe.utils.caching import http_cache
from frappe.utils.data import make_filter_tuple
@@ -62,6 +62,18 @@ def search_link(
return build_for_autosuggest(results, doctype=doctype)
+def make_dict_from_filter_list(filters: list) -> dict:
+ """Reverse of `make_filter_tuple`: convert
+ [[doctype, fieldname, operator, value], ..] back to {fieldname: value} for equality
+ filters and {fieldname: [operator, value]} otherwise.
+ """
+ _filters = {}
+ for f in filters:
+ fieldname, operator, value = f[1], f[2], f[3]
+ _filters[fieldname] = value if operator == "=" else [operator, value]
+ return _filters
+
+
# this is called by the search box
@frappe.whitelist()
def search_widget(
@@ -79,6 +91,8 @@ def search_widget(
*,
link_fieldname: str | None = None,
for_link_validation: bool = False,
+ # this param has been added temporarily for compatibility - may be removed later
+ query_filters_as_dict: bool = False,
):
if ignore_user_permissions:
if reference_doctype and link_fieldname:
@@ -110,15 +124,25 @@ def search_widget(
filters = {}
if query: # Query = custom search query i.e. python function
+ meta = frappe.get_meta(doctype)
+ # For translated doctypes, pass empty txt and a large page_length so the custom query
+ # returns all records without SQL-level text filtering; Python-level filtering against
+ # translated values is applied below.
+ query_txt = "" if meta.translated_doctype else txt
+ query_page_length = PAGE_LENGTH_FOR_LINK_VALIDATION if meta.translated_doctype else page_length
+
+ if sbool(query_filters_as_dict) and isinstance(filters, list):
+ filters = make_dict_from_filter_list(filters)
+
try:
is_whitelisted(frappe.get_attr(query))
- return frappe.call(
+ values = frappe.call(
query,
doctype,
- txt,
+ query_txt,
searchfield,
start,
- page_length,
+ query_page_length,
filters,
as_dict=as_dict,
reference_doctype=reference_doctype,
@@ -137,6 +161,14 @@ def search_widget(
)
return []
+ if not for_link_validation:
+ if meta.translated_doctype:
+ values = filter_translated(values, txt, as_dict)
+ values = sorted(values, key=lambda x: relevance_sorter(x, txt, as_dict))
+ values = values[:page_length]
+
+ return values
+
meta = frappe.get_meta(doctype)
include_disabled = False
@@ -225,15 +257,7 @@ def search_widget(
if not for_link_validation:
if meta.translated_doctype:
- # Filtering the values array so that query is included in very element
- values = (
- result
- for result in values
- if any(
- re.search(f"{re.escape(txt)}.*", _(cstr(value)) or "", re.IGNORECASE)
- for value in (result.values() if as_dict else result)
- )
- )
+ values = filter_translated(values, txt, as_dict)
# Sorting the values array so that relevant results always come first
# This will first bring elements on top in which query is a prefix of element
@@ -382,6 +406,18 @@ def relevance_sorter(key, query, as_dict):
return (cstr(value).casefold().startswith(query.casefold()) is not True, value)
+def filter_translated(values, txt: str, as_dict: bool) -> list:
+ """Return only those results where txt matches any translated field value."""
+ return [
+ result
+ for result in values
+ if any(
+ re.search(f"{re.escape(txt)}.*", _(cstr(value)) or "", re.IGNORECASE)
+ for value in (result.values() if as_dict else result)
+ )
+ ]
+
+
@frappe.whitelist()
def get_names_for_mentions(search_term):
users_for_mentions = frappe.cache.get_value("users_for_mentions", get_users_for_mentions)
diff --git a/frappe/desktop_icon/framework.json b/frappe/desktop_icon/framework.json
index 93fb518fa877..d00615d1d4ed 100644
--- a/frappe/desktop_icon/framework.json
+++ b/frappe/desktop_icon/framework.json
@@ -10,10 +10,14 @@
"link": "/desk/build",
"link_type": "External",
"logo_url": "/assets/frappe/images/frappe-framework-logo.svg",
- "modified": "2025-12-12 07:36:09.059666",
+ "modified": "2026-06-07",
"modified_by": "Administrator",
"name": "Framework",
"owner": "Administrator",
- "roles": [],
+ "roles": [
+ {
+ "role": "System Manager"
+ }
+ ],
"standard": 1
}
diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.js b/frappe/email/doctype/auto_email_report/auto_email_report.js
index acbd1385c1a6..3744f51013a4 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.js
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.js
@@ -195,6 +195,9 @@ frappe.ui.form.on("Auto Email Report", {
reference_report.onload(frappe.query_report);
}
+ dialog.doc = dialog.doc || {};
+ dialog.fields_list.forEach((f) => (f.doc = dialog.doc));
+
dialog.set_values(filters);
});
diff --git a/frappe/email/doctype/document_follow/document_follow.json b/frappe/email/doctype/document_follow/document_follow.json
index 2261fcb54104..f42a25f9810b 100644
--- a/frappe/email/doctype/document_follow/document_follow.json
+++ b/frappe/email/doctype/document_follow/document_follow.json
@@ -41,7 +41,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-03-23 16:03:22.555030",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Email",
"name": "Document Follow",
@@ -58,18 +58,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Desk User",
- "share": 1,
- "write": 1
}
],
"read_only": 1,
diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json
index 19fa3985d7ed..5ae3ee2a52f2 100644
--- a/frappe/email/doctype/email_account/email_account.json
+++ b/frappe/email/doctype/email_account/email_account.json
@@ -506,7 +506,6 @@
},
{
"default": "0",
- "documentation_url": "https://docs.frappe.io/erpnext/user/manual/en/linking-emails-to-document",
"fieldname": "enable_automatic_linking",
"fieldtype": "Check",
"hide_days": 1,
@@ -768,7 +767,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
- "modified": "2026-02-11 16:18:15.572240",
+ "modified": "2026-05-25",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",
diff --git a/frappe/email/doctype/email_template/email_template.json b/frappe/email/doctype/email_template/email_template.json
index e92f677f028e..238d4fdd5b15 100644
--- a/frappe/email/doctype/email_template/email_template.json
+++ b/frappe/email/doctype/email_template/email_template.json
@@ -57,17 +57,13 @@
],
"icon": "fa fa-comment",
"links": [],
- "modified": "2024-03-23 16:03:24.779791",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Template",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
- {
- "read": 1,
- "role": "Desk User"
- },
{
"create": 1,
"delete": 1,
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index 45d587a69a84..0a9efab5e781 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -467,6 +467,7 @@ def set_subject(self):
def set_from(self):
# gmail mailing-list compatibility
# use X-Original-Sender if available, as gmail sometimes modifies the 'From'
+ self.from_real_name = None
_from_email = self.decode_email(self.mail.get("X-Original-From") or self.mail["From"])
_reply_to = self.decode_email(self.mail.get("Reply-To"))
@@ -493,6 +494,8 @@ def set_from(self):
def decode_email(email: bytes | str | None) -> str | None:
if not email:
return
+
+ raw_email = email if isinstance(email, str) else email.decode("utf-8", "replace")
email = frappe.as_unicode(email)
try:
parts = decode_header(email)
@@ -507,6 +510,20 @@ def decode_email(email: bytes | str | None) -> str | None:
decoded += part.decode(encoding, "replace")
else:
decoded += safe_decode(part)
+
+ # Reject malformed address headers where decoding synthesizes a bare addr-spec.
+ # Allow valid encoded display-name forms like "=?utf-8?...?=
".
+ if decoded and "@" in decoded and "@" not in raw_email:
+ decoded_addr_spec = parse_addr(decoded)[1]
+ if decoded_addr_spec and decoded.strip() == decoded_addr_spec:
+ frappe.log_error(
+ title=_("Malformed Address Header"),
+ message=_("Rejected malformed encoded address header with synthesized '@': {0}").format(
+ repr(raw_email)
+ ),
+ )
+ return None
+
return decoded
def set_content_and_type(self):
@@ -581,7 +598,8 @@ def get_attachment(self, part) -> None:
if not fcontent:
return
- attachment_limit = cint(self.email_account.attachment_limit)
+ email_account = getattr(self, "email_account", None)
+ attachment_limit = cint(email_account.attachment_limit) if email_account else 0
if attachment_limit and len(fcontent) > attachment_limit * 1024 * 1024:
return # skip attachments that are larger than the specified limit
diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py
index 27e061d0b057..6923e2a2044e 100644
--- a/frappe/email/test_email_body.py
+++ b/frappe/email/test_email_body.py
@@ -244,6 +244,25 @@ def test_poorly_encoded_messages2(self):
mail = Email.decode_email(" =?UTF-8?B?X\xe0\xe0Y?= ")
self.assertIn("xy@example.com", mail)
+ def test_rejects_encoded_addr_spec_without_raw_at_sign(self):
+ email = Email.decode_email("=?utf-8?Q?admin=40example=2Ecom?=")
+ self.assertIsNone(email)
+
+ content_bytes = b"""MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+Content-Disposition: inline
+Content-Transfer-Encoding: 8bit
+To: support@example.com
+From: =?utf-8?Q?admin=40example=2Ecom?=
+"""
+
+ mail = Email(content_bytes)
+ self.assertIsNone(mail.from_email)
+
+ def test_allows_encoded_display_name_with_valid_addr_spec(self):
+ email = Email.decode_email("=?utf-8?Q?Jane_Doe?= ")
+ self.assertIn("jane@example.com", email)
+
def test_quotes_in_email_sender(self):
content_bytes = rb"""MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json
index e41ee90d1196..c661d53b560f 100644
--- a/frappe/geo/country_info.json
+++ b/frappe/geo/country_info.json
@@ -1313,11 +1313,10 @@
"Iraq": {
"code": "iq",
"currency": "IQD",
- "currency_fraction": "Fils",
- "currency_fraction_units": 1000,
"currency_name": "Iraqi Dinar",
- "currency_symbol": "\u0639.\u062f",
- "number_format": "#,###.###",
+ "currency_symbol": "\u062f.\u0639",
+ "number_format": "#,###",
+ "symbol_on_right": 1,
"timezones": [
"Asia/Baghdad"
],
diff --git a/frappe/geo/doctype/country/country.json b/frappe/geo/doctype/country/country.json
index 0c0d978db327..49708d18b8d4 100644
--- a/frappe/geo/doctype/country/country.json
+++ b/frappe/geo/doctype/country/country.json
@@ -56,7 +56,7 @@
"icon": "fa fa-globe",
"idx": 1,
"links": [],
- "modified": "2025-01-03 13:01:46.030113",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Geo",
"name": "Country",
@@ -76,10 +76,7 @@
"write": 1
},
{
- "email": 1,
- "print": 1,
"read": 1,
- "report": 1,
"role": "All"
}
],
diff --git a/frappe/geo/doctype/country/country.py b/frappe/geo/doctype/country/country.py
index 4ada13f16f5c..d3c0944f95ce 100644
--- a/frappe/geo/doctype/country/country.py
+++ b/frappe/geo/doctype/country/country.py
@@ -84,6 +84,7 @@ def get_countries_and_currencies():
fraction_units=country.currency_fraction_units,
smallest_currency_fraction_value=country.smallest_currency_fraction_value,
number_format=frappe.db.escape(country.number_format)[1:-1],
+ symbol_on_right=country.symbol_on_right,
)
)
diff --git a/frappe/geo/doctype/currency/currency.json b/frappe/geo/doctype/currency/currency.json
index 20a1b77313c6..87307a284274 100644
--- a/frappe/geo/doctype/currency/currency.json
+++ b/frappe/geo/doctype/currency/currency.json
@@ -52,7 +52,7 @@
{
"description": "Smallest circulating fraction unit (coin). For e.g. 1 cent for USD and it should be entered as 0.01",
"fieldname": "smallest_currency_fraction_value",
- "fieldtype": "Currency",
+ "fieldtype": "Float",
"label": "Smallest Currency Fraction Value",
"non_negative": 1
},
@@ -82,7 +82,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-03-23 16:01:31.809369",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Geo",
"name": "Currency",
@@ -116,7 +116,7 @@
},
{
"read": 1,
- "role": "Purchase User"
+ "role": "All"
}
],
"sort_field": "creation",
diff --git a/frappe/geo/doctype/currency/currency.py b/frappe/geo/doctype/currency/currency.py
index 715adf19c0d6..83df80554f54 100644
--- a/frappe/geo/doctype/currency/currency.py
+++ b/frappe/geo/doctype/currency/currency.py
@@ -33,7 +33,7 @@ class Currency(Document):
"#.###",
"#,###",
]
- smallest_currency_fraction_value: DF.Currency
+ smallest_currency_fraction_value: DF.Float
symbol: DF.Data | None
symbol_on_right: DF.Check
# end: auto-generated types
diff --git a/frappe/handler.py b/frappe/handler.py
index c7a8e08eea55..8eafd07bc018 100644
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -131,6 +131,16 @@ def upload_file():
if frappe.session.user == "Guest":
if frappe.get_system_settings("allow_guests_to_upload_files"):
ignore_permissions = True
+ guest_allowed_docs = frappe.get_system_settings("allowed_doctypes_for_guest_uploads")
+ if guest_allowed_docs:
+ target_doctype = frappe.form_dict.doctype
+ allowed_docs = guest_allowed_docs.splitlines()
+ allowed_docs = [doc.strip() for doc in allowed_docs if doc.strip()]
+ if allowed_docs and target_doctype not in allowed_docs:
+ frappe.throw(
+ _("Guests are not allowed to upload files for {0} Doctype").format(target_doctype),
+ frappe.PermissionError,
+ )
else:
raise frappe.PermissionError
else:
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 215871bbbf25..98423ffc830a 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -480,6 +480,7 @@
"item_type": "Action",
"action": "frappe.ui.toolbar.show_about()",
"is_standard": 1,
+ "condition": "frappe.user.has_role('System Manager')",
},
{
"item_label": "Keyboard Shortcuts",
@@ -492,12 +493,7 @@
"item_type": "Route",
"route": "/desk/system-health-report",
"is_standard": 1,
- },
- {
- "item_label": "Frappe Support",
- "item_type": "Route",
- "route": "https://frappe.io/support",
- "is_standard": 1,
+ "condition": "frappe.user.has_role('System Manager')",
},
]
@@ -544,5 +540,6 @@
"logo": app_logo_url,
"title": app_title,
"route": app_home,
+ "has_permission": "frappe.permissions.check_app_permission",
}
]
diff --git a/frappe/installer.py b/frappe/installer.py
index 978598385b06..61b3a3f1e371 100644
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -359,6 +359,7 @@ def add_to_installed_apps(app_name, rebuild_website=True):
frappe.get_single("Installed Applications").update_versions()
frappe.db.commit()
+ _sync_installed_apps_to_site_config()
def remove_from_installed_apps(app_name):
@@ -374,6 +375,7 @@ def remove_from_installed_apps(app_name):
frappe.db.commit()
if frappe.flags.in_install:
post_install()
+ _sync_installed_apps_to_site_config()
def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False):
@@ -627,6 +629,14 @@ def make_site_config(
f.write(json.dumps(site_config, indent=1, sort_keys=True))
+def _sync_installed_apps_to_site_config():
+ """Mirror the installed-apps list into site_config.json for fast reads without a DB round-trip."""
+ try:
+ update_site_config("installed_apps", frappe.get_installed_apps())
+ except Exception:
+ pass
+
+
def update_site_config(key, value, validate=True, site_config_path=None):
"""Update a value in site_config"""
from frappe.config import clear_site_config_cache
diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json
index 9fc63cd52079..0791c52c574e 100644
--- a/frappe/integrations/doctype/connected_app/connected_app.json
+++ b/frappe/integrations/doctype/connected_app/connected_app.json
@@ -1,6 +1,5 @@
{
"actions": [],
- "beta": 1,
"creation": "2019-01-24 15:51:06.362222",
"doctype": "DocType",
"document_type": "Document",
@@ -139,7 +138,7 @@
"link_fieldname": "connected_app"
}
],
- "modified": "2024-07-05 08:24:50.182706",
+ "modified": "2026-05-30",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Connected App",
@@ -156,16 +155,13 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "read": 1,
- "role": "All"
}
],
+ "row_format": "Dynamic",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "provider_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.json b/frappe/integrations/doctype/google_calendar/google_calendar.json
index c12d012a05de..ff1c65a3dec2 100644
--- a/frappe/integrations/doctype/google_calendar/google_calendar.json
+++ b/frappe/integrations/doctype/google_calendar/google_calendar.json
@@ -117,7 +117,7 @@
}
],
"links": [],
- "modified": "2025-01-27 13:06:00.000000",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Calendar",
@@ -135,19 +135,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Desk User",
- "share": 1,
- "write": 1
}
],
"sort_field": "creation",
diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.json b/frappe/integrations/doctype/google_contacts/google_contacts.json
index 19e288cb8f90..0a811d00e9b3 100644
--- a/frappe/integrations/doctype/google_contacts/google_contacts.json
+++ b/frappe/integrations/doctype/google_contacts/google_contacts.json
@@ -99,7 +99,7 @@
}
],
"links": [],
- "modified": "2024-03-23 16:03:26.863560",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Contacts",
@@ -115,14 +115,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "if_owner": 1,
- "read": 1,
- "role": "Desk User",
- "write": 1
}
],
"sort_field": "creation",
diff --git a/frappe/integrations/doctype/push_notification_settings/push_notification_settings.json b/frappe/integrations/doctype/push_notification_settings/push_notification_settings.json
index 075726236caa..f1dfb1434339 100644
--- a/frappe/integrations/doctype/push_notification_settings/push_notification_settings.json
+++ b/frappe/integrations/doctype/push_notification_settings/push_notification_settings.json
@@ -1,7 +1,6 @@
{
"actions": [],
"allow_rename": 1,
- "beta": 1,
"creation": "2024-01-04 11:36:08.013039",
"description": "Enabling this will register your site on a central relay server to send push notifications for all installed apps through Firebase Cloud Messaging. This server only stores user tokens and error logs, and no messages are saved.",
"doctype": "DocType",
@@ -46,7 +45,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2024-03-23 16:03:35.768559",
+ "modified": "2026-05-30 23:05:07.179037",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Push Notification Settings",
@@ -63,7 +62,8 @@
"write": 1
}
],
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json
index f45edd24ffaf..0f42fc2441ed 100644
--- a/frappe/integrations/doctype/token_cache/token_cache.json
+++ b/frappe/integrations/doctype/token_cache/token_cache.json
@@ -1,7 +1,6 @@
{
"actions": [],
"autoname": "format:{connected_app}-{user}",
- "beta": 1,
"creation": "2019-01-24 16:56:55.631096",
"doctype": "DocType",
"document_type": "System",
@@ -86,26 +85,21 @@
}
],
"links": [],
- "modified": "2024-03-23 16:03:58.980060",
+ "modified": "2026-05-30",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Token Cache",
- "naming_rule": "Expression",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"delete": 1,
"read": 1,
"role": "System Manager"
- },
- {
- "delete": 1,
- "if_owner": 1,
- "read": 1,
- "role": "All"
}
],
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py
index 221ca3175f8a..c4902c509981 100644
--- a/frappe/integrations/doctype/token_cache/token_cache.py
+++ b/frappe/integrations/doctype/token_cache/token_cache.py
@@ -44,7 +44,7 @@ def update_data(self, data):
Params:
data - Dict with access_token, refresh_token, expires_in and scope.
"""
- token_type = cstr(data.get("token_type", "")).lower()
+ token_type = cstr(data.get("token_type", "bearer")).lower()
if token_type not in ["bearer", "mac"]:
frappe.throw(_("Received an invalid token type."))
# 'Bearer' or 'MAC'
diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py
index 1f52c3e4ae69..bcde1eabae82 100644
--- a/frappe/integrations/doctype/webhook/webhook.py
+++ b/frappe/integrations/doctype/webhook/webhook.py
@@ -153,8 +153,8 @@ def enqueue_webhook(doc, webhook) -> None:
request_url = webhook.request_url
if webhook.is_dynamic_url:
request_url = frappe.render_template(webhook.request_url, get_context(doc))
- headers = get_webhook_headers(doc, webhook)
data = get_webhook_data(doc, webhook)
+ headers = get_webhook_headers(doc, webhook, data=data)
except Exception as e:
frappe.logger().debug({"enqueue_webhook_error": e})
@@ -166,7 +166,7 @@ def enqueue_webhook(doc, webhook) -> None:
r = requests.request(
method=webhook.request_method,
url=request_url,
- data=json.dumps(data, default=str),
+ data=frappe.as_json(data),
headers=headers,
timeout=webhook.timeout or 5,
)
@@ -182,12 +182,11 @@ def enqueue_webhook(doc, webhook) -> None:
except Exception as e:
frappe.logger().debug({"webhook_error": e, "try": i + 1})
log_request(webhook.name, doc.doctype, doc.name, request_url, headers, data, r)
- sleep(3 * i + 1)
- if i != 2:
+ if i < 2:
+ sleep(3 * i + 1)
continue
- else:
- if webhook.webhook_docevent == "workflow_transition":
- raise e
+ if webhook.webhook_docevent == "workflow_transition":
+ raise e
def log_request(
@@ -217,15 +216,16 @@ def log_request(
request_log.save(ignore_permissions=True)
-def get_webhook_headers(doc, webhook):
+def get_webhook_headers(doc, webhook, data=None):
headers = {}
if webhook.enable_security:
- data = get_webhook_data(doc, webhook)
+ if data is None:
+ data = get_webhook_data(doc, webhook)
signature = base64.b64encode(
hmac.new(
webhook.get_password("webhook_secret").encode("utf8"),
- json.dumps(data).encode("utf8"),
+ frappe.as_json(data).encode("utf8"),
hashlib.sha256,
).digest()
)
diff --git a/frappe/locale/main.pot b/frappe/locale/main.pot
index 2c13a4c9975c..61f462e4d406 100644
--- a/frappe/locale/main.pot
+++ b/frappe/locale/main.pot
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Frappe Framework VERSION\n"
"Report-Msgid-Bugs-To: developers@frappe.io\n"
-"POT-Creation-Date: 2026-05-03 09:54+0000\n"
-"PO-Revision-Date: 2026-05-03 09:54+0000\n"
+"POT-Creation-Date: 2026-06-07 10:17+0000\n"
+"PO-Revision-Date: 2026-06-07 10:17+0000\n"
"Last-Translator: developers@frappe.io\n"
"Language-Team: developers@frappe.io\n"
"MIME-Version: 1.0\n"
@@ -59,7 +59,7 @@ msgstr ""
msgid "${values.doctype_name} has been added to queue for optimization"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/about.js:86
+#: frappe/public/js/frappe/ui/toolbar/about.js:68
msgid "© Frappe Technologies Pvt. Ltd. and contributors"
msgstr ""
@@ -68,7 +68,7 @@ msgstr ""
msgid "<head> HTML"
msgstr ""
-#: frappe/database/query.py:2355
+#: frappe/database/query.py:2358
msgid "'*' is only allowed in {0} SQL function(s)"
msgstr ""
@@ -76,7 +76,7 @@ msgstr ""
msgid "'In Global Search' is not allowed for field {0} of type {1}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1389
+#: frappe/core/doctype/doctype/doctype.py:1390
msgid "'In Global Search' not allowed for type {0} in row {1}"
msgstr ""
@@ -84,7 +84,7 @@ msgstr ""
msgid "'In List View' is not allowed for field {0} of type {1}"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:367
+#: frappe/custom/doctype/customize_form/customize_form.py:368
msgid "'In List View' not allowed for type {0} in row {1}"
msgstr ""
@@ -100,7 +100,7 @@ msgstr ""
msgid "'{0}' is not a valid URL"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1383
+#: frappe/core/doctype/doctype/doctype.py:1384
msgid "'{0}' not allowed for type {1} in row {2}"
msgstr ""
@@ -108,7 +108,7 @@ msgstr ""
msgid "(Mandatory)"
msgstr ""
-#: frappe/model/rename_doc.py:706
+#: frappe/model/rename_doc.py:695
msgid "** Failed: {0} to {1}: {2}"
msgstr ""
@@ -143,7 +143,7 @@ msgstr ""
msgid "0 is highest"
msgstr ""
-#: frappe/public/js/frappe/form/grid_row.js:883
+#: frappe/public/js/frappe/form/grid_row.js:909
msgid "1 = True & 0 = False"
msgstr ""
@@ -162,11 +162,11 @@ msgstr ""
msgid "1 Google Calendar Event synced."
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:996
+#: frappe/public/js/frappe/views/reports/query_report.js:997
msgid "1 Report"
msgstr ""
-#: frappe/tests/test_utils.py:906
+#: frappe/tests/test_utils.py:930
msgid "1 day ago"
msgstr ""
@@ -175,17 +175,17 @@ msgid "1 hour"
msgstr ""
#: frappe/public/js/frappe/utils/pretty_date.js:54
-#: frappe/tests/test_utils.py:904
+#: frappe/tests/test_utils.py:928
msgid "1 hour ago"
msgstr ""
#: frappe/public/js/frappe/utils/pretty_date.js:50
-#: frappe/tests/test_utils.py:902
+#: frappe/tests/test_utils.py:926
msgid "1 minute ago"
msgstr ""
#: frappe/public/js/frappe/utils/pretty_date.js:68
-#: frappe/tests/test_utils.py:910
+#: frappe/tests/test_utils.py:934
msgid "1 month ago"
msgstr ""
@@ -207,37 +207,37 @@ msgctxt "User added row to child table"
msgid "1 row to {0}"
msgstr ""
-#: frappe/tests/test_utils.py:901
+#: frappe/tests/test_utils.py:925
msgid "1 second ago"
msgstr ""
#: frappe/public/js/frappe/utils/pretty_date.js:64
-#: frappe/tests/test_utils.py:908
+#: frappe/tests/test_utils.py:932
msgid "1 week ago"
msgstr ""
#: frappe/public/js/frappe/utils/pretty_date.js:72
-#: frappe/tests/test_utils.py:912
+#: frappe/tests/test_utils.py:936
msgid "1 year ago"
msgstr ""
-#: frappe/tests/test_utils.py:905
+#: frappe/tests/test_utils.py:929
msgid "2 hours ago"
msgstr ""
-#: frappe/tests/test_utils.py:911
+#: frappe/tests/test_utils.py:935
msgid "2 months ago"
msgstr ""
-#: frappe/tests/test_utils.py:909
+#: frappe/tests/test_utils.py:933
msgid "2 weeks ago"
msgstr ""
-#: frappe/tests/test_utils.py:913
+#: frappe/tests/test_utils.py:937
msgid "2 years ago"
msgstr ""
-#: frappe/tests/test_utils.py:903
+#: frappe/tests/test_utils.py:927
msgid "3 minutes ago"
msgstr ""
@@ -253,7 +253,7 @@ msgstr ""
msgid "5 Records"
msgstr ""
-#: frappe/tests/test_utils.py:907
+#: frappe/tests/test_utils.py:931
msgid "5 days ago"
msgstr ""
@@ -624,7 +624,7 @@ msgstr ""
msgid ">="
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1058
+#: frappe/core/doctype/doctype/doctype.py:1059
msgid "A DocType's name should start with a letter and can only consist of letters, numbers, spaces, underscores and hyphens"
msgstr ""
@@ -642,7 +642,7 @@ msgstr ""
msgid "A field with the name {0} already exists in {1}"
msgstr ""
-#: frappe/core/doctype/file/file.py:343
+#: frappe/core/doctype/file/file.py:346
msgid "A file with same name {} already exists"
msgstr ""
@@ -825,7 +825,7 @@ msgstr ""
#. Label of a standard help item
#. Type: Action
-#: frappe/hooks.py
+#: frappe/hooks.py frappe/public/js/frappe/ui/toolbar/about.js:8
msgid "About"
msgstr ""
@@ -1016,7 +1016,7 @@ msgstr ""
#: frappe/public/js/frappe/views/reports/query_report.js:192
#: frappe/public/js/frappe/views/reports/query_report.js:205
#: frappe/public/js/frappe/views/reports/query_report.js:215
-#: frappe/public/js/frappe/views/reports/query_report.js:898
+#: frappe/public/js/frappe/views/reports/query_report.js:899
msgid "Actions"
msgstr ""
@@ -1076,10 +1076,10 @@ msgstr ""
#: frappe/core/page/permission_manager/permission_manager.js:534
#: frappe/email/doctype/email_group/email_group.js:60
-#: frappe/public/js/frappe/form/grid_row.js:487
+#: frappe/public/js/frappe/form/grid_row.js:489
#: frappe/public/js/frappe/form/sidebar/assign_to.js:112
#: frappe/public/js/frappe/form/templates/set_sharing.html:82
-#: frappe/public/js/frappe/list/bulk_operations.js:453
+#: frappe/public/js/frappe/list/bulk_operations.js:464
#: frappe/public/js/frappe/list/list_view.js:319
#: frappe/public/js/frappe/list/list_view.js:335
#: frappe/public/js/frappe/views/dashboard/dashboard_view.js:441
@@ -1089,7 +1089,7 @@ msgstr ""
msgid "Add"
msgstr ""
-#: frappe/public/js/frappe/form/grid_row.js:459
+#: frappe/public/js/frappe/form/grid_row.js:461
msgid "Add / Remove Columns"
msgstr ""
@@ -1138,8 +1138,8 @@ msgid "Add Child"
msgstr ""
#: frappe/public/js/frappe/views/kanban/kanban_board.html:4
-#: frappe/public/js/frappe/views/reports/query_report.js:1978
-#: frappe/public/js/frappe/views/reports/query_report.js:1981
+#: frappe/public/js/frappe/views/reports/query_report.js:1979
+#: frappe/public/js/frappe/views/reports/query_report.js:1982
#: frappe/public/js/frappe/views/reports/report_view.js:367
#: frappe/public/js/frappe/views/reports/report_view.js:392
#: frappe/public/js/print_format_builder/Field.vue:112
@@ -1150,7 +1150,7 @@ msgstr ""
msgid "Add Contact"
msgstr ""
-#: frappe/desk/doctype/event/event.js:38
+#: frappe/desk/doctype/event/event.js:36
msgid "Add Contacts"
msgstr ""
@@ -1191,7 +1191,7 @@ msgstr ""
msgid "Add New Permission Rule"
msgstr ""
-#: frappe/desk/doctype/event/event.js:35 frappe/desk/doctype/event/event.js:42
+#: frappe/desk/doctype/event/event.js:40
msgid "Add Participants"
msgstr ""
@@ -1205,7 +1205,7 @@ msgstr ""
msgid "Add Reply-To header"
msgstr ""
-#: frappe/core/doctype/user/user.py:890
+#: frappe/core/doctype/user/user.py:894
msgid "Add Roles"
msgstr ""
@@ -1230,11 +1230,11 @@ msgstr ""
msgid "Add Subscribers"
msgstr ""
-#: frappe/public/js/frappe/list/bulk_operations.js:441
+#: frappe/public/js/frappe/list/bulk_operations.js:452
msgid "Add Tags"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2287
+#: frappe/public/js/frappe/list/list_view.js:2281
msgctxt "Button in list view actions menu"
msgid "Add Tags"
msgstr ""
@@ -1324,7 +1324,7 @@ msgstr ""
msgid "Add field"
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:103
+#: frappe/public/js/frappe/form/grid.js:107
msgid "Add multiple"
msgstr ""
@@ -1341,7 +1341,7 @@ msgstr ""
msgid "Add page break"
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:100
+#: frappe/public/js/frappe/form/grid.js:104
msgid "Add row"
msgstr ""
@@ -1362,7 +1362,7 @@ msgstr ""
msgid "Add tab"
msgstr ""
-#: frappe/public/js/frappe/utils/dashboard_utils.js:262
+#: frappe/public/js/frappe/utils/dashboard_utils.js:275
#: frappe/public/js/frappe/views/reports/query_report.js:253
msgid "Add to Dashboard"
msgstr ""
@@ -1514,10 +1514,6 @@ msgstr ""
msgid "Adds a custom field to a DocType"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/search_utils.js:573
-msgid "Administration"
-msgstr ""
-
#. Name of a role
#: frappe/contacts/doctype/salutation/salutation.json
#: frappe/core/doctype/doctype/doctype.json
@@ -1541,15 +1537,15 @@ msgstr ""
msgid "Administrator"
msgstr ""
-#: frappe/core/doctype/user/user.py:1316
+#: frappe/core/doctype/user/user.py:1320
msgid "Administrator Logged In"
msgstr ""
-#: frappe/core/doctype/user/user.py:1310
+#: frappe/core/doctype/user/user.py:1314
msgid "Administrator accessed {0} on {1} via IP Address {2}."
msgstr ""
-#: frappe/desk/form/document_follow.py:52
+#: frappe/desk/form/document_follow.py:55
msgid "Administrator can't follow"
msgstr ""
@@ -1639,7 +1635,7 @@ msgstr ""
msgid "Aggregate Function Based On"
msgstr ""
-#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:410
+#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:412
msgid "Aggregate Function field is required to create a dashboard chart"
msgstr ""
@@ -1648,7 +1644,7 @@ msgstr ""
msgid "Alert"
msgstr ""
-#: frappe/database/query.py:2404
+#: frappe/database/query.py:2407
msgid "Alias must be a string"
msgstr ""
@@ -1704,6 +1700,7 @@ msgstr ""
#: frappe/geo/doctype/country/country.json
#: frappe/integrations/doctype/connected_app/connected_app.json
#: frappe/integrations/doctype/token_cache/token_cache.json
+#: frappe/public/js/frappe/ui/toolbar/search.js:729
#: frappe/website/doctype/personal_data_download_request/personal_data_download_request.json
#: frappe/website/doctype/website_settings/website_settings.json
msgid "All"
@@ -1713,7 +1710,7 @@ msgstr ""
#. Label of the all_day (Check) field in DocType 'Event'
#: frappe/desk/doctype/calendar_view/calendar_view.json
#: frappe/desk/doctype/event/event.json
-#: frappe/public/js/frappe/ui/notifications/notifications.js:444
+#: frappe/public/js/frappe/ui/notifications/notifications.js:446
msgid "All Day"
msgstr ""
@@ -1771,8 +1768,12 @@ msgid "Allow Auto Repeat"
msgstr ""
#. Label of the allow_bulk_edit (Check) field in DocType 'DocField'
+#. Label of the allow_bulk_edit (Check) field in DocType 'DocType'
+#. Label of the allow_bulk_edit (Check) field in DocType 'Customize Form'
#. Label of the allow_bulk_edit (Check) field in DocType 'Customize Form Field'
#: frappe/core/doctype/docfield/docfield.json
+#: frappe/core/doctype/doctype/doctype.json
+#: frappe/custom/doctype/customize_form/customize_form.json
#: frappe/custom/doctype/customize_form_field/customize_form_field.json
msgid "Allow Bulk Edit"
msgstr ""
@@ -2009,6 +2010,12 @@ msgstr ""
msgid "Allowed"
msgstr ""
+#. Label of the allowed_doctypes_for_guest_uploads (Small Text) field in
+#. DocType 'System Settings'
+#: frappe/core/doctype/system_settings/system_settings.json
+msgid "Allowed Doctypes for Guest Uploads"
+msgstr ""
+
#. Label of the allowed_file_extensions (Small Text) field in DocType 'System
#. Settings'
#: frappe/core/doctype/system_settings/system_settings.json
@@ -2132,15 +2139,15 @@ msgstr ""
msgid "Allows users to enable the mask property for any field of the respective doctype."
msgstr ""
-#: frappe/core/doctype/user/user.py:1117
+#: frappe/core/doctype/user/user.py:1121
msgid "Already Registered"
msgstr ""
-#: frappe/public/js/frappe/form/toolbar.js:741
+#: frappe/public/js/frappe/form/toolbar.js:742
msgid "Already amended as {0}"
msgstr ""
-#: frappe/desk/form/assign_to.py:137
+#: frappe/desk/form/assign_to.py:141
msgid "Already in the following Users ToDo list:{0}"
msgstr ""
@@ -2191,7 +2198,7 @@ msgstr ""
#: frappe/core/doctype/custom_docperm/custom_docperm.json
#: frappe/core/doctype/docperm/docperm.json
#: frappe/core/doctype/user_document_type/user_document_type.json
-#: frappe/public/js/frappe/form/toolbar.js:737
+#: frappe/public/js/frappe/form/toolbar.js:738
msgid "Amend"
msgstr ""
@@ -2300,7 +2307,7 @@ msgstr ""
msgid "Another transaction is blocking this one. Please try again in a few seconds."
msgstr ""
-#: frappe/model/rename_doc.py:379
+#: frappe/model/rename_doc.py:368
msgid "Another {0} with name {1} exists, select another name"
msgstr ""
@@ -2441,7 +2448,7 @@ msgstr ""
msgid "Apply"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2272
+#: frappe/public/js/frappe/list/list_view.js:2266
msgctxt "Button in list view actions menu"
msgid "Apply Assignment Rule"
msgstr ""
@@ -2532,7 +2539,7 @@ msgstr ""
msgid "Are you sure you want to clear the assignments?"
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:319
+#: frappe/public/js/frappe/form/grid.js:327
msgid "Are you sure you want to delete all {0} rows?"
msgstr ""
@@ -2564,7 +2571,7 @@ msgstr ""
msgid "Are you sure you want to discard the changes?"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1012
+#: frappe/public/js/frappe/views/reports/query_report.js:1013
msgid "Are you sure you want to generate a new report?"
msgstr ""
@@ -2588,6 +2595,10 @@ msgstr ""
msgid "Are you sure you want to relink this communication to {0}?"
msgstr ""
+#: frappe/core/doctype/communication/communication.js:166
+msgid "Are you sure you want to relink this communication?"
+msgstr ""
+
#: frappe/core/doctype/rq_job/rq_job_list.js:10
msgid "Are you sure you want to remove all failed jobs?"
msgstr ""
@@ -2627,7 +2638,7 @@ msgstr ""
msgid "As a best practice, do not assign the same set of permission rule to different Roles. Instead, set multiple Roles to the same User."
msgstr ""
-#: frappe/desk/form/assign_to.py:107
+#: frappe/desk/form/assign_to.py:111
msgid "As document sharing is disabled, please give them the required permissions before assigning."
msgstr ""
@@ -2641,7 +2652,7 @@ msgstr ""
msgid "Ask"
msgstr ""
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:91
+#: frappe/public/js/frappe/form/templates/form_sidebar.html:90
msgid "Assign"
msgstr ""
@@ -2654,7 +2665,7 @@ msgstr ""
msgid "Assign To"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2233
+#: frappe/public/js/frappe/list/list_view.js:2234
msgctxt "Button in list view actions menu"
msgid "Assign To"
msgstr ""
@@ -2773,7 +2784,7 @@ msgstr ""
msgid "Assignment Update on {0}"
msgstr ""
-#: frappe/desk/form/assign_to.py:78
+#: frappe/desk/form/assign_to.py:82
msgid "Assignment for {0} {1}"
msgstr ""
@@ -2794,7 +2805,7 @@ msgstr ""
msgid "Asynchronous"
msgstr ""
-#: frappe/public/js/frappe/form/grid_row.js:695
+#: frappe/public/js/frappe/form/grid_row.js:697
msgid "At least one column is required to show in the grid."
msgstr ""
@@ -2879,7 +2890,7 @@ msgstr ""
msgid "Attached To Name"
msgstr ""
-#: frappe/core/doctype/file/file.py:187
+#: frappe/core/doctype/file/file.py:190
msgid "Attached To Name must be a string or an integer"
msgstr ""
@@ -2895,7 +2906,7 @@ msgstr ""
msgid "Attachment Limit (MB)"
msgstr ""
-#: frappe/core/doctype/file/file.py:412
+#: frappe/core/doctype/file/file.py:415
#: frappe/public/js/frappe/form/sidebar/attachments.js:36
msgid "Attachment Limit Reached"
msgstr ""
@@ -2921,7 +2932,7 @@ msgstr ""
#. Label of the attachments (Code) field in DocType 'Email Queue'
#: frappe/email/doctype/email_queue/email_queue.json
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:106
+#: frappe/public/js/frappe/form/templates/form_sidebar.html:105
#: frappe/website/doctype/web_form/templates/web_form.html:122
msgid "Attachments"
msgstr ""
@@ -3142,7 +3153,7 @@ msgstr ""
msgid "Auto Reply Message"
msgstr ""
-#: frappe/automation/doctype/assignment_rule/assignment_rule.py:177
+#: frappe/automation/doctype/assignment_rule/assignment_rule.py:206
msgid "Auto assignment failed: {0}"
msgstr ""
@@ -3403,7 +3414,7 @@ msgstr ""
#. 'System Health Report'
#: frappe/core/workspace/build/build.json
#: frappe/desk/doctype/system_health_report/system_health_report.json
-#: frappe/public/js/frappe/ui/sidebar/sidebar.js:536
+#: frappe/public/js/frappe/ui/sidebar/sidebar.js:530
msgid "Background Jobs"
msgstr ""
@@ -3614,7 +3625,7 @@ msgstr ""
msgid "Beta"
msgstr ""
-#: frappe/core/doctype/user/user.py:1333 frappe/utils/password_strength.py:73
+#: frappe/core/doctype/user/user.py:1337 frappe/utils/password_strength.py:73
msgid "Better add a few more letters or another word"
msgstr ""
@@ -3817,19 +3828,20 @@ msgstr ""
msgid "Bulk Delete"
msgstr ""
-#: frappe/public/js/frappe/list/bulk_operations.js:321
+#: frappe/public/js/frappe/form/grid.js:1115
+#: frappe/public/js/frappe/list/bulk_operations.js:329
msgid "Bulk Edit"
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:1288
+#: frappe/public/js/frappe/form/grid.js:1479
msgid "Bulk Edit {0}"
msgstr ""
-#: frappe/desk/reportview.py:691
+#: frappe/desk/reportview.py:698
msgid "Bulk Operation Failed"
msgstr ""
-#: frappe/desk/reportview.py:695
+#: frappe/desk/reportview.py:702
msgid "Bulk Operation Successful"
msgstr ""
@@ -4005,10 +4017,6 @@ msgstr ""
msgid "Cache Cleared"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:234
-msgid "Calculate"
-msgstr ""
-
#. Option for the 'Select List View' (Select) field in DocType 'Form Tour'
#. Option for the 'DocType View' (Select) field in DocType 'Workspace Shortcut'
#: frappe/desk/doctype/form_tour/form_tour.json
@@ -4060,7 +4068,7 @@ msgid "Camera"
msgstr ""
#. Label of the campaign (Data) field in DocType 'Web Page View'
-#: frappe/public/js/frappe/utils/utils.js:2014
+#: frappe/public/js/frappe/utils/utils.js:2029
#: frappe/website/doctype/web_page_view/web_page_view.json
#: frappe/website/report/website_analytics/website_analytics.js:39
msgid "Campaign"
@@ -4076,7 +4084,7 @@ msgstr ""
msgid "Can not rename as column {0} is already present on DocType."
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1187
+#: frappe/core/doctype/doctype/doctype.py:1188
msgid "Can only change to/from Autoincrement naming rule when there is no data in the doctype"
msgstr ""
@@ -4086,11 +4094,11 @@ msgstr ""
msgid "Can only list down the document types which has been linked to the User document type."
msgstr ""
-#: frappe/desk/form/document_follow.py:48
+#: frappe/desk/form/document_follow.py:51
msgid "Can't follow since changes are not tracked."
msgstr ""
-#: frappe/model/rename_doc.py:366
+#: frappe/model/rename_doc.py:355
msgid "Can't rename {0} to {1} because {0} doesn't exist."
msgstr ""
@@ -4109,7 +4117,7 @@ msgstr ""
msgid "Cancel"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2342
+#: frappe/public/js/frappe/list/list_view.js:2336
msgctxt "Button in list view actions menu"
msgid "Cancel"
msgstr ""
@@ -4135,7 +4143,7 @@ msgstr ""
msgid "Cancel Prepared Report"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2347
+#: frappe/public/js/frappe/list/list_view.js:2341
msgctxt "Title of confirmation dialog"
msgid "Cancel {0} documents?"
msgstr ""
@@ -4155,7 +4163,7 @@ msgstr ""
msgid "Cancelled"
msgstr ""
-#: frappe/core/doctype/deleted_document/deleted_document.py:52
+#: frappe/core/doctype/deleted_document/deleted_document.py:67
msgid "Cancelled Document restored as Draft"
msgstr ""
@@ -4184,15 +4192,15 @@ msgstr ""
msgid "Cannot Remove"
msgstr ""
-#: frappe/model/base_document.py:1293
+#: frappe/model/base_document.py:1296
msgid "Cannot Update After Submit"
msgstr ""
-#: frappe/core/doctype/file/file.py:717
+#: frappe/core/doctype/file/file.py:720
msgid "Cannot access file path {0}"
msgstr ""
-#: frappe/core/doctype/file/file.py:150
+#: frappe/core/doctype/file/file.py:153
msgid "Cannot attach a folder to a document"
msgstr ""
@@ -4200,7 +4208,7 @@ msgstr ""
msgid "Cannot cancel before submitting while transitioning from {0} State to {1} State "
msgstr ""
-#: frappe/workflow/doctype/workflow/workflow.py:110
+#: frappe/workflow/doctype/workflow/workflow.py:111
msgid "Cannot cancel before submitting. See Transition {0}"
msgstr ""
@@ -4220,14 +4228,18 @@ msgstr ""
msgid "Cannot change state of Cancelled Document ({0} State) "
msgstr ""
-#: frappe/workflow/doctype/workflow/workflow.py:99
+#: frappe/workflow/doctype/workflow/workflow.py:100
msgid "Cannot change state of Cancelled Document. Transition row {0}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1177
+#: frappe/core/doctype/doctype/doctype.py:1178
msgid "Cannot change to/from autoincrement autoname in Customize Form"
msgstr ""
+#: frappe/desk/doctype/global_search_settings/global_search_settings.py:176
+msgid "Cannot configure Core DocTypes for Global Search."
+msgstr ""
+
#: frappe/core/doctype/communication/communication.py:169
msgid "Cannot create a {0} against a child document: {1}"
msgstr ""
@@ -4240,7 +4252,7 @@ msgstr ""
msgid "Cannot delete Desktop Icon '{0}' as it is restricted"
msgstr ""
-#: frappe/core/doctype/file/file.py:239
+#: frappe/core/doctype/file/file.py:242
msgid "Cannot delete Home and Attachments folders"
msgstr ""
@@ -4291,11 +4303,11 @@ msgstr ""
msgid "Cannot edit Standard Notification. To edit, please disable this and duplicate it"
msgstr ""
-#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:388
+#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:390
msgid "Cannot edit Standard charts"
msgstr ""
-#: frappe/core/doctype/report/report.py:73
+#: frappe/core/doctype/report/report.py:74
msgid "Cannot edit a standard report. Please duplicate and create a new report"
msgstr ""
@@ -4320,11 +4332,11 @@ msgstr ""
msgid "Cannot enable {0} for a non-submittable doctype"
msgstr ""
-#: frappe/core/doctype/file/file.py:338
+#: frappe/core/doctype/file/file.py:341
msgid "Cannot find file {} on disk"
msgstr ""
-#: frappe/core/doctype/file/file.py:657
+#: frappe/core/doctype/file/file.py:660
msgid "Cannot get file contents of a Folder"
msgstr ""
@@ -4332,7 +4344,7 @@ msgstr ""
msgid "Cannot have multiple printers mapped to a single print format."
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:1232
+#: frappe/public/js/frappe/form/grid.js:1423
msgid "Cannot import table with more than 5000 rows."
msgstr ""
@@ -4344,7 +4356,7 @@ msgstr ""
msgid "Cannot map because following condition fails:"
msgstr ""
-#: frappe/core/doctype/data_import/importer.py:975
+#: frappe/core/doctype/data_import/importer.py:978
msgid "Cannot match column {0} with any field"
msgstr ""
@@ -4352,7 +4364,7 @@ msgstr ""
msgid "Cannot move row"
msgstr ""
-#: frappe/utils/xlsxutils.py:150
+#: frappe/utils/xlsxutils.py:151
msgid "Cannot register an empty XLSX style"
msgstr ""
@@ -4377,7 +4389,7 @@ msgid "Cannot submit {0}."
msgstr ""
#: frappe/desk/doctype/bulk_update/bulk_update.js:26
-#: frappe/public/js/frappe/list/bulk_operations.js:378
+#: frappe/public/js/frappe/list/bulk_operations.js:387
msgid "Cannot update {0}"
msgstr ""
@@ -4453,7 +4465,6 @@ msgstr ""
msgid "Center"
msgstr ""
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:10
#: frappe/tests/test_translate.py:111
msgid "Change"
msgstr ""
@@ -4664,7 +4675,7 @@ msgstr ""
msgid "Child Item"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1681
+#: frappe/core/doctype/doctype/doctype.py:1682
msgid "Child Table {0} for field {1} must be virtual"
msgstr ""
@@ -4674,7 +4685,7 @@ msgstr ""
msgid "Child Tables are shown as a Grid in other DocTypes"
msgstr ""
-#: frappe/database/query.py:1172
+#: frappe/database/query.py:1174
msgid "Child query fields for '{0}' must be a list or tuple."
msgstr ""
@@ -4682,7 +4693,7 @@ msgstr ""
msgid "Choose Existing Card or create New Card"
msgstr ""
-#: frappe/public/js/frappe/views/workspace/workspace.js:652
+#: frappe/public/js/frappe/views/workspace/workspace.js:653
msgid "Choose a block or continue typing"
msgstr ""
@@ -4730,15 +4741,11 @@ msgstr ""
msgid "Clear All"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2248
+#: frappe/public/js/frappe/list/list_view.js:2249
msgctxt "Button in list view actions menu"
msgid "Clear Assignment"
msgstr ""
-#: frappe/public/js/frappe/ui/keyboard.js:289
-msgid "Clear Cache and Reload"
-msgstr ""
-
#: frappe/core/doctype/error_log/error_log_list.js:12
msgid "Clear Error Logs"
msgstr ""
@@ -4764,6 +4771,10 @@ msgstr ""
msgid "Clear all filters"
msgstr ""
+#: frappe/public/js/frappe/ui/keyboard.js:281
+msgid "Clear cache and reload"
+msgstr ""
+
#: frappe/public/js/frappe/views/communication.js:490
msgid "Clear the email message and add the template"
msgstr ""
@@ -4808,7 +4819,7 @@ msgstr ""
msgid "Click on the link below to verify your request"
msgstr ""
-#: frappe/public/js/frappe/views/workspace/workspace.js:721
+#: frappe/public/js/frappe/views/workspace/workspace.js:722
msgid "Click on {0} to edit"
msgstr ""
@@ -5016,7 +5027,7 @@ msgctxt "Shrink code field."
msgid "Collapse"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:2274
+#: frappe/public/js/frappe/views/reports/query_report.js:2282
#: frappe/public/js/frappe/views/treeview.js:124
msgid "Collapse All"
msgstr ""
@@ -5073,7 +5084,7 @@ msgstr ""
#: frappe/desk/doctype/number_card/number_card.json
#: frappe/desk/doctype/todo/todo.json
#: frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json
-#: frappe/public/js/frappe/views/reports/query_report.js:1293
+#: frappe/public/js/frappe/views/reports/query_report.js:1294
#: frappe/public/js/frappe/widgets/widget_dialog.js:546
#: frappe/public/js/frappe/widgets/widget_dialog.js:694
#: frappe/website/doctype/color/color.json
@@ -5129,11 +5140,11 @@ msgstr ""
msgid "Column Name cannot be empty"
msgstr ""
-#: frappe/public/js/frappe/form/grid_row.js:448
+#: frappe/public/js/frappe/form/grid_row.js:450
msgid "Column Width"
msgstr ""
-#: frappe/public/js/frappe/form/grid_row.js:660
+#: frappe/public/js/frappe/form/grid_row.js:662
msgid "Column width cannot be zero."
msgstr ""
@@ -5152,7 +5163,7 @@ msgstr ""
#: frappe/custom/doctype/custom_field/custom_field.json
#: frappe/custom/doctype/customize_form_field/customize_form_field.json
#: frappe/desk/doctype/kanban_board/kanban_board.json
-#: frappe/public/js/frappe/form/grid_row.js:593
+#: frappe/public/js/frappe/form/grid_row.js:595
msgid "Columns"
msgstr ""
@@ -5161,7 +5172,7 @@ msgstr ""
msgid "Columns / Fields"
msgstr ""
-#: frappe/public/js/frappe/views/kanban/kanban_view.js:412
+#: frappe/public/js/frappe/views/kanban/kanban_view.js:434
msgid "Columns based on"
msgstr ""
@@ -5199,11 +5210,11 @@ msgstr ""
msgid "Comment Type"
msgstr ""
-#: frappe/desk/form/utils.py:57
+#: frappe/desk/form/utils.py:58
msgid "Comment can only be edited by the owner"
msgstr ""
-#: frappe/desk/form/utils.py:73
+#: frappe/desk/form/utils.py:74
msgid "Comment publicity can only be updated by the original author or a System Manager."
msgstr ""
@@ -5219,7 +5230,7 @@ msgstr ""
msgid "Comments and Communications will be associated with this linked document"
msgstr ""
-#: frappe/templates/includes/comments/comments.py:52
+#: frappe/templates/includes/comments/comments.py:56
msgid "Comments cannot have links or email addresses"
msgstr ""
@@ -5326,7 +5337,7 @@ msgstr ""
msgid "Complete By"
msgstr ""
-#: frappe/core/doctype/user/user.py:544
+#: frappe/core/doctype/user/user.py:548
#: frappe/templates/emails/new_user.html:10
msgid "Complete Registration"
msgstr ""
@@ -5433,11 +5444,16 @@ msgstr ""
msgid "Configuration"
msgstr ""
+#. Label of the configure (Button) field in DocType 'Global Search DocType'
+#: frappe/desk/doctype/global_search_doctype/global_search_doctype.json
+msgid "Configure"
+msgstr ""
+
#: frappe/public/js/frappe/views/reports/report_view.js:566
msgid "Configure Chart"
msgstr ""
-#: frappe/public/js/frappe/form/grid_row.js:392
+#: frappe/public/js/frappe/form/grid_row.js:394
msgid "Configure Columns"
msgstr ""
@@ -5460,6 +5476,14 @@ msgid ""
"Default Naming will make the amended document to behave same as new documents."
msgstr ""
+#: frappe/desk/doctype/global_search_settings/global_search_settings.js:12
+msgid "Configure search fields"
+msgstr ""
+
+#: frappe/public/js/frappe/ui/toolbar/search.js:62
+msgid "Configure search settings"
+msgstr ""
+
#. Description of a DocType
#: frappe/core/doctype/document_naming_settings/document_naming_settings.json
msgid "Configure various aspects of how document naming works like naming series, current counter."
@@ -5637,11 +5661,11 @@ msgstr ""
msgid "Contacts"
msgstr ""
-#: frappe/utils/change_log.py:362
+#: frappe/utils/change_log.py:374
msgid "Contains {0} security fix"
msgstr ""
-#: frappe/utils/change_log.py:360
+#: frappe/utils/change_log.py:372
msgid "Contains {0} security fixes"
msgstr ""
@@ -5654,7 +5678,7 @@ msgstr ""
#. Label of the content (Data) field in DocType 'Web Page View'
#: frappe/core/doctype/comment/comment.json frappe/desk/doctype/note/note.json
#: frappe/desk/doctype/workspace/workspace.json
-#: frappe/public/js/frappe/utils/utils.js:2030
+#: frappe/public/js/frappe/utils/utils.js:2045
#: frappe/website/doctype/help_article/help_article.json
#: frappe/website/doctype/web_page/web_page.json
#: frappe/website/doctype/web_page_view/web_page_view.json
@@ -5727,19 +5751,15 @@ msgstr ""
msgid "Copied to clipboard."
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2566
+#: frappe/public/js/frappe/list/list_view.js:2560
msgid "Copied {0} {1} to clipboard"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/about.js:69
-msgid "Copy Apps Version"
-msgstr ""
-
#: frappe/public/js/frappe/views/file/file_view.js:310
msgid "Copy File URL"
msgstr ""
-#: frappe/public/js/frappe/form/templates/timeline_message_box.html:93
+#: frappe/public/js/frappe/form/templates/timeline_message_box.html:94
msgid "Copy Link"
msgstr ""
@@ -5753,7 +5773,7 @@ msgstr ""
#: frappe/public/js/frappe/form/controls/code.js:32
#: frappe/public/js/frappe/form/toolbar.js:543
-#: frappe/public/js/frappe/list/list_view.js:2450
+#: frappe/public/js/frappe/list/list_view.js:2444
msgid "Copy to Clipboard"
msgstr ""
@@ -5766,11 +5786,11 @@ msgstr ""
msgid "Copyright"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:126
+#: frappe/custom/doctype/customize_form/customize_form.py:127
msgid "Core DocTypes cannot be customized."
msgstr ""
-#: frappe/desk/doctype/global_search_settings/global_search_settings.py:36
+#: frappe/desk/doctype/global_search_settings/global_search_settings.py:38
msgid "Core Modules {0} cannot be searched in Global Search."
msgstr ""
@@ -5786,15 +5806,15 @@ msgstr ""
msgid "Could not find {0}"
msgstr ""
-#: frappe/core/doctype/data_import/importer.py:937
+#: frappe/core/doctype/data_import/importer.py:938
msgid "Could not map column {0} to field {1}"
msgstr ""
-#: frappe/database/query.py:1075
+#: frappe/database/query.py:1077
msgid "Could not parse field: {0}"
msgstr ""
-#: frappe/utils/pdf_generator/chrome_pdf_generator.py:168
+#: frappe/utils/pdf_generator/chrome_pdf_generator.py:175
msgid "Could not start Chromium. Check logs for details."
msgstr ""
@@ -5802,7 +5822,7 @@ msgstr ""
msgid "Could not start up:"
msgstr ""
-#: frappe/public/js/frappe/web_form/web_form.js:381
+#: frappe/public/js/frappe/web_form/web_form.js:384
msgid "Couldn't save, please check the data you have entered"
msgstr ""
@@ -5889,7 +5909,7 @@ msgstr ""
#: frappe/public/js/frappe/list/list_filter.js:121
#: frappe/public/js/frappe/views/file/file_view.js:118
#: frappe/public/js/frappe/views/interaction.js:18
-#: frappe/public/js/frappe/views/reports/query_report.js:1325
+#: frappe/public/js/frappe/views/reports/query_report.js:1326
#: frappe/public/js/frappe/views/workspace/workspace.js:517
#: frappe/workflow/page/workflow_builder/workflow_builder.js:46
msgid "Create"
@@ -5909,7 +5929,7 @@ msgid "Create Card"
msgstr ""
#: frappe/public/js/frappe/views/reports/query_report.js:286
-#: frappe/public/js/frappe/views/reports/query_report.js:1252
+#: frappe/public/js/frappe/views/reports/query_report.js:1253
msgid "Create Chart"
msgstr ""
@@ -5972,19 +5992,11 @@ msgstr ""
msgid "Create a Reminder"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/search_utils.js:558
-msgid "Create a new ..."
-msgstr ""
-
#: frappe/public/js/frappe/list/list_view.js:311
msgctxt "Description of a list view shortcut"
msgid "Create a new document"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:228
-msgid "Create a new record"
-msgstr ""
-
#: frappe/public/js/frappe/form/controls/link.js:505
#: frappe/public/js/frappe/form/controls/link.js:507
#: frappe/public/js/frappe/form/link_selector.js:146
@@ -6031,11 +6043,11 @@ msgstr ""
msgid "Created By"
msgstr ""
-#: frappe/public/js/frappe/form/sidebar/form_sidebar.js:174
+#: frappe/public/js/frappe/form/sidebar/form_sidebar.js:177
msgid "Created By You"
msgstr ""
-#: frappe/public/js/frappe/form/sidebar/form_sidebar.js:175
+#: frappe/public/js/frappe/form/sidebar/form_sidebar.js:178
msgid "Created By {0}"
msgstr ""
@@ -6140,6 +6152,11 @@ msgstr ""
msgid "Current"
msgstr ""
+#. Label of the current_index (Int) field in DocType 'Assignment Rule'
+#: frappe/automation/doctype/assignment_rule/assignment_rule.json
+msgid "Current Index"
+msgstr ""
+
#. Label of the current_job_id (Link) field in DocType 'RQ Worker'
#: frappe/core/doctype/rq_worker/rq_worker.json
msgid "Current Job ID"
@@ -6229,7 +6246,7 @@ msgstr ""
msgid "Custom Document Types (Select Permission)"
msgstr ""
-#: frappe/desk/desktop.py:479
+#: frappe/desk/desktop.py:476
msgid "Custom Documents"
msgstr ""
@@ -6322,7 +6339,7 @@ msgstr ""
msgid "Custom Report"
msgstr ""
-#: frappe/desk/desktop.py:480
+#: frappe/desk/desktop.py:477
msgid "Custom Reports"
msgstr ""
@@ -6391,13 +6408,13 @@ msgstr ""
#. Label of a Workspace Sidebar Item
#: frappe/printing/page/print/print.js:193
#: frappe/public/js/frappe/form/templates/print_layout.html:39
-#: frappe/public/js/frappe/form/toolbar.js:636
+#: frappe/public/js/frappe/form/toolbar.js:637
#: frappe/public/js/frappe/views/dashboard/dashboard_view.js:198
#: frappe/workspace_sidebar/website.json
msgid "Customize"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2009
+#: frappe/public/js/frappe/list/list_view.js:2010
msgctxt "Button in list view menu"
msgid "Customize"
msgstr ""
@@ -6417,7 +6434,7 @@ msgstr ""
#: frappe/core/doctype/doctype/doctype.js:61
#: frappe/core/workspace/build/build.json
#: frappe/custom/doctype/customize_form/customize_form.json
-#: frappe/public/js/frappe/views/kanban/kanban_view.js:358
+#: frappe/public/js/frappe/views/kanban/kanban_view.js:380
#: frappe/workspace_sidebar/build.json
msgid "Customize Form"
msgstr ""
@@ -6431,7 +6448,7 @@ msgstr ""
msgid "Customize Form Field"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2035
+#: frappe/public/js/frappe/list/list_view.js:2036
msgctxt "Customize qucik filters of List View"
msgid "Customize Quick Filters"
msgstr ""
@@ -6565,7 +6582,6 @@ msgstr ""
#: frappe/desk/doctype/form_tour/form_tour.json
#: frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json
#: frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json
-#: frappe/public/js/frappe/ui/toolbar/search_utils.js:583
#: frappe/public/js/frappe/utils/utils.js:959
#: frappe/workspace_sidebar/build.json
msgid "Dashboard"
@@ -6682,7 +6698,7 @@ msgstr ""
msgid "Data Import is not allowed for {0}. Enable 'Allow Import' in DocType settings."
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:623
+#: frappe/custom/doctype/customize_form/customize_form.py:624
msgid "Data Too Long"
msgstr ""
@@ -6713,7 +6729,7 @@ msgstr ""
msgid "Database Storage Usage By Tables"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:252
+#: frappe/custom/doctype/customize_form/customize_form.py:253
msgid "Database Table Row Size Limit"
msgstr ""
@@ -7031,11 +7047,11 @@ msgstr ""
msgid "Default display currency"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1411
+#: frappe/core/doctype/doctype/doctype.py:1412
msgid "Default for 'Check' type of field {0} must be either '0' or '1'"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1424
+#: frappe/core/doctype/doctype/doctype.py:1425
msgid "Default value for {0} must be in the list of options."
msgstr ""
@@ -7111,7 +7127,7 @@ msgstr ""
msgid "Delete"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2310
+#: frappe/public/js/frappe/list/list_view.js:2304
msgctxt "Button in list view actions menu"
msgid "Delete"
msgstr ""
@@ -7154,15 +7170,15 @@ msgctxt "Title of confirmation dialog"
msgid "Delete Tab"
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:92
+#: frappe/public/js/frappe/form/grid.js:96
msgid "Delete all"
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:367
+#: frappe/public/js/frappe/form/grid.js:375
msgid "Delete all {0} rows"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:979
+#: frappe/public/js/frappe/views/reports/query_report.js:980
msgid "Delete and Generate New"
msgstr ""
@@ -7190,7 +7206,7 @@ msgctxt "Button text"
msgid "Delete entire tab with fields"
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:237
+#: frappe/public/js/frappe/form/grid.js:242
msgid "Delete row"
msgstr ""
@@ -7208,17 +7224,17 @@ msgstr ""
msgid "Delete this record to allow sending to this email address"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2315
+#: frappe/public/js/frappe/list/list_view.js:2309
msgctxt "Title of confirmation dialog"
msgid "Delete {0} item permanently?"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2321
+#: frappe/public/js/frappe/list/list_view.js:2315
msgctxt "Title of confirmation dialog"
msgid "Delete {0} items permanently?"
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:240
+#: frappe/public/js/frappe/form/grid.js:246
msgid "Delete {0} rows"
msgstr ""
@@ -7250,7 +7266,7 @@ msgstr ""
msgid "Deleted Name"
msgstr ""
-#: frappe/desk/reportview.py:695
+#: frappe/desk/reportview.py:702
msgid "Deleted all documents successfully"
msgstr ""
@@ -7258,7 +7274,7 @@ msgstr ""
msgid "Deleted!"
msgstr ""
-#: frappe/desk/reportview.py:672
+#: frappe/desk/reportview.py:679
msgid "Deleting {0}"
msgstr ""
@@ -7323,10 +7339,6 @@ msgstr ""
msgid "Dependencies"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/about.js:79
-msgid "Dependencies & Licenses"
-msgstr ""
-
#. Label of the depends_on (Code) field in DocType 'Custom Field'
#. Label of the depends_on (Code) field in DocType 'Customize Form Field'
#: frappe/custom/doctype/custom_field/custom_field.json
@@ -7459,7 +7471,6 @@ msgstr ""
#. Name of a DocType
#: frappe/desk/doctype/desktop_icon/desktop_icon.json
-#: frappe/public/js/frappe/ui/toolbar/search_utils.js:578
msgid "Desktop Icon"
msgstr ""
@@ -7519,7 +7530,7 @@ msgstr ""
msgid "Digits"
msgstr ""
-#: frappe/utils/data.py:1563
+#: frappe/utils/data.py:1567
msgctxt "Currency"
msgid "Dinars"
msgstr ""
@@ -7564,6 +7575,12 @@ msgstr ""
msgid "Disable Document Sharing"
msgstr ""
+#. Label of the disable_prepared_report_automation (Check) field in DocType
+#. 'Report'
+#: frappe/core/doctype/report/report.json
+msgid "Disable Prepared Report Automation"
+msgstr ""
+
#. Label of the disable_product_suggestion (Check) field in DocType 'System
#. Settings'
#: frappe/core/doctype/system_settings/system_settings.json
@@ -7654,7 +7671,7 @@ msgstr ""
msgid "Disabled Auto Reply"
msgstr ""
-#: frappe/desk/page/desktop/desktop.html:61
+#: frappe/desk/page/desktop/desktop.html:62
#: frappe/public/js/frappe/form/toolbar.js:392
#: frappe/public/js/frappe/views/dashboard/dashboard_view.js:71
#: frappe/public/js/frappe/views/workspace/workspace.js:399
@@ -7728,10 +7745,7 @@ msgid "Display Depends On"
msgstr ""
#. Label of the depends_on (Code) field in DocType 'DocField'
-#. Label of the display_depends_on (Code) field in DocType 'Workspace Sidebar
-#. Item'
#: frappe/core/doctype/docfield/docfield.json
-#: frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json
msgid "Display Depends On (JS)"
msgstr ""
@@ -7750,7 +7764,7 @@ msgstr ""
msgid "Do not create new user if user with email does not exist in the system"
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:1293
+#: frappe/public/js/frappe/form/grid.js:1484
msgid "Do not edit headers which are preset in the template"
msgstr ""
@@ -7854,7 +7868,7 @@ msgstr ""
msgid "DocType"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1612
+#: frappe/core/doctype/doctype/doctype.py:1613
msgid "DocType {0} provided for the field {1} must have atleast one Link field"
msgstr ""
@@ -7905,11 +7919,11 @@ msgstr ""
msgid "DocType View"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:670
+#: frappe/core/doctype/doctype/doctype.py:671
msgid "DocType can not be merged"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:664
+#: frappe/core/doctype/doctype/doctype.py:665
msgid "DocType can only be renamed by Administrator"
msgstr ""
@@ -7947,7 +7961,7 @@ msgstr ""
msgid "DocType {} not found"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1052
+#: frappe/core/doctype/doctype/doctype.py:1053
msgid "DocType's name should not start or end with whitespace"
msgstr ""
@@ -7961,7 +7975,7 @@ msgstr ""
msgid "Doctype"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1046
+#: frappe/core/doctype/doctype/doctype.py:1047
msgid "Doctype name is limited to {0} characters ({1})"
msgstr ""
@@ -8000,7 +8014,7 @@ msgstr ""
msgid "Document Follow"
msgstr ""
-#: frappe/desk/form/document_follow.py:100
+#: frappe/desk/form/document_follow.py:103
msgid "Document Follow Notification"
msgstr ""
@@ -8023,19 +8037,19 @@ msgstr ""
msgid "Document Links"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1235
+#: frappe/core/doctype/doctype/doctype.py:1236
msgid "Document Links Row #{0}: Could not find field {1} in {2} DocType"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1255
+#: frappe/core/doctype/doctype/doctype.py:1256
msgid "Document Links Row #{0}: Invalid doctype or fieldname."
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1218
+#: frappe/core/doctype/doctype/doctype.py:1219
msgid "Document Links Row #{0}: Parent DocType is mandatory for internal links"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1224
+#: frappe/core/doctype/doctype/doctype.py:1225
msgid "Document Links Row #{0}: Table Fieldname is mandatory for internal links"
msgstr ""
@@ -8054,6 +8068,10 @@ msgstr ""
msgid "Document Name"
msgstr ""
+#: frappe/desk/doctype/global_search_settings/global_search_settings.py:134
+msgid "Document Name (ID)"
+msgstr ""
+
#: frappe/client.py:420
msgid "Document Name must not be empty"
msgstr ""
@@ -8081,7 +8099,7 @@ msgstr ""
msgid "Document Restoration Summary"
msgstr ""
-#: frappe/core/doctype/deleted_document/deleted_document.py:68
+#: frappe/core/doctype/deleted_document/deleted_document.py:96
msgid "Document Restored"
msgstr ""
@@ -8205,12 +8223,17 @@ msgstr ""
msgid "Document Type is not submittable"
msgstr ""
+#: frappe/desk/doctype/global_search_settings/global_search_settings.py:126
+#: frappe/desk/doctype/global_search_settings/global_search_settings.py:174
+msgid "Document Type is required"
+msgstr ""
+
#. Label of the document_type (Link) field in DocType 'Milestone Tracker'
#: frappe/automation/doctype/milestone_tracker/milestone_tracker.json
msgid "Document Type to Track"
msgstr ""
-#: frappe/desk/doctype/global_search_settings/global_search_settings.py:40
+#: frappe/desk/doctype/global_search_settings/global_search_settings.py:42
msgid "Document Type {0} has been repeated."
msgstr ""
@@ -8238,7 +8261,7 @@ msgstr ""
msgid "Document cannot be used as a filter value"
msgstr ""
-#: frappe/desk/form/document_follow.py:59
+#: frappe/desk/form/document_follow.py:62
msgid "Document follow is not enabled for this user."
msgstr ""
@@ -8258,11 +8281,11 @@ msgstr ""
msgid "Document is only editable by users with role"
msgstr ""
-#: frappe/core/doctype/communication/communication.js:182
+#: frappe/core/doctype/communication/communication.js:185
msgid "Document not Relinked"
msgstr ""
-#: frappe/model/rename_doc.py:229 frappe/public/js/frappe/form/toolbar.js:165
+#: frappe/model/rename_doc.py:218 frappe/public/js/frappe/form/toolbar.js:165
msgid "Document renamed from {0} to {1}"
msgstr ""
@@ -8270,11 +8293,11 @@ msgstr ""
msgid "Document renaming from {0} to {1} has been queued"
msgstr ""
-#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:397
+#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:399
msgid "Document type is required to create a dashboard chart"
msgstr ""
-#: frappe/core/doctype/deleted_document/deleted_document.py:45
+#: frappe/core/doctype/deleted_document/deleted_document.py:46
msgid "Document {0} Already Restored"
msgstr ""
@@ -8282,10 +8305,6 @@ msgstr ""
msgid "Document {0} has been set to state {1} by {2}"
msgstr ""
-#: frappe/public/js/frappe/form/controls/base_input.js:232
-msgid "Documentation"
-msgstr ""
-
#. Label of the documentation (Data) field in DocType 'DocType'
#: frappe/core/doctype/doctype/doctype.json
msgid "Documentation Link"
@@ -8343,7 +8362,7 @@ msgstr ""
msgid "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field"
msgstr ""
-#: frappe/public/js/frappe/data_import/import_preview.js:272
+#: frappe/public/js/frappe/data_import/import_preview.js:274
msgid "Don't Import"
msgstr ""
@@ -8393,7 +8412,7 @@ msgstr ""
#: frappe/core/doctype/file/file.js:15 frappe/core/doctype/user/user.js:483
#: frappe/email/doctype/auto_email_report/auto_email_report.js:8
-#: frappe/public/js/frappe/form/grid.js:110
+#: frappe/public/js/frappe/form/grid.js:114
msgid "Download"
msgstr ""
@@ -8422,7 +8441,7 @@ msgstr ""
msgid "Download PDF"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:888
+#: frappe/public/js/frappe/views/reports/query_report.js:889
msgid "Download Report"
msgstr ""
@@ -8518,7 +8537,7 @@ msgstr ""
msgid "Duplicate Filter Name"
msgstr ""
-#: frappe/model/base_document.py:779 frappe/model/rename_doc.py:111
+#: frappe/model/base_document.py:782 frappe/model/rename_doc.py:100
msgid "Duplicate Name"
msgstr ""
@@ -8530,15 +8549,15 @@ msgstr ""
msgid "Duplicate field"
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:238
+#: frappe/public/js/frappe/form/grid.js:244
msgid "Duplicate row"
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:96
+#: frappe/public/js/frappe/form/grid.js:100
msgid "Duplicate rows"
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:241
+#: frappe/public/js/frappe/form/grid.js:248
msgid "Duplicate {0} rows"
msgstr ""
@@ -8627,12 +8646,13 @@ msgstr ""
#: frappe/public/js/frappe/form/controls/markdown_editor.js:31
#: frappe/public/js/frappe/form/footer/form_timeline.js:670
#: frappe/public/js/frappe/form/footer/form_timeline.js:678
+#: frappe/public/js/frappe/form/grid.js:92
#: frappe/public/js/frappe/form/templates/address_list.html:13
#: frappe/public/js/frappe/form/templates/contact_list.html:13
#: frappe/public/js/frappe/form/toolbar.js:214
-#: frappe/public/js/frappe/form/toolbar.js:790
-#: frappe/public/js/frappe/views/reports/query_report.js:914
-#: frappe/public/js/frappe/views/reports/query_report.js:1924
+#: frappe/public/js/frappe/form/toolbar.js:791
+#: frappe/public/js/frappe/views/reports/query_report.js:915
+#: frappe/public/js/frappe/views/reports/query_report.js:1925
#: frappe/public/js/frappe/widgets/base_widget.js:65
#: frappe/public/js/frappe/widgets/chart_widget.js:304
#: frappe/public/js/frappe/widgets/number_card_widget.js:365
@@ -8643,7 +8663,7 @@ msgstr ""
msgid "Edit"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2396
+#: frappe/public/js/frappe/list/list_view.js:2390
msgctxt "Button in list view actions menu"
msgid "Edit"
msgstr ""
@@ -8653,7 +8673,7 @@ msgctxt "Button in web form"
msgid "Edit"
msgstr ""
-#: frappe/public/js/frappe/form/grid_row.js:340
+#: frappe/public/js/frappe/form/grid_row.js:342
msgctxt "Edit grid row"
msgid "Edit"
msgstr ""
@@ -8678,11 +8698,11 @@ msgstr ""
msgid "Edit Custom HTML"
msgstr ""
-#: frappe/public/js/frappe/form/toolbar.js:655
+#: frappe/public/js/frappe/form/toolbar.js:656
msgid "Edit DocType"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2028
+#: frappe/public/js/frappe/list/list_view.js:2029
msgctxt "Button in list view menu"
msgid "Edit DocType"
msgstr ""
@@ -8789,6 +8809,10 @@ msgstr ""
msgid "Edit mode"
msgstr ""
+#: frappe/public/js/frappe/form/grid.js:243
+msgid "Edit row"
+msgstr ""
+
#: frappe/public/js/form_builder/components/Field.vue:249
msgid "Edit the {0} Doctype"
msgstr ""
@@ -8797,7 +8821,7 @@ msgstr ""
msgid "Edit to add content"
msgstr ""
-#: frappe/public/js/frappe/web_form/web_form.js:470
+#: frappe/public/js/frappe/web_form/web_form.js:473
msgctxt "Button in web form"
msgid "Edit your response"
msgstr ""
@@ -8811,6 +8835,10 @@ msgstr ""
msgid "Edit {0}"
msgstr ""
+#: frappe/public/js/frappe/form/grid.js:247
+msgid "Edit {0} rows"
+msgstr ""
+
#. Label of the editable_grid (Check) field in DocType 'DocType'
#. Label of the editable_grid (Check) field in DocType 'Customize Form'
#: frappe/core/doctype/doctype/doctype.json
@@ -8901,7 +8929,7 @@ msgstr ""
#. Label of the email_account (Link) field in DocType 'Email Queue'
#. Label of the email_account (Link) field in DocType 'Unhandled Email'
#. Label of a Workspace Sidebar Item
-#: frappe/core/doctype/communication/communication.js:199
+#: frappe/core/doctype/communication/communication.js:202
#: frappe/core/doctype/communication/communication.json
#: frappe/core/doctype/user_email/user_email.json
#: frappe/email/doctype/email_account/email_account.json
@@ -8921,7 +8949,7 @@ msgstr ""
msgid "Email Account Name"
msgstr ""
-#: frappe/core/doctype/user/user.py:820
+#: frappe/core/doctype/user/user.py:824
msgid "Email Account added multiple times"
msgstr ""
@@ -9117,11 +9145,11 @@ msgstr ""
msgid "Email Unsubscribe"
msgstr ""
-#: frappe/core/doctype/communication/communication.js:342
+#: frappe/core/doctype/communication/communication.js:345
msgid "Email has been marked as spam"
msgstr ""
-#: frappe/core/doctype/communication/communication.js:355
+#: frappe/core/doctype/communication/communication.js:358
msgid "Email has been moved to trash"
msgstr ""
@@ -9180,7 +9208,7 @@ msgstr ""
msgid "Embed code copied"
msgstr ""
-#: frappe/database/query.py:2408
+#: frappe/database/query.py:2411
msgid "Empty alias is not allowed"
msgstr ""
@@ -9188,7 +9216,7 @@ msgstr ""
msgid "Empty column"
msgstr ""
-#: frappe/database/query.py:2349
+#: frappe/database/query.py:2352
msgid "Empty string arguments are not allowed"
msgstr ""
@@ -9347,6 +9375,17 @@ msgstr ""
msgid "Enable Two Factor Auth"
msgstr ""
+#. Description of the 'Allow Bulk Edit' (Check) field in DocType 'Customize
+#. Form'
+#: frappe/custom/doctype/customize_form/customize_form.json
+msgid "Enable bulk edit for child table fields in Form view."
+msgstr ""
+
+#. Description of the 'Allow Bulk Edit' (Check) field in DocType 'DocType'
+#: frappe/core/doctype/doctype/doctype.json
+msgid "Enable bulk update of this field across child table rows."
+msgstr ""
+
#: frappe/printing/doctype/print_format_field_template/print_format_field_template.py:28
msgid "Enable developer mode to create a standard Print Template"
msgstr ""
@@ -9684,7 +9723,7 @@ msgstr ""
msgid "Error parsing nested filters: {0}. {1}"
msgstr ""
-#: frappe/desk/search.py:256
+#: frappe/desk/search.py:262
msgid "Error validating \"Ignore User Permissions\""
msgstr ""
@@ -9700,15 +9739,15 @@ msgstr ""
msgid "Error {0}: {1}"
msgstr ""
-#: frappe/model/base_document.py:933
+#: frappe/model/base_document.py:936
msgid "Error: Data missing in table {0}"
msgstr ""
-#: frappe/model/base_document.py:943
+#: frappe/model/base_document.py:946
msgid "Error: Value missing for {0}: {1}"
msgstr ""
-#: frappe/model/base_document.py:937
+#: frappe/model/base_document.py:940
msgid "Error: {0} Row #{1}: Value missing for: {2}"
msgstr ""
@@ -9772,7 +9811,7 @@ msgstr ""
msgid "Event Type"
msgstr ""
-#: frappe/public/js/frappe/ui/notifications/notifications.js:70
+#: frappe/public/js/frappe/ui/notifications/notifications.js:72
msgid "Events"
msgstr ""
@@ -9869,7 +9908,7 @@ msgstr ""
msgid "Executing..."
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:2290
+#: frappe/public/js/frappe/views/reports/query_report.js:2298
msgid "Execution Time: {0} sec"
msgstr ""
@@ -9895,16 +9934,16 @@ msgctxt "Enlarge code field."
msgid "Expand"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:2272
+#: frappe/public/js/frappe/views/reports/query_report.js:2280
#: frappe/public/js/frappe/views/treeview.js:134
msgid "Expand All"
msgstr ""
-#: frappe/database/query.py:726
+#: frappe/database/query.py:728
msgid "Expected 'and' or 'or' operator, found: {0}"
msgstr ""
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:40
+#: frappe/public/js/frappe/form/templates/form_sidebar.html:39
msgid "Experimental"
msgstr ""
@@ -9969,13 +10008,13 @@ msgstr ""
#: frappe/core/page/permission_manager/permission_manager_help.html:66
#: frappe/public/js/frappe/data_import/data_exporter.js:92
#: frappe/public/js/frappe/data_import/data_exporter.js:247
-#: frappe/public/js/frappe/views/reports/query_report.js:1966
+#: frappe/public/js/frappe/views/reports/query_report.js:1967
#: frappe/public/js/frappe/views/reports/report_view.js:1731
#: frappe/public/js/frappe/widgets/chart_widget.js:320
msgid "Export"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2438
+#: frappe/public/js/frappe/list/list_view.js:2432
msgctxt "Button in list view actions menu"
msgid "Export"
msgstr ""
@@ -9997,7 +10036,7 @@ msgid "Export Data"
msgstr ""
#: frappe/core/doctype/data_import/data_import.js:87
-#: frappe/public/js/frappe/data_import/import_preview.js:199
+#: frappe/public/js/frappe/data_import/import_preview.js:201
msgid "Export Errored Rows"
msgstr ""
@@ -10006,7 +10045,7 @@ msgstr ""
msgid "Export From"
msgstr ""
-#: frappe/core/doctype/data_import/data_import.js:544
+#: frappe/core/doctype/data_import/data_import.js:546
msgid "Export Import Log"
msgstr ""
@@ -10173,7 +10212,7 @@ msgid "Failed to change password."
msgstr ""
#: frappe/desk/page/setup_wizard/setup_wizard.js:251
-#: frappe/desk/page/setup_wizard/setup_wizard.py:42
+#: frappe/desk/page/setup_wizard/setup_wizard.py:43
msgid "Failed to complete setup"
msgstr ""
@@ -10198,7 +10237,7 @@ msgstr ""
msgid "Failed to delete communication"
msgstr ""
-#: frappe/desk/reportview.py:689
+#: frappe/desk/reportview.py:696
msgid "Failed to delete {0} documents: {1}"
msgstr ""
@@ -10263,7 +10302,7 @@ msgstr ""
msgid "Failed to send notification email"
msgstr ""
-#: frappe/desk/page/setup_wizard/setup_wizard.py:24
+#: frappe/desk/page/setup_wizard/setup_wizard.py:25
msgid "Failed to update global settings"
msgstr ""
@@ -10277,7 +10316,7 @@ msgstr ""
msgid "Failing Scheduled Jobs (last 7 days)"
msgstr ""
-#: frappe/core/doctype/data_import/data_import.js:485
+#: frappe/core/doctype/data_import/data_import.js:487
msgid "Failure"
msgstr ""
@@ -10297,7 +10336,7 @@ msgstr ""
msgid "Fax"
msgstr ""
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:65
+#: frappe/public/js/frappe/form/templates/form_sidebar.html:64
msgid "Feedback"
msgstr ""
@@ -10333,7 +10372,7 @@ msgstr ""
msgid "Fetch on Save if Empty"
msgstr ""
-#: frappe/desk/doctype/global_search_settings/global_search_settings.py:61
+#: frappe/desk/doctype/global_search_settings/global_search_settings.py:63
msgid "Fetching default Global Search documents."
msgstr ""
@@ -10355,20 +10394,21 @@ msgstr ""
#: frappe/desk/doctype/bulk_update/bulk_update.json
#: frappe/desk/doctype/number_card/number_card.json
#: frappe/desk/doctype/onboarding_step/onboarding_step.json
-#: frappe/public/js/frappe/list/bulk_operations.js:327
+#: frappe/public/js/frappe/form/grid.js:1127
+#: frappe/public/js/frappe/list/bulk_operations.js:336
#: frappe/public/js/frappe/list/list_view_permission_restrictions.html:3
#: frappe/public/js/frappe/views/reports/query_report.js:237
-#: frappe/public/js/frappe/views/reports/query_report.js:2025
+#: frappe/public/js/frappe/views/reports/query_report.js:2026
#: frappe/website/doctype/web_form_field/web_form_field.json
#: frappe/website/doctype/web_form_list_column/web_form_list_column.json
msgid "Field"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:419
+#: frappe/core/doctype/doctype/doctype.py:420
msgid "Field \"route\" is mandatory for Web Views"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1561
+#: frappe/core/doctype/doctype/doctype.py:1562
msgid "Field \"title\" is mandatory if \"Website Search Field\" is set."
msgstr ""
@@ -10376,7 +10416,7 @@ msgstr ""
msgid "Field \"value\" is mandatory. Please specify value to be updated"
msgstr ""
-#: frappe/desk/search.py:271
+#: frappe/desk/search.py:277
msgid "Field {0} not found in {1}"
msgstr ""
@@ -10385,7 +10425,7 @@ msgstr ""
msgid "Field Description"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1101
+#: frappe/core/doctype/doctype/doctype.py:1102
msgid "Field Missing"
msgstr ""
@@ -10441,7 +10481,7 @@ msgstr ""
msgid "Field {0} is referring to non-existing doctype {1}."
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1689
+#: frappe/core/doctype/doctype/doctype.py:1690
msgid "Field {0} must be a virtual field to support virtual doctype."
msgstr ""
@@ -10467,16 +10507,16 @@ msgstr ""
#: frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json
#: frappe/desk/doctype/form_tour_step/form_tour_step.json
#: frappe/integrations/doctype/webhook_data/webhook_data.json
-#: frappe/public/js/frappe/form/grid_row.js:445
+#: frappe/public/js/frappe/form/grid_row.js:447
#: frappe/website/doctype/web_template_field/web_template_field.json
msgid "Fieldname"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:272
+#: frappe/core/doctype/doctype/doctype.py:273
msgid "Fieldname '{0}' conflicting with a {1} of the name {2} in {3}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1100
+#: frappe/core/doctype/doctype/doctype.py:1101
msgid "Fieldname called {0} must exist to enable autonaming"
msgstr ""
@@ -10504,7 +10544,7 @@ msgstr ""
msgid "Fieldname {0} conflicting with meta object"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:510
+#: frappe/core/doctype/doctype/doctype.py:511
#: frappe/public/js/form_builder/utils.js:299
msgid "Fieldname {0} is restricted"
msgstr ""
@@ -10540,7 +10580,7 @@ msgstr ""
msgid "Fields Multicheck"
msgstr ""
-#: frappe/core/doctype/file/file.py:505
+#: frappe/core/doctype/file/file.py:508
msgid "Fields `file_name` or `file_url` must be set for File"
msgstr ""
@@ -10548,7 +10588,7 @@ msgstr ""
msgid "Fields must be a list or tuple when as_list is enabled"
msgstr ""
-#: frappe/database/query.py:1121
+#: frappe/database/query.py:1123
msgid "Fields must be a string, list, tuple, pypika Field, or pypika Function"
msgstr ""
@@ -10576,7 +10616,7 @@ msgstr ""
msgid "Fieldtype cannot be changed from {0} to {1}"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:597
+#: frappe/custom/doctype/customize_form/customize_form.py:598
msgid "Fieldtype cannot be changed from {0} to {1} in row {2}"
msgstr ""
@@ -10636,7 +10676,7 @@ msgstr ""
msgid "File URL"
msgstr ""
-#: frappe/core/doctype/file/file.py:123
+#: frappe/core/doctype/file/file.py:126
msgid "File URL is required when copying an existing attachment."
msgstr ""
@@ -10644,7 +10684,7 @@ msgstr ""
msgid "File backup is ready"
msgstr ""
-#: frappe/core/doctype/file/file.py:720
+#: frappe/core/doctype/file/file.py:723
msgid "File name cannot have {0}"
msgstr ""
@@ -10652,7 +10692,7 @@ msgstr ""
msgid "File not attached"
msgstr ""
-#: frappe/core/doctype/file/file.py:830 frappe/public/js/frappe/request.js:198
+#: frappe/core/doctype/file/file.py:833 frappe/public/js/frappe/request.js:198
#: frappe/utils/file_manager.py:221
msgid "File size exceeded the maximum allowed size of {0} MB"
msgstr ""
@@ -10661,7 +10701,7 @@ msgstr ""
msgid "File too big"
msgstr ""
-#: frappe/core/doctype/file/file.py:464
+#: frappe/core/doctype/file/file.py:467
msgid "File type of {0} is not allowed"
msgstr ""
@@ -10669,7 +10709,7 @@ msgstr ""
msgid "File upload failed."
msgstr ""
-#: frappe/core/doctype/file/file.py:451 frappe/core/doctype/file/file.py:522
+#: frappe/core/doctype/file/file.py:454 frappe/core/doctype/file/file.py:525
msgid "File {0} does not exist"
msgstr ""
@@ -10722,11 +10762,11 @@ msgstr ""
msgid "Filter Values"
msgstr ""
-#: frappe/database/query.py:732
+#: frappe/database/query.py:734
msgid "Filter condition missing after operator: {0}"
msgstr ""
-#: frappe/database/query.py:819
+#: frappe/database/query.py:821
msgid "Filter fields have invalid backtick notation: {0}"
msgstr ""
@@ -10807,7 +10847,7 @@ msgstr ""
msgid "Filters Section"
msgstr ""
-#: frappe/public/js/frappe/views/kanban/kanban_view.js:203
+#: frappe/public/js/frappe/views/kanban/kanban_view.js:225
msgid "Filters saved"
msgstr ""
@@ -10824,12 +10864,8 @@ msgstr ""
msgid "Filters:"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/search_utils.js:593
-msgid "Find '{0}' in ..."
-msgstr ""
-
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:381
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:383
+#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:364
+#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:366
#: frappe/public/js/frappe/ui/toolbar/search_utils.js:152
#: frappe/public/js/frappe/ui/toolbar/search_utils.js:155
msgid "Find {0} in {1}"
@@ -10923,11 +10959,11 @@ msgstr ""
msgid "Fold"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1485
+#: frappe/core/doctype/doctype/doctype.py:1486
msgid "Fold can not be at the end of the form"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1483
+#: frappe/core/doctype/doctype/doctype.py:1484
msgid "Fold must come before a Section Break"
msgstr ""
@@ -10947,7 +10983,7 @@ msgstr ""
msgid "Folder name should not include '/' (slash)"
msgstr ""
-#: frappe/core/doctype/file/file.py:568
+#: frappe/core/doctype/file/file.py:571
msgid "Folder {0} is not empty"
msgstr ""
@@ -10956,12 +10992,12 @@ msgstr ""
msgid "Folio"
msgstr ""
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:151
-#: frappe/public/js/frappe/form/toolbar.js:950
+#: frappe/public/js/frappe/form/templates/form_sidebar.html:150
+#: frappe/public/js/frappe/form/toolbar.js:951
msgid "Follow"
msgstr ""
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:146
+#: frappe/public/js/frappe/form/templates/form_sidebar.html:145
msgid "Followed by"
msgstr ""
@@ -10969,7 +11005,7 @@ msgstr ""
msgid "Following Report Filters have missing values:"
msgstr ""
-#: frappe/desk/form/document_follow.py:66
+#: frappe/desk/form/document_follow.py:69
msgid "Following document {0}"
msgstr ""
@@ -10977,7 +11013,7 @@ msgstr ""
msgid "Following documents are linked with {0}"
msgstr ""
-#: frappe/website/doctype/web_form/web_form.py:112
+#: frappe/website/doctype/web_form/web_form.py:113
msgid "Following fields are missing:"
msgstr ""
@@ -11157,7 +11193,7 @@ msgid ""
"For ranges, use 5:10 (for values between 5 & 10)."
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:2287
+#: frappe/public/js/frappe/views/reports/query_report.js:2295
msgid "For comparison, use >5, <10 or =324. For ranges, use 5:10 (for values between 5 & 10)."
msgstr ""
@@ -11309,6 +11345,10 @@ msgstr ""
msgid "Fortnightly"
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/about.js:26
+msgid "Forum"
+msgstr ""
+
#: frappe/core/doctype/communication/communication.js:70
msgid "Forward"
msgstr ""
@@ -11347,16 +11387,8 @@ msgstr ""
msgid "Frappe"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/about.js:28
-msgid "Frappe Blog"
-msgstr ""
-
-#: frappe/public/js/frappe/ui/toolbar/about.js:34
-msgid "Frappe Forum"
-msgstr ""
-
-#: frappe/public/js/frappe/ui/toolbar/about.js:8
-msgid "Frappe Framework"
+#: frappe/public/js/frappe/ui/toolbar/about.js:35
+msgid "Frappe Framework Version"
msgstr ""
#: frappe/public/js/frappe/ui/theme_switcher.js:59
@@ -11379,7 +11411,7 @@ msgstr ""
#. Label of a standard help item
#. Type: Route
-#: frappe/hooks.py
+#: frappe/hooks.py frappe/public/js/frappe/ui/toolbar/about.js:46
msgid "Frappe Support"
msgstr ""
@@ -11449,7 +11481,7 @@ msgstr ""
msgid "From Date Field"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1986
+#: frappe/public/js/frappe/views/reports/query_report.js:1987
msgid "From Document Type"
msgstr ""
@@ -11516,7 +11548,7 @@ msgstr ""
msgid "Function {0} is not whitelisted."
msgstr ""
-#: frappe/database/query.py:2253
+#: frappe/database/query.py:2256
msgid "Function {0} requires arguments but none were provided"
msgstr ""
@@ -11524,7 +11556,7 @@ msgstr ""
msgid "Further sub-groups can only be created under records marked as 'Group'"
msgstr ""
-#: frappe/core/doctype/communication/communication.js:291
+#: frappe/core/doctype/communication/communication.js:294
msgid "Fw: {0}"
msgstr ""
@@ -11581,11 +11613,11 @@ msgstr ""
msgid "Generate Keys"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:908
+#: frappe/public/js/frappe/views/reports/query_report.js:909
msgid "Generate New Report"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:449
+#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:432
msgid "Generate Random Password"
msgstr ""
@@ -11595,8 +11627,8 @@ msgstr ""
msgid "Generate Separate Documents For Each Assignee"
msgstr ""
-#: frappe/public/js/frappe/ui/sidebar/sidebar.js:531
-#: frappe/public/js/frappe/utils/utils.js:2075
+#: frappe/public/js/frappe/ui/sidebar/sidebar.js:525
+#: frappe/public/js/frappe/utils/utils.js:2090
msgid "Generate Tracking URL"
msgstr ""
@@ -11640,7 +11672,7 @@ msgstr ""
msgid "Get Header and Footer wkhtmltopdf variables"
msgstr ""
-#: frappe/public/js/frappe/form/multi_select_dialog.js:86
+#: frappe/public/js/frappe/form/multi_select_dialog.js:87
msgid "Get Items"
msgstr ""
@@ -11694,7 +11726,7 @@ msgstr ""
msgid "Global Search DocType"
msgstr ""
-#: frappe/desk/doctype/global_search_settings/global_search_settings.js:24
+#: frappe/desk/doctype/global_search_settings/global_search_settings.js:99
msgid "Global Search Document Types Reset."
msgstr ""
@@ -11703,7 +11735,7 @@ msgstr ""
msgid "Global Search Settings"
msgstr ""
-#: frappe/public/js/frappe/ui/keyboard.js:122
+#: frappe/public/js/frappe/ui/keyboard.js:136
msgid "Global Shortcuts"
msgstr ""
@@ -11712,7 +11744,7 @@ msgstr ""
msgid "Global Unsubscribe"
msgstr ""
-#: frappe/public/js/frappe/form/toolbar.js:885
+#: frappe/public/js/frappe/form/toolbar.js:886
msgid "Go"
msgstr ""
@@ -11980,7 +12012,7 @@ msgstr ""
msgid "Grid Page Length"
msgstr ""
-#: frappe/public/js/frappe/ui/keyboard.js:127
+#: frappe/public/js/frappe/ui/keyboard.js:141
msgid "Grid Shortcuts"
msgstr ""
@@ -12009,11 +12041,11 @@ msgstr ""
msgid "Group By Type"
msgstr ""
-#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:408
+#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:410
msgid "Group By field is required to create a dashboard chart"
msgstr ""
-#: frappe/database/query.py:1309
+#: frappe/database/query.py:1311
msgid "Group By must be a string"
msgstr ""
@@ -12031,6 +12063,10 @@ msgstr ""
msgid "Grouped by {0}"
msgstr ""
+#: frappe/handler.py:141
+msgid "Guests are not allowed to upload files for {0} Doctype"
+msgstr ""
+
#. Description of the 'Policy' (Data) field in DocType 'Security Settings'
#: frappe/core/doctype/security_settings/security_settings.json
msgid "Guidelines and policies on vulnerability reporting. Defaults to `https://frappe.io/security`"
@@ -12275,7 +12311,7 @@ msgstr ""
#. Label of the help (HTML) field in DocType 'Property Setter'
#: frappe/core/doctype/server_script/server_script.json
#: frappe/custom/doctype/property_setter/property_setter.json
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:73
+#: frappe/public/js/frappe/form/templates/form_sidebar.html:72
#: frappe/public/js/frappe/form/workflow.js:23
#: frappe/public/js/frappe/utils/help.js:27
msgid "Help"
@@ -12330,7 +12366,7 @@ msgstr ""
msgid "Helvetica Neue"
msgstr ""
-#: frappe/public/js/frappe/utils/utils.js:2072
+#: frappe/public/js/frappe/utils/utils.js:2087
msgid "Here's your tracking URL"
msgstr ""
@@ -12366,7 +12402,7 @@ msgstr ""
msgid "Hidden Fields"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1774
+#: frappe/public/js/frappe/views/reports/query_report.js:1775
msgid "Hidden columns include: {0}"
msgstr ""
@@ -12620,10 +12656,10 @@ msgid "I guess you don't have access to any workspace yet, but you can create on
msgstr ""
#. Label of the id (Data) field in DocType 'User Session Display'
-#: frappe/core/doctype/data_import/importer.py:1179
#: frappe/core/doctype/data_import/importer.py:1185
-#: frappe/core/doctype/data_import/importer.py:1250
-#: frappe/core/doctype/data_import/importer.py:1253
+#: frappe/core/doctype/data_import/importer.py:1191
+#: frappe/core/doctype/data_import/importer.py:1256
+#: frappe/core/doctype/data_import/importer.py:1259
#: frappe/core/doctype/user_session_display/user_session_display.json
#: frappe/desk/report/todo/todo.py:36 frappe/model/meta.py:52
#: frappe/public/js/frappe/data_import/data_exporter.js:371
@@ -12631,13 +12667,13 @@ msgstr ""
#: frappe/public/js/frappe/list/list_settings.js:340
#: frappe/public/js/frappe/list/list_view.js:427
#: frappe/public/js/frappe/list/list_view.js:491
-#: frappe/public/js/frappe/list/list_view.js:2488
+#: frappe/public/js/frappe/list/list_view.js:2482
#: frappe/public/js/frappe/model/meta.js:208
#: frappe/public/js/frappe/model/model.js:122
msgid "ID"
msgstr ""
-#: frappe/desk/reportview.py:563
+#: frappe/desk/reportview.py:570
#: frappe/public/js/frappe/views/reports/report_view.js:1079
msgctxt "Label of name column in report"
msgid "ID"
@@ -12737,7 +12773,7 @@ msgstr ""
msgid "Icon Type"
msgstr ""
-#: frappe/desk/page/desktop/desktop.js:1058
+#: frappe/desk/page/desktop/desktop.js:1048
msgid "Icon is not correctly configured please check the workspace sidebar to it"
msgstr ""
@@ -12782,6 +12818,12 @@ msgstr ""
msgid "If a Role does not have access at Level 0, then higher levels are meaningless."
msgstr ""
+#. Description of the 'Disable Prepared Report Automation' (Check) field in
+#. DocType 'Report'
+#: frappe/core/doctype/report/report.json
+msgid "If checked, Prepared Report will not be enabled automatically on slow runs."
+msgstr ""
+
#. Description of the 'Enable Action Confirmation' (Check) field in DocType
#. 'Workflow'
#: frappe/workflow/doctype/workflow/workflow.json
@@ -12922,7 +12964,7 @@ msgstr ""
msgid "If these instructions where not helpful, please add in your suggestions on GitHub Issues."
msgstr ""
-#: frappe/core/doctype/user/user.py:1185
+#: frappe/core/doctype/user/user.py:1189
msgid "If this email is registered with us, we have sent password reset instructions to it. Please check your inbox."
msgstr ""
@@ -13088,11 +13130,11 @@ msgstr ""
msgid "Image Width (px)"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1541
+#: frappe/core/doctype/doctype/doctype.py:1542
msgid "Image field must be a valid fieldname"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1543
+#: frappe/core/doctype/doctype/doctype.py:1544
msgid "Image field must be of type Attach Image"
msgstr ""
@@ -13328,6 +13370,22 @@ msgstr ""
msgid "In seconds"
msgstr ""
+#: frappe/public/js/frappe/form/save.js:223
+msgid "In {0}, {1} is required in every row."
+msgstr ""
+
+#: frappe/public/js/frappe/form/save.js:229
+msgid "In {0}, {1} is required in row {2}."
+msgstr ""
+
+#: frappe/public/js/frappe/form/save.js:238
+msgid "In {0}, {1} is required in rows {2}."
+msgstr ""
+
+#: frappe/public/js/frappe/form/save.js:245
+msgid "In {0}, {1} is required in {2} rows."
+msgstr ""
+
#: frappe/core/doctype/recorder/recorder_list.js:209
msgid "Inactive"
msgstr ""
@@ -13372,15 +13430,15 @@ msgid "Include Web View Link in Email"
msgstr ""
#: frappe/public/js/frappe/form/print_utils.js:60
-#: frappe/public/js/frappe/views/reports/query_report.js:1748
+#: frappe/public/js/frappe/views/reports/query_report.js:1749
msgid "Include filters"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1770
+#: frappe/public/js/frappe/views/reports/query_report.js:1771
msgid "Include hidden columns"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1740
+#: frappe/public/js/frappe/views/reports/query_report.js:1741
msgid "Include indentation"
msgstr ""
@@ -13550,7 +13608,7 @@ msgstr ""
#. Label of the insert_after (Select) field in DocType 'Custom Field'
#: frappe/custom/doctype/custom_field/custom_field.json
-#: frappe/public/js/frappe/views/reports/query_report.js:2031
+#: frappe/public/js/frappe/views/reports/query_report.js:2032
msgid "Insert After"
msgstr ""
@@ -13585,12 +13643,8 @@ msgstr ""
msgid "Insert Style"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/about.js:60
-msgid "Instagram"
-msgstr ""
-
-#: frappe/public/js/frappe/ui/toolbar/search_utils.js:690
-#: frappe/public/js/frappe/ui/toolbar/search_utils.js:691
+#: frappe/public/js/frappe/ui/toolbar/search_utils.js:685
+#: frappe/public/js/frappe/ui/toolbar/search_utils.js:686
msgid "Install {0} from Marketplace"
msgstr ""
@@ -13607,7 +13661,7 @@ msgid "Installed Applications"
msgstr ""
#: frappe/core/doctype/installed_applications/installed_applications.js:18
-#: frappe/public/js/frappe/ui/toolbar/about.js:67
+#: frappe/public/js/frappe/ui/toolbar/about.js:58
msgid "Installed Apps"
msgstr ""
@@ -13624,11 +13678,11 @@ msgstr ""
msgid "Insufficient Permission Level for {0}"
msgstr ""
-#: frappe/database/query.py:1368
+#: frappe/database/query.py:1371
msgid "Insufficient Permission for {0}"
msgstr ""
-#: frappe/desk/reportview.py:363
+#: frappe/desk/reportview.py:370
msgid "Insufficient Permissions for deleting Report"
msgstr ""
@@ -13636,7 +13690,7 @@ msgstr ""
msgid "Insufficient Permissions for editing Report"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:447
+#: frappe/core/doctype/doctype/doctype.py:448
msgid "Insufficient attachment limit"
msgstr ""
@@ -13748,7 +13802,7 @@ msgid "Invalid"
msgstr ""
#: frappe/public/js/form_builder/utils.js:218
-#: frappe/public/js/frappe/form/grid_row.js:840
+#: frappe/public/js/frappe/form/grid_row.js:866
#: frappe/public/js/frappe/form/layout.js:806
#: frappe/public/js/frappe/views/reports/report_view.js:811
msgid "Invalid \"depends_on\" expression"
@@ -13758,7 +13812,7 @@ msgstr ""
msgid "Invalid \"depends_on\" expression set in filter {0}"
msgstr ""
-#: frappe/public/js/frappe/form/save.js:214
+#: frappe/public/js/frappe/form/save.js:282
msgid "Invalid \"mandatory_depends_on\" expression"
msgstr ""
@@ -13794,7 +13848,7 @@ msgstr ""
msgid "Invalid DocType"
msgstr ""
-#: frappe/query_builder/builder.py:59
+#: frappe/query_builder/builder.py:71
msgid "Invalid DocType: {0}"
msgstr ""
@@ -13802,17 +13856,17 @@ msgstr ""
msgid "Invalid Doctype"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1298
-#: frappe/core/doctype/doctype/doctype.py:1307
+#: frappe/core/doctype/doctype/doctype.py:1299
+#: frappe/core/doctype/doctype/doctype.py:1308
msgid "Invalid Fieldname"
msgstr ""
-#: frappe/core/doctype/file/file.py:295
+#: frappe/core/doctype/file/file.py:298
msgid "Invalid File URL"
msgstr ""
-#: frappe/database/query.py:821 frappe/database/query.py:848
-#: frappe/database/query.py:858
+#: frappe/database/query.py:823 frappe/database/query.py:850
+#: frappe/database/query.py:860
msgid "Invalid Filter"
msgstr ""
@@ -13855,8 +13909,8 @@ msgstr ""
msgid "Invalid Operation"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1676
-#: frappe/core/doctype/doctype/doctype.py:1684
+#: frappe/core/doctype/doctype/doctype.py:1677
+#: frappe/core/doctype/doctype/doctype.py:1685
msgid "Invalid Option"
msgstr ""
@@ -13894,7 +13948,7 @@ msgstr ""
msgid "Invalid Search Field {0}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1238
+#: frappe/core/doctype/doctype/doctype.py:1239
msgid "Invalid Table Fieldname"
msgstr ""
@@ -13902,7 +13956,7 @@ msgstr ""
msgid "Invalid Transition"
msgstr ""
-#: frappe/core/doctype/file/file.py:306
+#: frappe/core/doctype/file/file.py:309
#: frappe/public/js/frappe/file_uploader/FileUploader.vue:551
#: frappe/public/js/frappe/widgets/widget_dialog.js:602
#: frappe/utils/csvutils.py:226 frappe/utils/csvutils.py:247
@@ -13925,7 +13979,7 @@ msgstr ""
msgid "Invalid aggregate function"
msgstr ""
-#: frappe/database/query.py:2414
+#: frappe/database/query.py:2417
msgid "Invalid alias format: {0}. Alias must be a simple identifier."
msgstr ""
@@ -13933,15 +13987,19 @@ msgstr ""
msgid "Invalid app"
msgstr ""
-#: frappe/database/query.py:2374 frappe/database/query.py:2390
+#: frappe/database/query.py:2377 frappe/database/query.py:2393
msgid "Invalid argument format: {0}. Only quoted string literals or simple field names are allowed."
msgstr ""
-#: frappe/database/query.py:2338
+#: frappe/database/query.py:2341
msgid "Invalid argument type: {0}. Only strings, numbers, dicts, and None are allowed."
msgstr ""
-#: frappe/database/query.py:854
+#: frappe/utils/response.py:289
+msgid "Invalid backup path"
+msgstr ""
+
+#: frappe/database/query.py:856
msgid "Invalid characters in fieldname: {0}. Only letters, numbers, and underscores are allowed."
msgstr ""
@@ -13949,11 +14007,11 @@ msgstr ""
msgid "Invalid column"
msgstr ""
-#: frappe/database/query.py:755
+#: frappe/database/query.py:757
msgid "Invalid condition type in nested filters: {0}"
msgstr ""
-#: frappe/database/query.py:1353
+#: frappe/database/query.py:1355
msgid "Invalid direction in Order By: {0}. Must be 'ASC' or 'DESC'."
msgstr ""
@@ -13965,31 +14023,31 @@ msgstr ""
msgid "Invalid expression in Workflow Update Value: {0}"
msgstr ""
-#: frappe/public/js/frappe/utils/dashboard_utils.js:229
+#: frappe/public/js/frappe/utils/dashboard_utils.js:223
msgid "Invalid expression set in filter {0}"
msgstr ""
-#: frappe/public/js/frappe/utils/dashboard_utils.js:219
+#: frappe/public/js/frappe/utils/dashboard_utils.js:234
msgid "Invalid expression set in filter {0} ({1})"
msgstr ""
-#: frappe/database/query.py:2141
+#: frappe/database/query.py:2144
msgid "Invalid field format for SELECT: {0}. Field names must be simple, backticked, table-qualified, aliased, or '*'."
msgstr ""
-#: frappe/database/query.py:1294
+#: frappe/database/query.py:1296
msgid "Invalid field format in {0}: {1}. Use 'field', 'link_field.field', or 'child_table.field'."
msgstr ""
-#: frappe/utils/data.py:2310
+#: frappe/utils/data.py:2314
msgid "Invalid field name {0}"
msgstr ""
-#: frappe/database/query.py:1180
+#: frappe/database/query.py:1182
msgid "Invalid field type: {0}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1109
+#: frappe/core/doctype/doctype/doctype.py:1110
msgid "Invalid fieldname '{0}' in autoname"
msgstr ""
@@ -13997,11 +14055,11 @@ msgstr ""
msgid "Invalid file path: {0}"
msgstr ""
-#: frappe/database/query.py:738
+#: frappe/database/query.py:740
msgid "Invalid filter condition: {0}. Expected a list or tuple."
msgstr ""
-#: frappe/database/query.py:844
+#: frappe/database/query.py:846
msgid "Invalid filter field format: {0}. Use 'fieldname' or 'link_fieldname.target_fieldname'."
msgstr ""
@@ -14009,7 +14067,7 @@ msgstr ""
msgid "Invalid filter: {0}"
msgstr ""
-#: frappe/database/query.py:2258
+#: frappe/database/query.py:2261
msgid "Invalid function argument type: {0}. Only strings, numbers, lists, and None are allowed."
msgstr ""
@@ -14018,7 +14076,7 @@ msgid "Invalid input"
msgstr ""
#: frappe/desk/doctype/dashboard/dashboard.py:67
-#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:424
+#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:426
msgid "Invalid json added in the custom options: {0}"
msgstr ""
@@ -14034,7 +14092,7 @@ msgstr ""
msgid "Invalid naming series {}: dot (.) missing before the numeric placeholders. Kindly use a format like ABCD.##### ."
msgstr ""
-#: frappe/database/query.py:2330
+#: frappe/database/query.py:2333
msgid "Invalid nested expression: dictionary must represent a function or operator"
msgstr ""
@@ -14062,11 +14120,11 @@ msgstr ""
msgid "Invalid role"
msgstr ""
-#: frappe/database/query.py:795
+#: frappe/database/query.py:797
msgid "Invalid simple filter format: {0}"
msgstr ""
-#: frappe/database/query.py:715
+#: frappe/database/query.py:717
msgid "Invalid start for filter condition: {0}. Expected a list or tuple."
msgstr ""
@@ -14096,11 +14154,11 @@ msgstr ""
msgid "Invalid wkhtmltopdf version"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1599
+#: frappe/core/doctype/doctype/doctype.py:1600
msgid "Invalid {0} condition"
msgstr ""
-#: frappe/database/query.py:2219
+#: frappe/database/query.py:2222
msgid "Invalid {0} dictionary format"
msgstr ""
@@ -14300,7 +14358,7 @@ msgstr ""
msgid "Is Published Field"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1550
+#: frappe/core/doctype/doctype/doctype.py:1551
msgid "Is Published Field must be a valid fieldname"
msgstr ""
@@ -14545,12 +14603,12 @@ msgstr ""
msgid "Job stopped successfully"
msgstr ""
-#: frappe/desk/doctype/event/event.js:55
+#: frappe/desk/doctype/event/event.js:53
msgid "Join video conference with {0}"
msgstr ""
#: frappe/public/js/frappe/form/toolbar.js:421
-#: frappe/public/js/frappe/form/toolbar.js:875
+#: frappe/public/js/frappe/form/toolbar.js:876
msgid "Jump to field"
msgstr ""
@@ -14583,11 +14641,11 @@ msgstr ""
#. Label of the kanban_board_name (Data) field in DocType 'Kanban Board'
#: frappe/desk/doctype/kanban_board/kanban_board.json
-#: frappe/public/js/frappe/views/kanban/kanban_view.js:403
+#: frappe/public/js/frappe/views/kanban/kanban_view.js:425
msgid "Kanban Board Name"
msgstr ""
-#: frappe/public/js/frappe/views/kanban/kanban_view.js:280
+#: frappe/public/js/frappe/views/kanban/kanban_view.js:302
msgctxt "Button in kanban view menu"
msgid "Kanban Settings"
msgstr ""
@@ -14630,7 +14688,7 @@ msgstr ""
#. Label of a standard help item
#. Type: Action
-#: frappe/hooks.py frappe/public/js/frappe/ui/keyboard.js:130
+#: frappe/hooks.py frappe/public/js/frappe/ui/keyboard.js:144
msgid "Keyboard Shortcuts"
msgstr ""
@@ -14951,11 +15009,11 @@ msgid "Last Active"
msgstr ""
#: frappe/public/js/frappe/form/sidebar/form_sidebar.js:163
-msgid "Last Edited by You"
+msgid "Last Edited By You"
msgstr ""
#: frappe/public/js/frappe/form/sidebar/form_sidebar.js:164
-msgid "Last Edited by {0}"
+msgid "Last Edited By {0}"
msgstr ""
#. Label of the last_execution (Datetime) field in DocType 'Scheduled Job Type'
@@ -15314,7 +15372,7 @@ msgstr ""
msgid "Like"
msgstr ""
-#: frappe/desk/like.py:92
+#: frappe/desk/like.py:94
msgid "Liked"
msgstr ""
@@ -15519,10 +15577,6 @@ msgstr ""
msgid "Linked with {0}"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/about.js:40
-msgid "LinkedIn"
-msgstr ""
-
#. Label of the links (Table) field in DocType 'Address'
#. Label of the links (Table) field in DocType 'Contact'
#. Label of the links_section (Tab Break) field in DocType 'DocType'
@@ -15541,7 +15595,7 @@ msgstr ""
#: frappe/desk/doctype/sidebar_item_group/sidebar_item_group.json
#: frappe/desk/doctype/workspace/workspace.json
#: frappe/public/js/frappe/form/linked_with.js:23
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:81
+#: frappe/public/js/frappe/form/templates/form_sidebar.html:80
msgid "Links"
msgstr ""
@@ -15583,7 +15637,7 @@ msgstr ""
msgid "List Settings"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2126
+#: frappe/public/js/frappe/list/list_view.js:2127
msgctxt "Button in list view menu"
msgid "List Settings"
msgstr ""
@@ -15597,10 +15651,6 @@ msgstr ""
msgid "List View Settings"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:229
-msgid "List a document type"
-msgstr ""
-
#. Description of the 'Breadcrumbs' (Code) field in DocType 'Web Form'
#. Description of the 'Breadcrumbs' (Code) field in DocType 'Web Page'
#: frappe/website/doctype/web_form/web_form.json
@@ -15624,10 +15674,6 @@ msgstr ""
msgid "List setting message"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/search_utils.js:563
-msgid "Lists"
-msgstr ""
-
#. Option for the 'Rule' (Select) field in DocType 'Assignment Rule'
#: frappe/automation/doctype/assignment_rule/assignment_rule.json
msgid "Load Balancing"
@@ -15654,7 +15700,7 @@ msgstr ""
#: frappe/public/js/frappe/list/base_list.js:509
#: frappe/public/js/frappe/list/list_view.js:405
#: frappe/public/js/frappe/ui/listing.html:16
-#: frappe/public/js/frappe/views/reports/query_report.js:1153
+#: frappe/public/js/frappe/views/reports/query_report.js:1154
msgid "Loading"
msgstr ""
@@ -15666,14 +15712,11 @@ msgstr ""
msgid "Loading import file..."
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/about.js:75
-msgid "Loading versions..."
-msgstr ""
-
#: frappe/public/js/frappe/file_uploader/TreeNode.vue:45
#: frappe/public/js/frappe/form/sidebar/share.js:62
#: frappe/public/js/frappe/list/base_list.js:1066
#: frappe/public/js/frappe/list/list_sidebar_group_by.js:93
+#: frappe/public/js/frappe/ui/toolbar/about.js:37
#: frappe/public/js/frappe/views/kanban/kanban_board.html:11
#: frappe/public/js/frappe/widgets/chart_widget.js:52
#: frappe/public/js/frappe/widgets/number_card_widget.js:189
@@ -15937,7 +15980,7 @@ msgstr ""
msgid "Looks like you haven’t added any third party apps."
msgstr ""
-#: frappe/public/js/frappe/ui/notifications/notifications.js:356
+#: frappe/public/js/frappe/ui/notifications/notifications.js:358
msgid "Looks like you haven’t received any notifications."
msgstr ""
@@ -16023,7 +16066,7 @@ msgstr ""
msgid "Make use of longer keyboard patterns"
msgstr ""
-#: frappe/public/js/frappe/form/multi_select_dialog.js:87
+#: frappe/public/js/frappe/form/multi_select_dialog.js:88
msgid "Make {0}"
msgstr ""
@@ -16039,7 +16082,7 @@ msgstr ""
msgid "Manage 3rd party apps"
msgstr ""
-#: frappe/public/js/billing.bundle.js:77
+#: frappe/public/js/billing.bundle.js:78
msgid "Manage Billing"
msgstr ""
@@ -16071,7 +16114,7 @@ msgstr ""
msgid "Mandatory Depends On (JS)"
msgstr ""
-#: frappe/website/doctype/web_form/web_form.py:552
+#: frappe/website/doctype/web_form/web_form.py:548
msgid "Mandatory Information missing:"
msgstr ""
@@ -16083,14 +16126,6 @@ msgstr ""
msgid "Mandatory field: {0}"
msgstr ""
-#: frappe/public/js/frappe/form/save.js:181
-msgid "Mandatory fields required in table {0}, Row {1}"
-msgstr ""
-
-#: frappe/public/js/frappe/form/save.js:186
-msgid "Mandatory fields required in {0}"
-msgstr ""
-
#: frappe/public/js/frappe/web_form/web_form.js:256
msgctxt "Error message in web form"
msgid "Mandatory fields required:"
@@ -16105,8 +16140,8 @@ msgstr ""
msgid "Map"
msgstr ""
-#: frappe/public/js/frappe/data_import/import_preview.js:194
-#: frappe/public/js/frappe/data_import/import_preview.js:306
+#: frappe/public/js/frappe/data_import/import_preview.js:196
+#: frappe/public/js/frappe/data_import/import_preview.js:308
msgid "Map Columns"
msgstr ""
@@ -16114,7 +16149,7 @@ msgstr ""
msgid "Map View"
msgstr ""
-#: frappe/public/js/frappe/data_import/import_preview.js:296
+#: frappe/public/js/frappe/data_import/import_preview.js:298
msgid "Map columns from {0} to fields in {1}"
msgstr ""
@@ -16159,7 +16194,7 @@ msgstr ""
#: frappe/core/doctype/communication/communication.js:78
#: frappe/core/doctype/communication/communication_list.js:19
-#: frappe/public/js/frappe/ui/notifications/notifications.js:311
+#: frappe/public/js/frappe/ui/notifications/notifications.js:313
msgid "Mark as Read"
msgstr ""
@@ -16273,7 +16308,7 @@ msgstr ""
msgid "Max signups allowed per hour"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1377
+#: frappe/core/doctype/doctype/doctype.py:1378
msgid "Max width for type Currency is 100px in row {0}"
msgstr ""
@@ -16282,7 +16317,7 @@ msgstr ""
msgid "Maximum"
msgstr ""
-#: frappe/core/doctype/file/file.py:406
+#: frappe/core/doctype/file/file.py:409
msgid "Maximum Attachment Limit of {0} has been reached for {1} {2}."
msgstr ""
@@ -16290,7 +16325,7 @@ msgstr ""
msgid "Maximum attachment limit of {0} has been reached."
msgstr ""
-#: frappe/model/rename_doc.py:692
+#: frappe/model/rename_doc.py:681
msgid "Maximum {0} rows allowed"
msgstr ""
@@ -16314,7 +16349,7 @@ msgstr ""
#. Label of the medium (Data) field in DocType 'Web Page View'
#: frappe/desk/doctype/todo/todo.json
#: frappe/public/js/frappe/form/sidebar/assign_to.js:232
-#: frappe/public/js/frappe/utils/utils.js:2022
+#: frappe/public/js/frappe/utils/utils.js:2037
#: frappe/website/doctype/web_page_view/web_page_view.json
#: frappe/website/report/website_analytics/website_analytics.js:40
msgid "Medium"
@@ -16388,7 +16423,7 @@ msgstr ""
#: frappe/automation/doctype/auto_repeat/auto_repeat.json
#: frappe/core/doctype/activity_log/activity_log.json
#: frappe/core/doctype/communication/communication.json
-#: frappe/core/doctype/data_import/data_import.js:514
+#: frappe/core/doctype/data_import/data_import.js:516
#: frappe/core/doctype/sms_log/sms_log.json
#: frappe/core/doctype/success_action/success_action.json
#: frappe/desk/doctype/notification_log/notification_log.json
@@ -16622,11 +16657,11 @@ msgstr ""
msgid "Missing DocType"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1561
+#: frappe/core/doctype/doctype/doctype.py:1562
msgid "Missing Field"
msgstr ""
-#: frappe/public/js/frappe/form/save.js:192
+#: frappe/public/js/frappe/form/save.js:261
msgid "Missing Fields"
msgstr ""
@@ -16634,7 +16669,7 @@ msgstr ""
msgid "Missing Filters Required"
msgstr ""
-#: frappe/desk/form/assign_to.py:110
+#: frappe/desk/form/assign_to.py:114
msgid "Missing Permission"
msgstr ""
@@ -16849,10 +16884,12 @@ msgid "Monthly Long"
msgstr ""
#: frappe/public/js/frappe/form/link_selector.js:38
-#: frappe/public/js/frappe/form/multi_select_dialog.js:45
-#: frappe/public/js/frappe/form/multi_select_dialog.js:72
-#: frappe/public/js/frappe/ui/toolbar/search.js:287
-#: frappe/public/js/frappe/ui/toolbar/search.js:301
+#: frappe/public/js/frappe/form/multi_select_dialog.js:46
+#: frappe/public/js/frappe/form/multi_select_dialog.js:73
+#: frappe/public/js/frappe/ui/toolbar/search.js:745
+#: frappe/public/js/frappe/ui/toolbar/search.js:925
+#: frappe/public/js/frappe/ui/toolbar/search.js:941
+#: frappe/public/js/frappe/ui/toolbar/search.js:991
#: frappe/public/js/frappe/widgets/chart_widget.js:765
#: frappe/templates/includes/list/list.html:25
#: frappe/templates/includes/search_template.html:13
@@ -16902,8 +16939,8 @@ msgid "Most probably your password is too long."
msgstr ""
#: frappe/core/doctype/communication/communication.js:86
-#: frappe/core/doctype/communication/communication.js:194
-#: frappe/core/doctype/communication/communication.js:212
+#: frappe/core/doctype/communication/communication.js:197
+#: frappe/core/doctype/communication/communication.js:215
#: frappe/public/js/frappe/form/grid_row_form.js:53
msgid "Move"
msgstr ""
@@ -16990,7 +17027,7 @@ msgstr ""
msgid "Must be of type \"Attach Image\""
msgstr ""
-#: frappe/desk/query_report.py:218
+#: frappe/desk/query_report.py:223
msgid "Must have report permission to access this report."
msgstr ""
@@ -17008,7 +17045,7 @@ msgid "Mx"
msgstr ""
#: frappe/templates/includes/web_sidebar.html:41
-#: frappe/website/doctype/web_form/web_form.py:541
+#: frappe/website/doctype/web_form/web_form.py:537
#: frappe/website/doctype/website_settings/website_settings.py:181
#: frappe/www/me.html:8 frappe/www/update_password.py:10
msgid "My Account"
@@ -17063,8 +17100,9 @@ msgstr ""
#: frappe/email/doctype/reply_to_address/reply_to_address.json
#: frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.json
#: frappe/public/js/frappe/form/layout.js:76
-#: frappe/public/js/frappe/form/multi_select_dialog.js:240
-#: frappe/public/js/frappe/form/save.js:168
+#: frappe/public/js/frappe/form/multi_select_dialog.js:241
+#: frappe/public/js/frappe/form/save.js:170
+#: frappe/public/js/frappe/ui/toolbar/search.js:775
#: frappe/public/js/frappe/views/file/file_view.js:103
#: frappe/website/doctype/website_slideshow/website_slideshow.js:25
msgid "Name"
@@ -17196,15 +17234,19 @@ msgstr ""
msgid "Need Help?"
msgstr ""
-#: frappe/desk/doctype/workspace/workspace.py:360
-msgid "Need Workspace Manager role to edit private workspace of other users"
+#: frappe/desk/doctype/workspace/workspace.py:365
+msgid "Need Workspace Manager role to edit private workspace of other users."
+msgstr ""
+
+#: frappe/desk/doctype/workspace/workspace.py:362
+msgid "Need Workspace Manager role to edit public workspaces."
msgstr ""
#: frappe/model/document.py:839
msgid "Negative Value"
msgstr ""
-#: frappe/database/query.py:707
+#: frappe/database/query.py:709
msgid "Nested filters must be provided as a list or tuple."
msgstr ""
@@ -17243,7 +17285,7 @@ msgstr ""
#: frappe/public/js/frappe/form/templates/address_list.html:3
#: frappe/public/js/frappe/ui/address_autocomplete/autocomplete_dialog.js:5
-#: frappe/public/js/frappe/utils/address_and_contact.js:87
+#: frappe/public/js/frappe/utils/address_and_contact.js:91
msgid "New Address"
msgstr ""
@@ -17291,7 +17333,7 @@ msgstr ""
msgid "New Folder"
msgstr ""
-#: frappe/public/js/frappe/views/kanban/kanban_view.js:359
+#: frappe/public/js/frappe/views/kanban/kanban_view.js:381
msgid "New Kanban Board"
msgstr ""
@@ -17395,7 +17437,7 @@ msgstr ""
msgid "New password cannot be same as old password"
msgstr ""
-#: frappe/utils/change_log.py:389
+#: frappe/utils/change_log.py:401
msgid "New updates are available"
msgstr ""
@@ -17424,7 +17466,7 @@ msgstr ""
#: frappe/public/js/frappe/views/breadcrumbs.js:232
#: frappe/public/js/frappe/views/treeview.js:375
#: frappe/public/js/frappe/widgets/widget_dialog.js:72
-#: frappe/website/doctype/web_form/web_form.py:454
+#: frappe/website/doctype/web_form/web_form.py:455
msgid "New {0}"
msgstr ""
@@ -17444,11 +17486,11 @@ msgstr ""
msgid "New {0}: {1}"
msgstr ""
-#: frappe/utils/change_log.py:375
+#: frappe/utils/change_log.py:387
msgid "New {} releases for the following apps are available"
msgstr ""
-#: frappe/core/doctype/user/user.py:886
+#: frappe/core/doctype/user/user.py:890
msgid "Newly created user {0} has no roles enabled."
msgstr ""
@@ -17620,8 +17662,8 @@ msgstr ""
#: frappe/core/doctype/data_export/exporter.py:162
#: frappe/email/doctype/auto_email_report/auto_email_report.py:311
#: frappe/public/js/form_builder/components/controls/TableControl.vue:64
-#: frappe/public/js/frappe/data_import/import_preview.js:146
-#: frappe/public/js/frappe/form/multi_select_dialog.js:224
+#: frappe/public/js/frappe/data_import/import_preview.js:148
+#: frappe/public/js/frappe/form/multi_select_dialog.js:225
#: frappe/public/js/frappe/utils/datatable.js:10
#: frappe/public/js/frappe/widgets/chart_widget.js:59
msgid "No Data"
@@ -17694,7 +17736,7 @@ msgstr ""
msgid "No Name Specified for {0}"
msgstr ""
-#: frappe/public/js/frappe/ui/notifications/notifications.js:354
+#: frappe/public/js/frappe/ui/notifications/notifications.js:356
msgid "No New notifications"
msgstr ""
@@ -17734,15 +17776,15 @@ msgstr ""
msgid "No Results"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/search.js:51
+#: frappe/public/js/frappe/ui/toolbar/search.js:98
msgid "No Results found"
msgstr ""
-#: frappe/core/doctype/user/user.py:887
+#: frappe/core/doctype/user/user.py:891
msgid "No Roles Specified"
msgstr ""
-#: frappe/public/js/frappe/views/kanban/kanban_view.js:359
+#: frappe/public/js/frappe/views/kanban/kanban_view.js:381
msgid "No Select Field Found"
msgstr ""
@@ -17750,11 +17792,11 @@ msgstr ""
msgid "No Suggestions"
msgstr ""
-#: frappe/desk/reportview.py:760 frappe/public/js/frappe/list/base_list.js:1051
+#: frappe/desk/reportview.py:767 frappe/public/js/frappe/list/base_list.js:1051
msgid "No Tags"
msgstr ""
-#: frappe/public/js/frappe/ui/notifications/notifications.js:482
+#: frappe/public/js/frappe/ui/notifications/notifications.js:484
msgid "No Upcoming Events"
msgstr ""
@@ -17778,11 +17820,11 @@ msgstr ""
msgid "No changes in document"
msgstr ""
-#: frappe/public/js/frappe/views/workspace/workspace.js:762
+#: frappe/public/js/frappe/views/workspace/workspace.js:763
msgid "No changes made"
msgstr ""
-#: frappe/model/rename_doc.py:369
+#: frappe/model/rename_doc.py:358
msgid "No changes made because old and new name are the same."
msgstr ""
@@ -17810,11 +17852,11 @@ msgstr ""
msgid "No currency fields in {0}"
msgstr ""
-#: frappe/desk/query_report.py:400
+#: frappe/desk/query_report.py:405
msgid "No data to export"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1556
+#: frappe/public/js/frappe/views/reports/query_report.js:1557
msgid "No data to perform this action"
msgstr ""
@@ -17822,10 +17864,14 @@ msgstr ""
msgid "No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template."
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/search.js:71
+#: frappe/public/js/frappe/ui/toolbar/search.js:109
msgid "No documents found tagged with {0}"
msgstr ""
+#: frappe/public/js/frappe/form/grid.js:1086
+msgid "No editable fields available for bulk edit."
+msgstr ""
+
#: frappe/public/js/frappe/views/inbox/inbox_view.js:21
msgid "No email account associated with the User. Please add an account under User > Email Inbox."
msgstr ""
@@ -17834,11 +17880,11 @@ msgstr ""
msgid "No email addresses to invite"
msgstr ""
-#: frappe/core/doctype/data_import/data_import.js:505
+#: frappe/core/doctype/data_import/data_import.js:507
msgid "No failed logs"
msgstr ""
-#: frappe/public/js/frappe/views/kanban/kanban_view.js:389
+#: frappe/public/js/frappe/views/kanban/kanban_view.js:411
msgid "No fields found that can be used as a Kanban Column. Use the Customize Form to add a Custom Field of type \"Select\"."
msgstr ""
@@ -17855,7 +17901,7 @@ msgstr ""
msgid "No filters selected"
msgstr ""
-#: frappe/desk/form/utils.py:122
+#: frappe/desk/form/utils.py:129
msgid "No further records"
msgstr ""
@@ -17898,6 +17944,10 @@ msgstr ""
msgid "No of Sent SMS"
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/search.js:691
+msgid "No other document types."
+msgstr ""
+
#: frappe/__init__.py:625 frappe/client.py:119 frappe/client.py:161
msgid "No permission for {0}"
msgstr ""
@@ -17911,10 +17961,14 @@ msgstr ""
msgid "No permission to read {0}"
msgstr ""
-#: frappe/share.py:221
+#: frappe/share.py:223
msgid "No permission to {0} {1} {2}"
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/search.js:679
+msgid "No pinned document types."
+msgstr ""
+
#: frappe/core/doctype/user_permission/user_permission_list.js:175
msgid "No records deleted"
msgstr ""
@@ -17935,7 +17989,8 @@ msgstr ""
msgid "No rows"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2455
+#: frappe/public/js/frappe/form/grid.js:1063
+#: frappe/public/js/frappe/list/list_view.js:2449
msgid "No rows selected"
msgstr ""
@@ -17951,7 +18006,7 @@ msgstr ""
msgid "No user has the role {0} "
msgstr ""
-#: frappe/public/js/frappe/form/controls/multiselect_list.js:277
+#: frappe/public/js/frappe/form/controls/multiselect_list.js:301
#: frappe/public/js/frappe/utils/utils.js:990
msgid "No values to show"
msgstr ""
@@ -18016,7 +18071,7 @@ msgstr ""
msgid "Normalized Query"
msgstr ""
-#: frappe/core/doctype/user/user.py:1112 frappe/utils/oauth.py:301
+#: frappe/core/doctype/user/user.py:1116 frappe/utils/oauth.py:301
msgid "Not Allowed"
msgstr ""
@@ -18060,14 +18115,14 @@ msgstr ""
#: frappe/__init__.py:552 frappe/app.py:389 frappe/desk/calendar.py:28
#: frappe/public/js/frappe/web_form/webform_script.js:15
-#: frappe/website/doctype/web_form/web_form.py:794
+#: frappe/website/doctype/web_form/web_form.py:798
#: frappe/website/page_renderers/not_permitted_page.py:22
#: frappe/www/login.py:202 frappe/www/qrcode.py:22 frappe/www/qrcode.py:25
#: frappe/www/qrcode.py:37
msgid "Not Permitted"
msgstr ""
-#: frappe/desk/query_report.py:765
+#: frappe/desk/query_report.py:770
msgid "Not Permitted to read {0}"
msgstr ""
@@ -18077,9 +18132,9 @@ msgid "Not Published"
msgstr ""
#: frappe/public/js/frappe/form/toolbar.js:316
-#: frappe/public/js/frappe/form/toolbar.js:858
+#: frappe/public/js/frappe/form/toolbar.js:859
#: frappe/public/js/frappe/model/indicator.js:28
-#: frappe/public/js/frappe/views/kanban/kanban_view.js:184
+#: frappe/public/js/frappe/views/kanban/kanban_view.js:206
#: frappe/public/js/frappe/views/reports/report_view.js:215
#: frappe/public/js/print_format_builder/print_format_builder.bundle.js:39
#: frappe/website/doctype/web_form/templates/web_form.html:94
@@ -18111,7 +18166,7 @@ msgstr ""
msgid "Not a valid Comma Separated Value (CSV File)"
msgstr ""
-#: frappe/core/doctype/user/user.py:316
+#: frappe/core/doctype/user/user.py:320
msgid "Not a valid User Image."
msgstr ""
@@ -18131,7 +18186,7 @@ msgstr ""
msgid "Not allowed to attach {0} document, please enable Allow Print For {0} in Print Settings"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:337
+#: frappe/core/doctype/doctype/doctype.py:338
msgid "Not allowed to create custom Virtual DocType."
msgstr ""
@@ -18155,16 +18210,16 @@ msgstr ""
msgid "Not in Developer Mode"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:332
+#: frappe/core/doctype/doctype/doctype.py:333
msgid "Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."
msgstr ""
-#: frappe/core/doctype/system_settings/system_settings.py:235
+#: frappe/core/doctype/system_settings/system_settings.py:236
#: frappe/public/js/frappe/request.js:157
#: frappe/public/js/frappe/request.js:168
#: frappe/public/js/frappe/request.js:173
#: frappe/public/js/frappe/views/kanban/kanban_board.bundle.js:67
-#: frappe/utils/messages.py:173 frappe/website/doctype/web_form/web_form.py:807
+#: frappe/utils/messages.py:173 frappe/website/doctype/web_form/web_form.py:811
#: frappe/website/js/website.js:97
msgid "Not permitted"
msgstr ""
@@ -18224,7 +18279,7 @@ msgstr ""
msgid "Notes:"
msgstr ""
-#: frappe/public/js/frappe/ui/notifications/notifications.js:531
+#: frappe/public/js/frappe/ui/notifications/notifications.js:533
msgid "Nothing New"
msgstr ""
@@ -18255,7 +18310,7 @@ msgstr ""
#: frappe/core/doctype/communication/mixins.py:158
#: frappe/desk/doctype/event_notifications/event_notifications.json
#: frappe/email/doctype/notification/notification.json
-#: frappe/public/js/frappe/ui/sidebar/sidebar.js:497
+#: frappe/public/js/frappe/ui/sidebar/sidebar.js:491
#: frappe/workspace_sidebar/system.json
msgid "Notification"
msgstr ""
@@ -18283,7 +18338,7 @@ msgstr ""
msgid "Notification Subscribed Document"
msgstr ""
-#: frappe/public/js/frappe/form/templates/timeline_message_box.html:8
+#: frappe/public/js/frappe/form/templates/timeline_message_box.html:9
msgid "Notification sent to"
msgstr ""
@@ -18304,13 +18359,13 @@ msgstr ""
#. Label of the notifications (Table) field in DocType 'Event'
#. Label of a Workspace Sidebar Item
#: frappe/core/doctype/user/user.json frappe/desk/doctype/event/event.json
-#: frappe/public/js/frappe/ui/notifications/notifications.js:64
-#: frappe/public/js/frappe/ui/notifications/notifications.js:223
+#: frappe/public/js/frappe/ui/notifications/notifications.js:66
+#: frappe/public/js/frappe/ui/notifications/notifications.js:225
#: frappe/workspace_sidebar/system.json
msgid "Notifications"
msgstr ""
-#: frappe/public/js/frappe/ui/notifications/notifications.js:337
+#: frappe/public/js/frappe/ui/notifications/notifications.js:339
msgid "Notifications Disabled"
msgstr ""
@@ -18408,12 +18463,12 @@ msgstr ""
msgid "Number of Queries"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:444
+#: frappe/core/doctype/doctype/doctype.py:445
#: frappe/public/js/frappe/doctype/index.js:66
msgid "Number of attachment fields are more than {}, limit updated to {}."
msgstr ""
-#: frappe/core/doctype/system_settings/system_settings.py:190
+#: frappe/core/doctype/system_settings/system_settings.py:191
msgid "Number of backups must be greater than zero."
msgstr ""
@@ -18535,7 +18590,7 @@ msgstr ""
msgid "OTP SMS Template"
msgstr ""
-#: frappe/core/doctype/system_settings/system_settings.py:169
+#: frappe/core/doctype/system_settings/system_settings.py:170
msgid "OTP SMS Template must contain {0} placeholder to insert the OTP."
msgstr ""
@@ -18740,7 +18795,7 @@ msgstr ""
msgid "Only Administrator can edit"
msgstr ""
-#: frappe/core/doctype/report/report.py:77
+#: frappe/core/doctype/report/report.py:424
msgid "Only Administrator can save a standard report. Please rename and save."
msgstr ""
@@ -18753,7 +18808,7 @@ msgstr ""
msgid "Only Allow Edit For"
msgstr ""
-#: frappe/desk/query_report.py:448
+#: frappe/desk/query_report.py:453
msgid "Only CSV and Excel formats are supported for export"
msgstr ""
@@ -18761,7 +18816,7 @@ msgstr ""
msgid "Only Custom Modules can be renamed."
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1655
+#: frappe/core/doctype/doctype/doctype.py:1656
msgid "Only Options allowed for Data field are:"
msgstr ""
@@ -18770,7 +18825,7 @@ msgstr ""
msgid "Only Send Records Updated in Last X Hours"
msgstr ""
-#: frappe/core/doctype/file/file.py:201
+#: frappe/core/doctype/file/file.py:204
msgid "Only System Managers can make this file public."
msgstr ""
@@ -18807,7 +18862,7 @@ msgstr ""
msgid "Only one {0} can be set as primary."
msgstr ""
-#: frappe/desk/reportview.py:360
+#: frappe/desk/reportview.py:367
msgid "Only reports of type Report Builder can be deleted"
msgstr ""
@@ -18815,7 +18870,7 @@ msgstr ""
msgid "Only reports of type Report Builder can be edited"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:132
+#: frappe/custom/doctype/customize_form/customize_form.py:133
msgid "Only standard DocTypes are allowed to be customized from Customize Form."
msgstr ""
@@ -18823,7 +18878,7 @@ msgstr ""
msgid "Only the Administrator can delete a standard DocType."
msgstr ""
-#: frappe/desk/form/assign_to.py:198
+#: frappe/desk/form/assign_to.py:206
msgid "Only the assignee can complete this to-do."
msgstr ""
@@ -18854,15 +18909,13 @@ msgctxt "Access"
msgid "Open"
msgstr ""
-#: frappe/desk/page/desktop/desktop.js:521
-#: frappe/public/js/frappe/ui/keyboard.js:207
-#: frappe/public/js/frappe/ui/keyboard.js:218
+#: frappe/public/js/frappe/ui/keyboard.js:219
msgid "Open Awesomebar"
msgstr ""
-#: frappe/public/js/frappe/form/templates/timeline_message_box.html:75
-#: frappe/public/js/frappe/form/templates/timeline_message_box.html:96
+#: frappe/public/js/frappe/form/templates/timeline_message_box.html:76
#: frappe/public/js/frappe/form/templates/timeline_message_box.html:97
+#: frappe/public/js/frappe/form/templates/timeline_message_box.html:98
msgid "Open Communication"
msgstr ""
@@ -18876,8 +18929,8 @@ msgstr ""
msgid "Open Documents"
msgstr ""
-#: frappe/public/js/frappe/ui/keyboard.js:245
-msgid "Open Help"
+#: frappe/public/js/frappe/ui/keyboard.js:228
+msgid "Open Global Search"
msgstr ""
#: frappe/public/js/frappe/form/controls/data.js:84
@@ -18891,12 +18944,8 @@ msgstr ""
msgid "Open Reference Document"
msgstr ""
-#: frappe/public/js/frappe/ui/keyboard.js:228
-msgid "Open Settings"
-msgstr ""
-
-#: frappe/public/js/frappe/ui/toolbar/about.js:12
-msgid "Open Source Applications for the Web"
+#: frappe/public/js/frappe/ui/toolbar/about.js:15
+msgid "Open Source applications for the web."
msgstr ""
#: frappe/public/js/frappe/form/controls/base_control.js:165
@@ -18913,11 +18962,7 @@ msgstr ""
msgid "Open a dialog with mandatory fields to create a new record quickly. There must be at least one mandatory field to show in dialog."
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:232
-msgid "Open a module or tool"
-msgstr ""
-
-#: frappe/public/js/frappe/ui/keyboard.js:369
+#: frappe/public/js/frappe/ui/keyboard.js:361
msgid "Open console"
msgstr ""
@@ -18925,10 +18970,6 @@ msgstr ""
msgid "Open in a new tab"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:233
-msgid "Open in new tab"
-msgstr ""
-
#: frappe/public/js/frappe/list/list_view.js:1500
msgctxt "Description of a list view shortcut"
msgid "Open list item"
@@ -18981,11 +19022,11 @@ msgstr ""
msgid "Operation"
msgstr ""
-#: frappe/utils/data.py:2241
+#: frappe/utils/data.py:2245
msgid "Operator must be one of {0}"
msgstr ""
-#: frappe/database/query.py:2286
+#: frappe/database/query.py:2289
msgid "Operator {0} requires exactly 2 arguments (left and right operands)"
msgstr ""
@@ -19011,7 +19052,7 @@ msgstr ""
msgid "Option 3"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1673
+#: frappe/core/doctype/doctype/doctype.py:1674
msgid "Option {0} for field {1} is not a child table"
msgstr ""
@@ -19045,7 +19086,7 @@ msgstr ""
msgid "Options"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1401
+#: frappe/core/doctype/doctype/doctype.py:1402
msgid "Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'"
msgstr ""
@@ -19054,7 +19095,7 @@ msgstr ""
msgid "Options Help"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1702
+#: frappe/core/doctype/doctype/doctype.py:1703
msgid "Options for Rating field can range from 3 to 10"
msgstr ""
@@ -19062,7 +19103,7 @@ msgstr ""
msgid "Options for select. Each option on a new line."
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1418
+#: frappe/core/doctype/doctype/doctype.py:1419
msgid "Options for {0} must be set before setting the default value."
msgstr ""
@@ -19070,7 +19111,7 @@ msgstr ""
msgid "Options is required for field {0} of type {1}"
msgstr ""
-#: frappe/model/base_document.py:999
+#: frappe/model/base_document.py:1002
msgid "Options not set for link field {0}"
msgstr ""
@@ -19086,7 +19127,7 @@ msgstr ""
msgid "Order"
msgstr ""
-#: frappe/database/query.py:1325
+#: frappe/database/query.py:1327
msgid "Order By must be a string"
msgstr ""
@@ -19124,6 +19165,7 @@ msgstr ""
#: frappe/core/doctype/doctype/doctype.json
#: frappe/desk/doctype/event/event.json
#: frappe/desk/page/setup_wizard/install_fixtures.py:30
+#: frappe/public/js/frappe/ui/toolbar/search.js:683
msgid "Other"
msgstr ""
@@ -19188,7 +19230,7 @@ msgstr ""
#: frappe/email/doctype/auto_email_report/auto_email_report.json
#: frappe/printing/page/print/print.js:91
#: frappe/public/js/frappe/form/templates/print_layout.html:44
-#: frappe/public/js/frappe/views/reports/query_report.js:1948
+#: frappe/public/js/frappe/views/reports/query_report.js:1949
msgid "PDF"
msgstr ""
@@ -19223,7 +19265,7 @@ msgstr ""
msgid "PDF Settings"
msgstr ""
-#: frappe/utils/print_format.py:343
+#: frappe/utils/print_format.py:347
msgid "PDF generation failed"
msgstr ""
@@ -19379,7 +19421,7 @@ msgstr ""
msgid "Page Settings"
msgstr ""
-#: frappe/public/js/frappe/ui/keyboard.js:125
+#: frappe/public/js/frappe/ui/keyboard.js:139
msgid "Page Shortcuts"
msgstr ""
@@ -19416,7 +19458,7 @@ msgstr ""
#: frappe/public/html/print_template.html:38
#: frappe/public/js/frappe/views/reports/print_tree.html:89
-#: frappe/public/js/frappe/web_form/web_form.js:286
+#: frappe/public/js/frappe/web_form/web_form.js:289
#: frappe/templates/print_formats/standard.html:34
msgid "Page {0} of {1}"
msgstr ""
@@ -19460,11 +19502,11 @@ msgstr ""
#. Label of the nsm_parent_field (Data) field in DocType 'DocType'
#: frappe/core/doctype/doctype/doctype.json
-#: frappe/core/doctype/doctype/doctype.py:951
+#: frappe/core/doctype/doctype/doctype.py:952
msgid "Parent Field (Tree)"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:957
+#: frappe/core/doctype/doctype/doctype.py:958
msgid "Parent Field must be a valid fieldname"
msgstr ""
@@ -19478,7 +19520,7 @@ msgstr ""
msgid "Parent Label"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1221
+#: frappe/core/doctype/doctype/doctype.py:1222
msgid "Parent Missing"
msgstr ""
@@ -19491,7 +19533,7 @@ msgstr ""
msgid "Parent Table"
msgstr ""
-#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:404
+#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:406
msgid "Parent document type is required to create a dashboard chart"
msgstr ""
@@ -19563,7 +19605,7 @@ msgstr ""
msgid "Password"
msgstr ""
-#: frappe/core/doctype/user/user.py:518 frappe/core/doctype/user/user.py:1188
+#: frappe/core/doctype/user/user.py:522 frappe/core/doctype/user/user.py:1192
msgid "Password Reset"
msgstr ""
@@ -19572,7 +19614,7 @@ msgstr ""
msgid "Password Reset Link Generation Limit"
msgstr ""
-#: frappe/public/js/frappe/form/grid_row.js:887
+#: frappe/public/js/frappe/form/grid_row.js:913
msgid "Password cannot be filtered"
msgstr ""
@@ -19601,7 +19643,7 @@ msgstr ""
msgid "Password not found for {0} {1} {2}"
msgstr ""
-#: frappe/core/doctype/user/user.py:1350
+#: frappe/core/doctype/user/user.py:1354
msgid "Password requirements not met"
msgstr ""
@@ -19613,7 +19655,7 @@ msgstr ""
msgid "Password size exceeded the maximum allowed size"
msgstr ""
-#: frappe/core/doctype/user/user.py:959
+#: frappe/core/doctype/user/user.py:963
msgid "Password size exceeded the maximum allowed size."
msgstr ""
@@ -19782,7 +19824,8 @@ msgstr ""
msgid "Permission"
msgstr ""
-#: frappe/database/query.py:993 frappe/desk/doctype/workspace/workspace.py:108
+#: frappe/database/query.py:995 frappe/database/query.py:1370
+#: frappe/desk/doctype/workspace/workspace.py:108
msgid "Permission Error"
msgstr ""
@@ -19965,6 +20008,11 @@ msgstr ""
msgid "Pie"
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/search.js:661
+#: frappe/public/js/frappe/ui/toolbar/search.js:663
+msgid "Pin"
+msgstr ""
+
#. Label of the pincode (Data) field in DocType 'Contact Us Settings'
#: frappe/website/doctype/contact_us_settings/contact_us_settings.json
msgid "Pincode"
@@ -19977,6 +20025,10 @@ msgstr ""
msgid "Pink"
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/search.js:671
+msgid "Pinned"
+msgstr ""
+
#. Label of the placeholder (Data) field in DocType 'DocField'
#. Label of the placeholder (Data) field in DocType 'Custom Field'
#. Label of the placeholder (Data) field in DocType 'Customize Form Field'
@@ -20030,11 +20082,11 @@ msgstr ""
msgid "Please add a valid comment."
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1557
+#: frappe/public/js/frappe/views/reports/query_report.js:1558
msgid "Please adjust filters to include some data"
msgstr ""
-#: frappe/core/doctype/user/user.py:1159
+#: frappe/core/doctype/user/user.py:1163
msgid "Please ask your administrator to verify your sign-up"
msgstr ""
@@ -20058,11 +20110,11 @@ msgstr ""
msgid "Please check the filter values set for Dashboard Chart: {}"
msgstr ""
-#: frappe/model/base_document.py:1079
+#: frappe/model/base_document.py:1082
msgid "Please check the value of \"Fetch From\" set for field {0}"
msgstr ""
-#: frappe/core/doctype/user/user.py:1157
+#: frappe/core/doctype/user/user.py:1161
msgid "Please check your email for verification"
msgstr ""
@@ -20118,7 +20170,7 @@ msgstr ""
msgid "Please duplicate this to make changes"
msgstr ""
-#: frappe/core/doctype/system_settings/system_settings.py:183
+#: frappe/core/doctype/system_settings/system_settings.py:184
msgid "Please enable atleast one Social Login Key or LDAP or Login With Email Link before disabling username/password based login."
msgstr ""
@@ -20127,7 +20179,7 @@ msgstr ""
#: frappe/printing/page/print/print.js:689
#: frappe/printing/page/print/print.js:734
#: frappe/public/js/frappe/list/bulk_operations.js:161
-#: frappe/public/js/frappe/utils/utils.js:1702
+#: frappe/public/js/frappe/utils/utils.js:1717
msgid "Please enable pop-ups"
msgstr ""
@@ -20201,12 +20253,16 @@ msgstr ""
msgid "Please enter your old password."
msgstr ""
+#: frappe/public/js/frappe/form/save.js:256
+msgid "Please fill the following mandatory fields before saving:"
+msgstr ""
+
#: frappe/automation/doctype/auto_repeat/auto_repeat.py:444
msgid "Please find attached {0}: {1}"
msgstr ""
-#: frappe/templates/includes/comments/comments.py:42
-#: frappe/templates/includes/comments/comments.py:45
+#: frappe/templates/includes/comments/comments.py:46
+#: frappe/templates/includes/comments/comments.py:49
msgid "Please login to post a comment."
msgstr ""
@@ -20250,15 +20306,19 @@ msgstr ""
msgid "Please select DocType first"
msgstr ""
+#: frappe/desk/doctype/global_search_settings/global_search_settings.js:113
+msgid "Please select Document Type first."
+msgstr ""
+
#: frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.js:27
msgid "Please select Entity Type first"
msgstr ""
-#: frappe/core/doctype/system_settings/system_settings.py:119
+#: frappe/core/doctype/system_settings/system_settings.py:120
msgid "Please select Minimum Password Score"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1246
+#: frappe/public/js/frappe/views/reports/query_report.js:1247
msgid "Please select X and Y fields"
msgstr ""
@@ -20278,7 +20338,7 @@ msgstr ""
msgid "Please select a file or url"
msgstr ""
-#: frappe/model/rename_doc.py:687
+#: frappe/model/rename_doc.py:676
msgid "Please select a valid csv file with data"
msgstr ""
@@ -20320,7 +20380,7 @@ msgstr ""
msgid "Please set a printer mapping for this print format in the Printer Settings"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1468
+#: frappe/public/js/frappe/views/reports/query_report.js:1469
msgid "Please set filters"
msgstr ""
@@ -20340,7 +20400,7 @@ msgstr ""
msgid "Please set the series to be used."
msgstr ""
-#: frappe/core/doctype/system_settings/system_settings.py:133
+#: frappe/core/doctype/system_settings/system_settings.py:134
msgid "Please setup SMS before setting it as an authentication method, via SMS Settings"
msgstr ""
@@ -20348,7 +20408,7 @@ msgstr ""
msgid "Please setup a message first"
msgstr ""
-#: frappe/core/doctype/user/user.py:483
+#: frappe/core/doctype/user/user.py:487
msgid "Please setup default outgoing Email Account from Settings > Email Account"
msgstr ""
@@ -20520,11 +20580,11 @@ msgstr ""
msgid "Precision"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1711
+#: frappe/core/doctype/doctype/doctype.py:1712
msgid "Precision ({0}) for {1} cannot be greater than its length ({2})."
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1435
+#: frappe/core/doctype/doctype/doctype.py:1436
msgid "Precision should be between 1 and 6"
msgstr ""
@@ -20578,7 +20638,7 @@ msgstr ""
msgid "Prepared Report User"
msgstr ""
-#: frappe/desk/query_report.py:318
+#: frappe/desk/query_report.py:323
msgid "Prepared report render failed"
msgstr ""
@@ -20590,7 +20650,7 @@ msgstr ""
msgid "Prepend the template to the email message"
msgstr ""
-#: frappe/public/js/frappe/ui/keyboard.js:141
+#: frappe/public/js/frappe/ui/keyboard.js:155
msgid "Press Alt Key to trigger additional shortcuts in Menu and Sidebar"
msgstr ""
@@ -20708,7 +20768,7 @@ msgstr ""
msgid "Primary Phone"
msgstr ""
-#: frappe/database/mariadb/schema.py:156 frappe/database/postgres/schema.py:202
+#: frappe/database/mariadb/schema.py:187 frappe/database/postgres/schema.py:273
#: frappe/database/sqlite/schema.py:141
msgid "Primary key of doctype {0} can not be changed as there are existing values."
msgstr ""
@@ -20726,13 +20786,13 @@ msgstr ""
#: frappe/public/js/frappe/form/success_action.js:81
#: frappe/public/js/frappe/form/templates/print_layout.html:46
#: frappe/public/js/frappe/list/bulk_operations.js:95
-#: frappe/public/js/frappe/views/reports/query_report.js:1930
+#: frappe/public/js/frappe/views/reports/query_report.js:1931
#: frappe/public/js/frappe/views/reports/report_view.js:1630
#: frappe/public/js/frappe/views/treeview.js:501 frappe/www/printview.html:18
msgid "Print"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2302
+#: frappe/public/js/frappe/list/list_view.js:2296
msgctxt "Button in list view actions menu"
msgid "Print"
msgstr ""
@@ -20800,7 +20860,7 @@ msgstr ""
msgid "Print Format Type"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1676
+#: frappe/public/js/frappe/views/reports/query_report.js:1677
msgid "Print Format not found"
msgstr ""
@@ -20931,7 +20991,7 @@ msgstr ""
msgid "Printing"
msgstr ""
-#: frappe/utils/print_format.py:345
+#: frappe/utils/print_format.py:349
msgid "Printing failed"
msgstr ""
@@ -20980,7 +21040,7 @@ msgstr ""
msgid "Proceed"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:973
+#: frappe/public/js/frappe/views/reports/query_report.js:974
msgid "Proceed Anyway"
msgstr ""
@@ -21015,7 +21075,7 @@ msgstr ""
msgid "Progress"
msgstr ""
-#: frappe/public/js/frappe/views/kanban/kanban_view.js:423
+#: frappe/public/js/frappe/views/kanban/kanban_view.js:445
msgid "Project"
msgstr ""
@@ -21061,10 +21121,16 @@ msgstr ""
msgid "Protect Attached Files"
msgstr ""
-#: frappe/core/doctype/file/file.py:597
+#: frappe/core/doctype/file/file.py:600
msgid "Protected File"
msgstr ""
+#. Description of the 'Allowed Doctypes for Guest Uploads' (Small Text) field
+#. in DocType 'System Settings'
+#: frappe/core/doctype/system_settings/system_settings.json
+msgid "Provide a list of allowed Doctypes for file uploads. Each line should contain one allowed Doctype. If unset, uploads to all doctypes are allowed. Example: User Item Quotation"
+msgstr ""
+
#. Description of the 'Allowed File Extensions' (Small Text) field in DocType
#. 'System Settings'
#: frappe/core/doctype/system_settings/system_settings.json
@@ -21125,7 +21191,7 @@ msgstr ""
#. Label of the published (Check) field in DocType 'Web Form'
#. Label of the published (Check) field in DocType 'Web Page'
#: frappe/core/doctype/comment/comment.json
-#: frappe/public/js/frappe/form/templates/timeline_message_box.html:42
+#: frappe/public/js/frappe/form/templates/timeline_message_box.html:43
#: frappe/website/doctype/help_article/help_article.json
#: frappe/website/doctype/help_category/help_category.json
#: frappe/website/doctype/web_form/web_form.json
@@ -21304,7 +21370,7 @@ msgstr ""
msgid "Query analysis complete. Check suggested indexes."
msgstr ""
-#: frappe/utils/safe_exec.py:497
+#: frappe/utils/safe_exec.py:495
msgid "Query must be of SELECT or read-only WITH type."
msgstr ""
@@ -21505,7 +21571,7 @@ msgstr ""
msgid "Re:"
msgstr ""
-#: frappe/core/doctype/communication/communication.js:268
+#: frappe/core/doctype/communication/communication.js:271
#: frappe/public/js/frappe/form/footer/form_timeline.js:601
#: frappe/public/js/frappe/views/communication.js:420
msgid "Re: {0}"
@@ -21584,6 +21650,10 @@ msgstr ""
msgid "Read the documentation to know more"
msgstr ""
+#: frappe/utils/safe_exec.py:482
+msgid "Read-Only queries are allowed"
+msgstr ""
+
#. Label of the readme (Markdown Editor) field in DocType 'Package'
#: frappe/core/doctype/package/package.json
msgid "Readme"
@@ -21600,7 +21670,7 @@ msgstr ""
msgid "Reason"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:927
+#: frappe/public/js/frappe/views/reports/query_report.js:928
msgid "Rebuild"
msgstr ""
@@ -21642,10 +21712,6 @@ msgstr ""
msgid "Recent years are easy to guess."
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/search_utils.js:553
-msgid "Recents"
-msgstr ""
-
#. Label of the recipients (Table) field in DocType 'Email Queue'
#. Label of the recipient (Data) field in DocType 'Email Queue Recipient'
#: frappe/email/doctype/email_queue/email_queue.json
@@ -21695,7 +21761,7 @@ msgstr ""
msgid "Records for following doctypes will be filtered"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1643
+#: frappe/core/doctype/doctype/doctype.py:1644
msgid "Recursive Fetch From"
msgstr ""
@@ -21987,7 +22053,7 @@ msgstr ""
#: frappe/public/js/frappe/form/form.js:1250
#: frappe/public/js/frappe/form/templates/print_layout.html:6
#: frappe/public/js/frappe/list/base_list.js:67
-#: frappe/public/js/frappe/views/reports/query_report.js:1919
+#: frappe/public/js/frappe/views/reports/query_report.js:1920
#: frappe/public/js/frappe/views/treeview.js:507
#: frappe/public/js/frappe/widgets/chart_widget.js:296
#: frappe/public/js/frappe/widgets/number_card_widget.js:358
@@ -22034,7 +22100,7 @@ msgstr ""
msgid "Refreshing..."
msgstr ""
-#: frappe/core/doctype/user/user.py:1119
+#: frappe/core/doctype/user/user.py:1123
msgid "Registered but disabled"
msgstr ""
@@ -22136,7 +22202,6 @@ msgid "Reminder set at {0}"
msgstr ""
#: frappe/public/js/frappe/file_uploader/FilePreview.vue:70
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:13
#: frappe/public/js/frappe/ui/filters/edit_filter.html:4
#: frappe/public/js/frappe/ui/group_by/group_by.html:4
msgid "Remove"
@@ -22198,7 +22263,7 @@ msgstr ""
msgid "Removed"
msgstr ""
-#: frappe/desk/page/desktop/desktop.js:1197
+#: frappe/desk/page/desktop/desktop.js:1192
msgid "Removed Icons"
msgstr ""
@@ -22220,7 +22285,7 @@ msgstr ""
msgid "Rename {0}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:712
+#: frappe/core/doctype/doctype/doctype.py:713
msgid "Renamed files and replaced code in controllers, please check!"
msgstr ""
@@ -22280,7 +22345,7 @@ msgstr ""
msgid "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\""
msgstr ""
-#: frappe/public/js/frappe/form/sidebar/form_sidebar.js:196
+#: frappe/public/js/frappe/form/sidebar/form_sidebar.js:202
msgid "Repeats {0}"
msgstr ""
@@ -22445,7 +22510,7 @@ msgstr ""
#: frappe/core/report/prepared_report_analytics/prepared_report_analytics.py:39
#: frappe/desk/doctype/dashboard_chart/dashboard_chart.json
#: frappe/desk/doctype/number_card/number_card.json
-#: frappe/public/js/frappe/views/reports/query_report.js:2115
+#: frappe/public/js/frappe/views/reports/query_report.js:2119
msgid "Report Name"
msgstr ""
@@ -22497,7 +22562,7 @@ msgstr ""
msgid "Report has no numeric fields, please change the Report Name"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1054
+#: frappe/public/js/frappe/views/reports/query_report.js:1055
msgid "Report initiated, click to view status"
msgstr ""
@@ -22509,7 +22574,7 @@ msgstr ""
msgid "Report timed out."
msgstr ""
-#: frappe/desk/query_report.py:823
+#: frappe/desk/query_report.py:828
msgid "Report updated successfully"
msgstr ""
@@ -22517,7 +22582,7 @@ msgstr ""
msgid "Report was not saved (there were errors)"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:2153
+#: frappe/public/js/frappe/views/reports/query_report.js:2157
msgid "Report with more than 10 columns looks better in Landscape mode."
msgstr ""
@@ -22526,7 +22591,7 @@ msgstr ""
msgid "Report {0}"
msgstr ""
-#: frappe/desk/reportview.py:367
+#: frappe/desk/reportview.py:374
msgid "Report {0} deleted"
msgstr ""
@@ -22534,7 +22599,7 @@ msgstr ""
msgid "Report {0} is disabled"
msgstr ""
-#: frappe/desk/reportview.py:344
+#: frappe/desk/reportview.py:351
msgid "Report {0} saved"
msgstr ""
@@ -22545,7 +22610,6 @@ msgstr ""
#. Label of the prepared_report_section (Section Break) field in DocType
#. 'System Settings'
#: frappe/core/doctype/system_settings/system_settings.json
-#: frappe/public/js/frappe/ui/toolbar/search_utils.js:568
msgid "Reports"
msgstr ""
@@ -22553,7 +22617,7 @@ msgstr ""
msgid "Reports & Masters"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:970
+#: frappe/public/js/frappe/views/reports/query_report.js:971
msgid "Reports already in Queue"
msgstr ""
@@ -22655,12 +22719,12 @@ msgstr ""
msgid "Requires any valid fdn path. i.e. ou=users,dc=example,dc=com"
msgstr ""
-#: frappe/core/doctype/communication/communication.js:279
+#: frappe/core/doctype/communication/communication.js:282
msgid "Res: {0}"
msgstr ""
#: frappe/desk/doctype/form_tour/form_tour.js:101
-#: frappe/desk/doctype/global_search_settings/global_search_settings.js:19
+#: frappe/desk/doctype/global_search_settings/global_search_settings.js:94
#: frappe/desk/doctype/module_onboarding/module_onboarding.js:17
#: frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue:347
#: frappe/website/doctype/portal_settings/portal_settings.js:19
@@ -22739,7 +22803,7 @@ msgstr ""
msgid "Reset sorting"
msgstr ""
-#: frappe/public/js/frappe/form/grid_row.js:419
+#: frappe/public/js/frappe/form/grid_row.js:421
msgid "Reset to default"
msgstr ""
@@ -22797,7 +22861,7 @@ msgstr ""
msgid "Response Type"
msgstr ""
-#: frappe/public/js/frappe/ui/notifications/notifications.js:450
+#: frappe/public/js/frappe/ui/notifications/notifications.js:452
msgid "Rest of the day"
msgstr ""
@@ -22819,7 +22883,7 @@ msgstr ""
msgid "Restored"
msgstr ""
-#: frappe/core/doctype/deleted_document/deleted_document.py:74
+#: frappe/core/doctype/deleted_document/deleted_document.py:103
msgid "Restoring Deleted Document"
msgstr ""
@@ -22860,8 +22924,8 @@ msgctxt "Title of message showing restrictions in list view"
msgid "Restrictions"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:437
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:452
+#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:420
+#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:435
msgid "Result"
msgstr ""
@@ -23043,7 +23107,7 @@ msgstr ""
msgid "Role and Level"
msgstr ""
-#: frappe/core/doctype/user/user.py:432
+#: frappe/core/doctype/user/user.py:436
msgid "Role has been set as per the user type {0}"
msgstr ""
@@ -23164,7 +23228,7 @@ msgstr ""
msgid "Route: Example \"/desk\""
msgstr ""
-#: frappe/model/base_document.py:982 frappe/model/document.py:824
+#: frappe/model/base_document.py:985 frappe/model/document.py:824
msgid "Row"
msgstr ""
@@ -23177,11 +23241,11 @@ msgstr ""
msgid "Row # {0}: Non administrator user can not set the role {1} to the custom doctype"
msgstr ""
-#: frappe/model/base_document.py:1110
+#: frappe/model/base_document.py:1113
msgid "Row #{0}:"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:505
+#: frappe/core/doctype/doctype/doctype.py:506
msgid "Row #{}: Fieldname is required"
msgstr ""
@@ -23200,7 +23264,7 @@ msgstr ""
msgid "Row Name"
msgstr ""
-#: frappe/core/doctype/data_import/data_import.js:512
+#: frappe/core/doctype/data_import/data_import.js:514
msgid "Row Number"
msgstr ""
@@ -23212,11 +23276,11 @@ msgstr ""
msgid "Row {0}"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:358
+#: frappe/custom/doctype/customize_form/customize_form.py:359
msgid "Row {0}: Not allowed to disable Mandatory for standard fields"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:347
+#: frappe/custom/doctype/customize_form/customize_form.py:348
msgid "Row {0}: Not allowed to enable Allow on Submit for standard fields"
msgstr ""
@@ -23367,7 +23431,7 @@ msgstr ""
msgid "SQL Queries"
msgstr ""
-#: frappe/database/query.py:2131
+#: frappe/database/query.py:2134
msgid "SQL functions are not allowed as strings in SELECT: {0}. Use dict syntax like {{'COUNT': '*'}} instead."
msgstr ""
@@ -23461,7 +23525,8 @@ msgstr ""
#. Option for the 'Send Alert On' (Select) field in DocType 'Notification'
#: cypress/integration/web_form.js:52
#: frappe/core/doctype/data_import/data_import.js:119
-#: frappe/desk/page/desktop/desktop.html:64
+#: frappe/desk/doctype/global_search_settings/global_search_settings.js:31
+#: frappe/desk/page/desktop/desktop.html:65
#: frappe/email/doctype/notification/notification.json
#: frappe/printing/page/print/print.js:924
#: frappe/printing/page/print_format_builder/print_format_builder.js:162
@@ -23469,13 +23534,13 @@ msgstr ""
#: frappe/public/js/frappe/form/quick_entry.js:186
#: frappe/public/js/frappe/list/list_settings.js:37
#: frappe/public/js/frappe/list/list_settings.js:250
-#: frappe/public/js/frappe/list/list_view.js:2057
+#: frappe/public/js/frappe/list/list_view.js:2058
#: frappe/public/js/frappe/ui/toolbar/toolbar.js:335
#: frappe/public/js/frappe/utils/common.js:452
#: frappe/public/js/frappe/views/kanban/kanban_settings.js:45
#: frappe/public/js/frappe/views/kanban/kanban_settings.js:189
-#: frappe/public/js/frappe/views/kanban/kanban_view.js:358
-#: frappe/public/js/frappe/views/reports/query_report.js:2107
+#: frappe/public/js/frappe/views/kanban/kanban_view.js:380
+#: frappe/public/js/frappe/views/reports/query_report.js:2111
#: frappe/public/js/frappe/views/reports/report_view.js:1837
#: frappe/public/js/frappe/views/workspace/workspace.js:384
#: frappe/public/js/frappe/widgets/base_widget.js:143
@@ -23498,7 +23563,7 @@ msgstr ""
msgid "Save Customizations"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:2110
+#: frappe/public/js/frappe/views/reports/query_report.js:2114
msgid "Save Report"
msgstr ""
@@ -23515,11 +23580,11 @@ msgstr ""
msgid "Save the document."
msgstr ""
-#: frappe/model/rename_doc.py:106
+#: frappe/model/rename_doc.py:95
#: frappe/printing/page/print_format_builder/print_format_builder.js:860
#: frappe/public/js/frappe/form/toolbar.js:315
#: frappe/public/js/frappe/views/kanban/kanban_board.bundle.js:928
-#: frappe/public/js/frappe/views/workspace/workspace.js:784
+#: frappe/public/js/frappe/views/workspace/workspace.js:785
msgid "Saved"
msgstr ""
@@ -23538,7 +23603,7 @@ msgctxt "Freeze message while saving a document"
msgid "Saving"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2068
+#: frappe/public/js/frappe/list/list_view.js:2069
msgid "Saving Changes..."
msgstr ""
@@ -23752,17 +23817,18 @@ msgstr ""
#. Label of the search_section (Section Break) field in DocType 'System
#. Settings'
#: frappe/core/doctype/system_settings/system_settings.json
+#: frappe/desk/doctype/global_search_settings/global_search_settings.js:65
#: frappe/desk/page/desktop/desktop.html:19
#: frappe/public/js/frappe/data_import/data_exporter.js:335
#: frappe/public/js/frappe/form/link_selector.js:45
#: frappe/public/js/frappe/list/base_list.js:921
#: frappe/public/js/frappe/list/list_sidebar_group_by.js:141
#: frappe/public/js/frappe/list/list_sidebar_stat.html:4
-#: frappe/public/js/frappe/list/list_view.js:2077
+#: frappe/public/js/frappe/list/list_view.js:2078
#: frappe/public/js/frappe/ui/address_autocomplete/autocomplete_dialog.js:20
-#: frappe/public/js/frappe/ui/sidebar/sidebar.js:485
-#: frappe/public/js/frappe/ui/toolbar/search.js:49
-#: frappe/public/js/frappe/ui/toolbar/search.js:68
+#: frappe/public/js/frappe/ui/sidebar/sidebar.js:479
+#: frappe/public/js/frappe/ui/toolbar/search.js:96
+#: frappe/public/js/frappe/ui/toolbar/search.js:106
#: frappe/public/js/frappe/views/reports/report_view.js:1717
#: frappe/templates/discussions/search.html:2
#: frappe/templates/includes/search_template.html:26
@@ -23781,10 +23847,6 @@ msgstr ""
msgid "Search Fields"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:238
-msgid "Search Help"
-msgstr ""
-
#. Label of the allowed_in_global_search (Table) field in DocType 'Global
#. Search Settings'
#: frappe/desk/doctype/global_search_settings/global_search_settings.json
@@ -23799,7 +23861,7 @@ msgstr ""
msgid "Search by filename or extension"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1502
+#: frappe/core/doctype/doctype/doctype.py:1503
msgid "Search field {0} is not valid"
msgstr ""
@@ -23807,12 +23869,17 @@ msgstr ""
msgid "Search fields"
msgstr ""
+#: frappe/desk/doctype/global_search_settings/global_search_settings.js:45
+msgid "Search fields updated."
+msgstr ""
+
#: frappe/public/js/form_builder/components/AddFieldButton.vue:19
msgid "Search fieldtypes..."
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/search.js:50
-#: frappe/public/js/frappe/ui/toolbar/search.js:69
+#: frappe/public/js/frappe/ui/toolbar/search.js:97
+#: frappe/public/js/frappe/ui/toolbar/search.js:107
+#: frappe/public/js/frappe/ui/toolbar/search.js:140
msgid "Search for anything"
msgstr ""
@@ -23824,15 +23891,11 @@ msgstr ""
msgid "Search for icons..."
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:354
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:358
+#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:337
+#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:341
msgid "Search for {0}"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:230
-msgid "Search in a document type"
-msgstr ""
-
#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:35
msgid "Search or type a command"
msgstr ""
@@ -23855,7 +23918,7 @@ msgstr ""
msgid "Search..."
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/search.js:210
+#: frappe/public/js/frappe/ui/toolbar/search.js:340
msgid "Searching ..."
msgstr ""
@@ -23904,11 +23967,11 @@ msgstr ""
msgid "Section must have at least one column"
msgstr ""
-#: frappe/core/doctype/user/user.py:1515
+#: frappe/core/doctype/user/user.py:1519
msgid "Security Alert: Your account is being impersonated"
msgstr ""
-#: frappe/core/doctype/user/user.py:402
+#: frappe/core/doctype/user/user.py:406
msgid "Security Alert: Your password has been changed."
msgstr ""
@@ -23947,7 +24010,7 @@ msgstr ""
msgid "Security.txt will expire soon!"
msgstr ""
-#: frappe/public/js/frappe/ui/notifications/notifications.js:347
+#: frappe/public/js/frappe/ui/notifications/notifications.js:349
msgid "See all Activity"
msgstr ""
@@ -24013,7 +24076,7 @@ msgstr ""
#: frappe/public/js/frappe/data_import/data_exporter.js:154
#: frappe/public/js/frappe/form/controls/multicheck.js:174
#: frappe/public/js/frappe/form/controls/multiselect_list.js:19
-#: frappe/public/js/frappe/form/grid_row.js:483
+#: frappe/public/js/frappe/form/grid_row.js:485
#: frappe/public/js/frappe/list/list_view.js:760
#: frappe/public/js/frappe/list/list_view.js:825
#: frappe/public/js/frappe/views/file/file_view.js:374
@@ -24052,7 +24115,7 @@ msgstr ""
#. Label of the dashboard_name (Link) field in DocType 'Form Tour'
#: frappe/desk/doctype/form_tour/form_tour.json
-#: frappe/public/js/frappe/utils/dashboard_utils.js:239
+#: frappe/public/js/frappe/utils/dashboard_utils.js:252
msgid "Select Dashboard"
msgstr ""
@@ -24088,7 +24151,7 @@ msgstr ""
#: frappe/public/js/form_builder/components/controls/FetchFromControl.vue:33
#: frappe/public/js/frappe/doctype/index.js:207
-#: frappe/public/js/frappe/form/toolbar.js:880
+#: frappe/public/js/frappe/form/toolbar.js:881
msgid "Select Field"
msgstr ""
@@ -24097,7 +24160,7 @@ msgstr ""
msgid "Select Field..."
msgstr ""
-#: frappe/public/js/frappe/form/grid_row.js:475
+#: frappe/public/js/frappe/form/grid_row.js:477
#: frappe/public/js/frappe/views/kanban/kanban_settings.js:181
msgid "Select Fields"
msgstr ""
@@ -24114,7 +24177,7 @@ msgstr ""
msgid "Select Fields To Update"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2053
+#: frappe/public/js/frappe/list/list_view.js:2054
msgid "Select Filters"
msgstr ""
@@ -24283,8 +24346,8 @@ msgid "Select which delivery events should trigger a delivery status notificatio
msgstr ""
#: frappe/public/js/frappe/form/link_selector.js:24
-#: frappe/public/js/frappe/form/multi_select_dialog.js:80
-#: frappe/public/js/frappe/form/multi_select_dialog.js:282
+#: frappe/public/js/frappe/form/multi_select_dialog.js:81
+#: frappe/public/js/frappe/form/multi_select_dialog.js:283
#: frappe/public/js/frappe/list/list_view_select.js:148
#: frappe/public/js/print_format_builder/Preview.vue:90
msgid "Select {0}"
@@ -24573,7 +24636,7 @@ msgstr ""
msgid "Series counter for {} updated to {} successfully"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1133
+#: frappe/core/doctype/doctype/doctype.py:1134
#: frappe/core/doctype/document_naming_settings/document_naming_settings.py:170
msgid "Series {0} already used in {1}"
msgstr ""
@@ -24671,7 +24734,7 @@ msgstr ""
msgid "Session Expiry (idle timeout)"
msgstr ""
-#: frappe/core/doctype/system_settings/system_settings.py:126
+#: frappe/core/doctype/system_settings/system_settings.py:127
msgid "Session Expiry must be in format {0}"
msgstr ""
@@ -24725,7 +24788,7 @@ msgstr ""
msgid "Set Filters for {0}"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:2270
+#: frappe/public/js/frappe/views/reports/query_report.js:2278
msgid "Set Level"
msgstr ""
@@ -24884,7 +24947,7 @@ msgstr ""
msgid "Setting this Address Template as default as there is no other default"
msgstr ""
-#: frappe/desk/doctype/global_search_settings/global_search_settings.py:86
+#: frappe/desk/doctype/global_search_settings/global_search_settings.py:88
msgid "Setting up Global Search documents."
msgstr ""
@@ -24927,7 +24990,6 @@ msgstr ""
#. Option for the 'Show in Module Section' (Select) field in DocType 'DocType'
#: frappe/core/doctype/doctype/doctype.json
-#: frappe/public/js/frappe/ui/toolbar/search_utils.js:588
msgid "Setup"
msgstr ""
@@ -24943,7 +25005,7 @@ msgstr ""
msgid "Setup > User Permissions"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1972
+#: frappe/public/js/frappe/views/reports/query_report.js:1973
#: frappe/public/js/frappe/views/reports/report_view.js:1815
msgid "Setup Auto Email"
msgstr ""
@@ -24975,7 +25037,7 @@ msgstr ""
#: frappe/core/doctype/user_document_type/user_document_type.json
#: frappe/core/page/permission_manager/permission_manager_help.html:76
#: frappe/desk/doctype/notification_log/notification_log.json
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:135
+#: frappe/public/js/frappe/form/templates/form_sidebar.html:134
#: frappe/public/js/frappe/form/templates/set_sharing.html:5
msgid "Share"
msgstr ""
@@ -24997,7 +25059,7 @@ msgstr ""
msgid "Shared"
msgstr ""
-#: frappe/desk/form/assign_to.py:132
+#: frappe/desk/form/assign_to.py:136
msgid "Shared with the following Users with Read access:{0}"
msgstr ""
@@ -25048,7 +25110,7 @@ msgstr ""
msgid "Show Absolute Values"
msgstr ""
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:116
+#: frappe/public/js/frappe/form/templates/form_sidebar.html:115
msgid "Show All"
msgstr ""
@@ -25133,10 +25195,6 @@ msgstr ""
msgid "Show Full Number"
msgstr ""
-#: frappe/public/js/frappe/ui/keyboard.js:236
-msgid "Show Keyboard Shortcuts"
-msgstr ""
-
#. Label of the show_labels (Check) field in DocType 'Kanban Board'
#: frappe/desk/doctype/kanban_board/kanban_board.json
#: frappe/public/js/frappe/views/kanban/kanban_settings.js:30
@@ -25250,7 +25308,7 @@ msgstr ""
msgid "Show Tour"
msgstr ""
-#: frappe/core/doctype/data_import/data_import.js:476
+#: frappe/core/doctype/data_import/data_import.js:478
msgid "Show Traceback"
msgstr ""
@@ -25260,7 +25318,7 @@ msgstr ""
msgid "Show Values over Chart"
msgstr ""
-#: frappe/public/js/frappe/data_import/import_preview.js:204
+#: frappe/public/js/frappe/data_import/import_preview.js:206
msgid "Show Warnings"
msgstr ""
@@ -25320,6 +25378,10 @@ msgstr ""
msgid "Show in filter"
msgstr ""
+#: frappe/public/js/frappe/ui/keyboard.js:237
+msgid "Show keyboard shortcuts"
+msgstr ""
+
#. Label of the show_document_link (Check) field in DocType 'Slack Webhook URL'
#: frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.json
msgid "Show link to document"
@@ -25330,6 +25392,10 @@ msgstr ""
msgid "Show list"
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/search.js:977
+msgid "Show more"
+msgstr ""
+
#: frappe/public/js/frappe/form/layout.js:286
#: frappe/public/js/frappe/form/layout.js:301
msgid "Show more details"
@@ -25364,7 +25430,7 @@ msgstr ""
msgid "Showing only Numeric fields from Report"
msgstr ""
-#: frappe/public/js/frappe/data_import/import_preview.js:155
+#: frappe/public/js/frappe/data_import/import_preview.js:157
msgid "Showing only first {0} rows out of {1}"
msgstr ""
@@ -25416,7 +25482,7 @@ msgstr ""
msgid "Sign Up and Confirmation"
msgstr ""
-#: frappe/core/doctype/user/user.py:1112
+#: frappe/core/doctype/user/user.py:1116
msgid "Sign Up is disabled"
msgstr ""
@@ -25476,7 +25542,7 @@ msgstr ""
msgid "Simultaneous Sessions"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:129
+#: frappe/custom/doctype/customize_form/customize_form.py:130
msgid "Single DocTypes cannot be customized."
msgstr ""
@@ -25536,15 +25602,15 @@ msgstr ""
msgid "Skipped"
msgstr ""
-#: frappe/core/doctype/data_import/importer.py:956
+#: frappe/core/doctype/data_import/importer.py:957
msgid "Skipping Duplicate Column {0}"
msgstr ""
-#: frappe/core/doctype/data_import/importer.py:981
+#: frappe/core/doctype/data_import/importer.py:986
msgid "Skipping Untitled Column"
msgstr ""
-#: frappe/core/doctype/data_import/importer.py:967
+#: frappe/core/doctype/data_import/importer.py:970
msgid "Skipping column {0}"
msgstr ""
@@ -25764,13 +25830,13 @@ msgstr ""
msgid "Sort Order"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1585
+#: frappe/core/doctype/doctype/doctype.py:1586
msgid "Sort field {0} must be a valid fieldname"
msgstr ""
#. Label of the source (Data) field in DocType 'Web Page View'
#. Label of the source (Small Text) field in DocType 'Website Route Redirect'
-#: frappe/public/js/frappe/utils/utils.js:2005
+#: frappe/public/js/frappe/utils/utils.js:2020
#: frappe/website/doctype/web_page_view/web_page_view.json
#: frappe/website/doctype/website_route_redirect/website_route_redirect.json
#: frappe/website/report/website_analytics/website_analytics.js:38
@@ -25839,7 +25905,7 @@ msgstr ""
msgid "Splash Image"
msgstr ""
-#: frappe/desk/reportview.py:459
+#: frappe/desk/reportview.py:466
#: frappe/email/doctype/auto_email_report/auto_email_report.py:162
#: frappe/public/js/frappe/web_form/web_form_list.js:182
#: frappe/templates/print_formats/standard_macros.html:44
@@ -25879,7 +25945,7 @@ msgstr ""
msgid "Standard DocType can not be deleted."
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:230
+#: frappe/core/doctype/doctype/doctype.py:231
msgid "Standard DocType cannot have default print format, use Customize Form"
msgstr ""
@@ -25899,7 +25965,7 @@ msgstr ""
msgid "Standard Print Style cannot be changed. Please duplicate to edit."
msgstr ""
-#: frappe/desk/reportview.py:357
+#: frappe/desk/reportview.py:364
msgid "Standard Reports cannot be deleted"
msgstr ""
@@ -25917,6 +25983,10 @@ msgstr ""
msgid "Standard Web Forms can not be modified, duplicate the Web Form instead."
msgstr ""
+#: frappe/core/doctype/report/report.py:432
+msgid "Standard reports can only be created in developer mode."
+msgstr ""
+
#: frappe/website/doctype/web_page/web_page.js:94
msgid "Standard rich text editor with controls"
msgstr ""
@@ -26090,7 +26160,7 @@ msgstr ""
#: frappe/contacts/doctype/contact/contact.json
#: frappe/core/doctype/activity_log/activity_log.json
#: frappe/core/doctype/communication/communication.json
-#: frappe/core/doctype/data_import/data_import.js:513
+#: frappe/core/doctype/data_import/data_import.js:515
#: frappe/core/doctype/data_import/data_import.json
#: frappe/core/doctype/permission_log/permission_log.json
#: frappe/core/doctype/prepared_report/prepared_report.json
@@ -26108,7 +26178,7 @@ msgstr ""
#: frappe/integrations/doctype/integration_request/integration_request.json
#: frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.json
#: frappe/public/js/frappe/list/list_settings.js:362
-#: frappe/public/js/frappe/list/list_view.js:2494
+#: frappe/public/js/frappe/list/list_view.js:2488
#: frappe/public/js/frappe/views/reports/report_view.js:1070
#: frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.json
#: frappe/website/doctype/personal_data_deletion_step/personal_data_deletion_step.json
@@ -26146,8 +26216,8 @@ msgstr ""
#. Label of the sticky (Check) field in DocType 'DocField'
#: frappe/core/doctype/docfield/docfield.json
-#: frappe/public/js/frappe/form/grid_row.js:451
-#: frappe/public/js/frappe/form/grid_row.js:599
+#: frappe/public/js/frappe/form/grid_row.js:453
+#: frappe/public/js/frappe/form/grid_row.js:601
msgid "Sticky"
msgstr ""
@@ -26303,7 +26373,7 @@ msgstr ""
msgid "Submit"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2369
+#: frappe/public/js/frappe/list/list_view.js:2363
msgctxt "Button in list view actions menu"
msgid "Submit"
msgstr ""
@@ -26361,7 +26431,7 @@ msgstr ""
msgid "Submit this document to confirm"
msgstr ""
-#: frappe/public/js/frappe/list/list_view.js:2374
+#: frappe/public/js/frappe/list/list_view.js:2368
msgctxt "Title of confirmation dialog"
msgid "Submit {0} documents?"
msgstr ""
@@ -26374,7 +26444,7 @@ msgstr ""
msgid "Submitted"
msgstr ""
-#: frappe/workflow/doctype/workflow/workflow.py:104
+#: frappe/workflow/doctype/workflow/workflow.py:105
msgid "Submitted Document cannot be converted back to draft. Transition row {0}"
msgstr ""
@@ -26410,14 +26480,14 @@ msgstr ""
#. Field'
#. Option for the 'Style' (Select) field in DocType 'Workflow State'
#: frappe/core/doctype/activity_log/activity_log.json
-#: frappe/core/doctype/data_import/data_import.js:485
+#: frappe/core/doctype/data_import/data_import.js:487
#: frappe/core/doctype/data_import/data_import.json
#: frappe/core/doctype/data_import_log/data_import_log.json
#: frappe/core/doctype/docfield/docfield.json
#: frappe/custom/doctype/custom_field/custom_field.json
#: frappe/custom/doctype/customize_form_field/customize_form_field.json
#: frappe/desk/doctype/bulk_update/bulk_update.js:31
-#: frappe/public/js/frappe/form/grid.js:1270
+#: frappe/public/js/frappe/form/grid.js:1461
#: frappe/public/js/frappe/views/translation_manager.js:21
#: frappe/templates/includes/login/login.js:228
#: frappe/templates/includes/login/login.js:234
@@ -26463,7 +26533,7 @@ msgstr ""
msgid "Successful Transactions"
msgstr ""
-#: frappe/model/rename_doc.py:701
+#: frappe/model/rename_doc.py:690
msgid "Successful: {0} to {1}"
msgstr ""
@@ -26472,7 +26542,7 @@ msgstr ""
msgid "Successfully Updated"
msgstr ""
-#: frappe/core/doctype/data_import/data_import.js:449
+#: frappe/core/doctype/data_import/data_import.js:451
msgid "Successfully imported {0}"
msgstr ""
@@ -26480,11 +26550,11 @@ msgstr ""
msgid "Successfully imported {0} out of {1} records."
msgstr ""
-#: frappe/desk/doctype/form_tour/form_tour.py:87
+#: frappe/desk/doctype/form_tour/form_tour.py:88
msgid "Successfully reset onboarding status for all users."
msgstr ""
-#: frappe/core/doctype/user/user.py:1534
+#: frappe/core/doctype/user/user.py:1538
msgid "Successfully signed out"
msgstr ""
@@ -26492,7 +26562,7 @@ msgstr ""
msgid "Successfully updated translations"
msgstr ""
-#: frappe/core/doctype/data_import/data_import.js:457
+#: frappe/core/doctype/data_import/data_import.js:459
msgid "Successfully updated {0}"
msgstr ""
@@ -26509,7 +26579,7 @@ msgstr ""
msgid "Suggested Indexes"
msgstr ""
-#: frappe/core/doctype/user/user.py:804
+#: frappe/core/doctype/user/user.py:808
msgid "Suggested Username: {0}"
msgstr ""
@@ -26558,7 +26628,6 @@ msgstr ""
msgid "Switch Camera"
msgstr ""
-#: frappe/public/js/frappe/desk.js:96
#: frappe/public/js/frappe/ui/theme_switcher.js:11
msgid "Switch Theme"
msgstr ""
@@ -26567,6 +26636,10 @@ msgstr ""
msgid "Switch To Desk"
msgstr ""
+#: frappe/public/js/frappe/desk.js:96
+msgid "Switch theme"
+msgstr ""
+
#: frappe/public/js/frappe/ui/sidebar/sidebar.js:121
msgid "Switch to Frappe CRM"
msgstr ""
@@ -26639,7 +26712,7 @@ msgstr ""
msgid "Syncing {0} of {1}"
msgstr ""
-#: frappe/utils/data.py:2645
+#: frappe/utils/data.py:2649
msgid "Syntax Error"
msgstr ""
@@ -26948,7 +27021,7 @@ msgstr ""
msgid "Table Fieldname"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1227
+#: frappe/core/doctype/doctype/doctype.py:1228
msgid "Table Fieldname Missing"
msgstr ""
@@ -26966,7 +27039,7 @@ msgstr ""
msgid "Table MultiSelect"
msgstr ""
-#: frappe/desk/search.py:284
+#: frappe/desk/search.py:290
msgid "Table MultiSelect requires a table with at least one Link field, but none was found in {0}"
msgstr ""
@@ -26974,7 +27047,7 @@ msgstr ""
msgid "Table Trimmed"
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:1269
+#: frappe/public/js/frappe/form/grid.js:1460
msgid "Table updated"
msgstr ""
@@ -26998,13 +27071,12 @@ msgid "Tag Link"
msgstr ""
#: frappe/model/meta.py:59
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:125
+#: frappe/public/js/frappe/form/templates/form_sidebar.html:124
#: frappe/public/js/frappe/list/base_list.js:814
#: frappe/public/js/frappe/list/base_list.js:997
-#: frappe/public/js/frappe/list/bulk_operations.js:446
+#: frappe/public/js/frappe/list/bulk_operations.js:457
#: frappe/public/js/frappe/model/meta.js:215
#: frappe/public/js/frappe/model/model.js:133
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:231
msgid "Tags"
msgstr ""
@@ -27092,7 +27164,7 @@ msgstr ""
msgid "Templates"
msgstr ""
-#: frappe/core/doctype/user/user.py:1125
+#: frappe/core/doctype/user/user.py:1129
msgid "Temporarily Disabled"
msgstr ""
@@ -27196,7 +27268,7 @@ msgstr ""
msgid "The Bulk Update could not happen due to {0} "
msgstr ""
-#: frappe/public/js/frappe/form/grid.js:1292
+#: frappe/public/js/frappe/form/grid.js:1483
msgid "The CSV format is case sensitive"
msgstr ""
@@ -27212,7 +27284,7 @@ msgstr ""
msgid "The Condition '{0}' is invalid"
msgstr ""
-#: frappe/core/doctype/file/file.py:294
+#: frappe/core/doctype/file/file.py:297
msgid "The File URL you've entered is incorrect"
msgstr ""
@@ -27254,11 +27326,11 @@ msgstr ""
msgid "The changes have been reverted."
msgstr ""
-#: frappe/core/doctype/data_import/importer.py:1013
+#: frappe/core/doctype/data_import/importer.py:1018
msgid "The column {0} has {1} different date formats. Automatically setting {2} as the default format as it is the most common. Please change other values in this column to this format."
msgstr ""
-#: frappe/templates/includes/comments/comments.py:48
+#: frappe/templates/includes/comments/comments.py:52
msgid "The comment cannot be empty"
msgstr ""
@@ -27300,11 +27372,11 @@ msgstr ""
msgid "The email button is enabled for the user in the document."
msgstr ""
-#: frappe/desk/search.py:297
+#: frappe/desk/search.py:303
msgid "The field {0} in {1} does not allow ignoring user permissions"
msgstr ""
-#: frappe/desk/search.py:312
+#: frappe/desk/search.py:318
msgid "The field {0} in {1} links to {2} and not {3}"
msgstr ""
@@ -27312,7 +27384,7 @@ msgstr ""
msgid "The field {0} is mandatory"
msgstr ""
-#: frappe/core/doctype/file/file.py:192
+#: frappe/core/doctype/file/file.py:195
msgid "The fieldname you've specified in Attached To Field is invalid"
msgstr ""
@@ -27328,11 +27400,11 @@ msgstr ""
msgid "The following configured IMAP folder(s) were not found or are not accessible on the server: Please verify the folder names exactly as they appear on the server and ensure the account has access to them."
msgstr ""
-#: frappe/core/doctype/data_import/importer.py:1093
+#: frappe/core/doctype/data_import/importer.py:1099
msgid "The following values are invalid: {0}. Values must be one of {1}"
msgstr ""
-#: frappe/core/doctype/data_import/importer.py:1050
+#: frappe/core/doctype/data_import/importer.py:1056
msgid "The following values do not exist for {0}: {1}"
msgstr ""
@@ -27391,11 +27463,11 @@ msgstr ""
msgid "The report you requested has been generated. Click here to download:{0} This link will expire in {1} hours."
msgstr ""
-#: frappe/core/doctype/user/user.py:1083
+#: frappe/core/doctype/user/user.py:1087
msgid "The reset password link has been expired"
msgstr ""
-#: frappe/core/doctype/user/user.py:1085
+#: frappe/core/doctype/user/user.py:1089
msgid "The reset password link has either been used before or is invalid"
msgstr ""
@@ -27411,7 +27483,7 @@ msgstr ""
msgid "The selected document {0} is not a {1}."
msgstr ""
-#: frappe/utils/response.py:343
+#: frappe/utils/response.py:351
msgid "The system is being updated. Please refresh again after a few moments."
msgstr ""
@@ -27451,7 +27523,7 @@ msgstr ""
msgid "The user can view Sales Invoices but cannot modify any field values in them."
msgstr ""
-#: frappe/model/base_document.py:827
+#: frappe/model/base_document.py:830
msgid "The value of the field {0} is too long in the {1} document. To resolve this issue, please reduce the value length or change the {0} field Type to Long Text using customize form, and then try again."
msgstr ""
@@ -27496,7 +27568,7 @@ msgstr ""
msgid "There are documents which have workflow states that do not exist in this Workflow. It is recommended that you add these states to the Workflow and change their states before removing these states."
msgstr ""
-#: frappe/public/js/frappe/ui/notifications/notifications.js:484
+#: frappe/public/js/frappe/ui/notifications/notifications.js:486
msgid "There are no upcoming events for you."
msgstr ""
@@ -27504,7 +27576,7 @@ msgstr ""
msgid "There are no {0} for this {1}, why don't you start one!"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1006
+#: frappe/public/js/frappe/views/reports/query_report.js:1007
msgid "There are {0} with the same filters already in the queue:"
msgstr ""
@@ -27513,7 +27585,7 @@ msgstr ""
msgid "There can be only 9 Page Break fields in a Web Form"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1478
+#: frappe/core/doctype/doctype/doctype.py:1479
msgid "There can be only one Fold in a form"
msgstr ""
@@ -27529,15 +27601,15 @@ msgstr ""
msgid "There is no task called \"{}\""
msgstr ""
-#: frappe/public/js/frappe/ui/notifications/notifications.js:533
+#: frappe/public/js/frappe/ui/notifications/notifications.js:535
msgid "There is nothing new to show you right now."
msgstr ""
-#: frappe/core/doctype/file/file.py:714 frappe/utils/file_manager.py:372
+#: frappe/core/doctype/file/file.py:717 frappe/utils/file_manager.py:372
msgid "There is some problem with the file url: {0}"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1003
+#: frappe/public/js/frappe/views/reports/query_report.js:1004
msgid "There is {0} with the same filters already in the queue:"
msgstr ""
@@ -27549,7 +27621,7 @@ msgstr ""
msgid "There was an error building this page"
msgstr ""
-#: frappe/public/js/frappe/views/kanban/kanban_view.js:197
+#: frappe/public/js/frappe/views/kanban/kanban_view.js:219
msgid "There was an error saving filters"
msgstr ""
@@ -27606,7 +27678,7 @@ msgstr ""
msgid "This Currency is disabled. Enable to use in transactions"
msgstr ""
-#: frappe/public/js/frappe/views/kanban/kanban_view.js:406
+#: frappe/public/js/frappe/views/kanban/kanban_view.js:428
msgid "This Kanban Board will be private"
msgstr ""
@@ -27614,7 +27686,7 @@ msgstr ""
msgid "This Month"
msgstr ""
-#: frappe/core/doctype/file/file.py:470
+#: frappe/core/doctype/file/file.py:473
msgid "This PDF cannot be uploaded as it contains unsafe content."
msgstr ""
@@ -27662,7 +27734,7 @@ msgstr ""
msgid "This doctype has no orphan fields to trim"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1078
+#: frappe/core/doctype/doctype/doctype.py:1079
msgid "This doctype has pending migrations, run 'bench migrate' before modifying the doctype to avoid losing changes."
msgstr ""
@@ -27700,7 +27772,7 @@ msgid ""
"\t\t\t\tPlease contact your system manager to enable this by installing pycups!"
msgstr ""
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:40
+#: frappe/public/js/frappe/form/templates/form_sidebar.html:39
msgid "This feature is brand new and still experimental"
msgstr ""
@@ -27714,7 +27786,7 @@ msgid ""
"eval:doc.age>18"
msgstr ""
-#: frappe/core/doctype/file/file.py:596
+#: frappe/core/doctype/file/file.py:599
msgid "This file is attached to a protected document and cannot be deleted."
msgstr ""
@@ -27749,7 +27821,7 @@ msgstr ""
msgid "This goes above the slideshow."
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:2347
+#: frappe/public/js/frappe/views/reports/query_report.js:2355
msgid "This is a background report. Please set the appropriate filters and then generate a new one."
msgstr ""
@@ -27799,7 +27871,7 @@ msgstr ""
msgid "This month"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1082
+#: frappe/public/js/frappe/views/reports/query_report.js:1083
msgid "This report contains {0} rows and is too big to display in browser, you can {1} this report instead."
msgstr ""
@@ -27807,7 +27879,7 @@ msgstr ""
msgid "This report was generated on {0}"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:790
+#: frappe/public/js/frappe/views/reports/query_report.js:791
msgid "This report was generated {0}."
msgstr ""
@@ -27875,7 +27947,7 @@ msgstr ""
msgid "This will terminate the job immediately and might be dangerous, are you sure?"
msgstr ""
-#: frappe/core/doctype/user/user.py:1365
+#: frappe/core/doctype/user/user.py:1369
msgid "Throttled"
msgstr ""
@@ -27984,7 +28056,7 @@ msgstr ""
msgid "Time in seconds to retain QR code image on server. Min:240 "
msgstr ""
-#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:413
+#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:415
msgid "Time series based on is required to create a dashboard chart"
msgstr ""
@@ -28028,11 +28100,11 @@ msgstr ""
msgid "Timeline Name"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1573
+#: frappe/core/doctype/doctype/doctype.py:1574
msgid "Timeline field must be a Link or Dynamic Link"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1569
+#: frappe/core/doctype/doctype/doctype.py:1570
msgid "Timeline field must be a valid fieldname"
msgstr ""
@@ -28126,7 +28198,7 @@ msgstr ""
msgid "Title Prefix"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1510
+#: frappe/core/doctype/doctype/doctype.py:1511
msgid "Title field must be a valid fieldname"
msgstr ""
@@ -28265,7 +28337,7 @@ msgstr ""
msgid "Today"
msgstr ""
-#: frappe/desk/page/desktop/desktop.js:531
+#: frappe/desk/page/desktop/desktop.js:521
msgid "Toggle Awesomebar"
msgstr ""
@@ -28273,15 +28345,25 @@ msgstr ""
msgid "Toggle Chart"
msgstr ""
+#: frappe/public/js/frappe/ui/sidebar/sidebar_header.js:248
+msgid "Toggle Full Width"
+msgstr ""
+
#: frappe/public/js/frappe/views/file/file_view.js:33
msgid "Toggle Grid View"
msgstr ""
#: frappe/public/js/frappe/form/toolbar.js:472
+#: frappe/public/js/frappe/ui/sidebar/sidebar.html:72
+#: frappe/public/js/frappe/ui/sidebar/sidebar_header.js:256
msgid "Toggle Sidebar"
msgstr ""
-#: frappe/public/js/frappe/ui/sidebar/sidebar.js:319
+#: frappe/public/js/frappe/ui/sidebar/sidebar_header.js:239
+msgid "Toggle Theme"
+msgstr ""
+
+#: frappe/public/js/frappe/ui/sidebar/sidebar.js:321
msgid "Toggle sidebar"
msgstr ""
@@ -28340,7 +28422,7 @@ msgstr ""
msgid "Too many requests. Please try again later."
msgstr ""
-#: frappe/core/doctype/user/user.py:1126
+#: frappe/core/doctype/user/user.py:1130
msgid "Too many users signed up recently, so the registration is disabled. Please try back in an hour"
msgstr ""
@@ -28401,9 +28483,9 @@ msgstr ""
msgid "Topic"
msgstr ""
-#: frappe/desk/query_report.py:756
+#: frappe/desk/query_report.py:761
#: frappe/public/js/frappe/views/reports/print_grid.html:50
-#: frappe/public/js/frappe/views/reports/query_report.js:1384
+#: frappe/public/js/frappe/views/reports/query_report.js:1385
#: frappe/public/js/frappe/views/reports/report_view.js:1645
msgid "Total"
msgstr ""
@@ -28520,7 +28602,7 @@ msgstr ""
msgid "Track milestones for any document"
msgstr ""
-#: frappe/public/js/frappe/utils/utils.js:2069
+#: frappe/public/js/frappe/utils/utils.js:2084
msgid "Tracking URL generated and copied to clipboard"
msgstr ""
@@ -28556,7 +28638,7 @@ msgstr ""
msgid "Translatable"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:2408
+#: frappe/public/js/frappe/views/reports/query_report.js:2417
msgid "Translate Data"
msgstr ""
@@ -28629,10 +28711,6 @@ msgstr ""
msgid "Trigger Method"
msgstr ""
-#: frappe/public/js/frappe/ui/keyboard.js:196
-msgid "Trigger Primary Action"
-msgstr ""
-
#: frappe/tests/test_translate.py:55
msgid "Trigger caching"
msgstr ""
@@ -28642,6 +28720,10 @@ msgstr ""
msgid "Trigger on valid methods like \"before_insert\", \"after_update\", etc (will depend on the DocType selected)"
msgstr ""
+#: frappe/public/js/frappe/ui/keyboard.js:210
+msgid "Trigger primary action"
+msgstr ""
+
#: frappe/custom/doctype/customize_form/customize_form.js:153
msgid "Trim Table"
msgstr ""
@@ -28824,7 +28906,7 @@ msgstr ""
msgid "URL for documentation or help"
msgstr ""
-#: frappe/core/doctype/file/file.py:305
+#: frappe/core/doctype/file/file.py:308
msgid "URL must start with http:// or https://"
msgstr ""
@@ -28895,7 +28977,7 @@ msgstr ""
msgid "UUID"
msgstr ""
-#: frappe/desk/form/document_follow.py:85
+#: frappe/desk/form/document_follow.py:88
msgid "Un-following document {0}"
msgstr ""
@@ -28927,7 +29009,7 @@ msgstr ""
msgid "Unable to update event"
msgstr ""
-#: frappe/core/doctype/file/file.py:560
+#: frappe/core/doctype/file/file.py:563
msgid "Unable to write file format for {0}"
msgstr ""
@@ -28953,8 +29035,8 @@ msgstr ""
msgid "Undo last action"
msgstr ""
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:154
-#: frappe/public/js/frappe/form/toolbar.js:950
+#: frappe/public/js/frappe/form/templates/form_sidebar.html:153
+#: frappe/public/js/frappe/form/toolbar.js:951
msgid "Unfollow"
msgstr ""
@@ -29011,6 +29093,11 @@ msgstr ""
msgid "Unlock Reference Document"
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/search.js:657
+#: frappe/public/js/frappe/ui/toolbar/search.js:659
+msgid "Unpin"
+msgstr ""
+
#: frappe/public/js/frappe/form/footer/form_timeline.js:634
#: frappe/website/doctype/web_form/web_form.js:86
msgid "Unpublish"
@@ -29027,7 +29114,7 @@ msgstr ""
msgid "Unread Notification Sent"
msgstr ""
-#: frappe/utils/safe_exec.py:498
+#: frappe/utils/safe_exec.py:483 frappe/utils/safe_exec.py:496
msgid "Unsafe SQL query"
msgstr ""
@@ -29066,15 +29153,15 @@ msgstr ""
msgid "Unsubscribed"
msgstr ""
-#: frappe/desk/query_report.py:447
+#: frappe/desk/query_report.py:452
msgid "Unsupported file format: {0}"
msgstr ""
-#: frappe/database/query.py:1165
+#: frappe/database/query.py:1167
msgid "Unsupported function or operator: {0}"
msgstr ""
-#: frappe/database/query.py:2222
+#: frappe/database/query.py:2225
msgid "Unsupported {0}: {1}"
msgstr ""
@@ -29108,7 +29195,7 @@ msgstr ""
#: frappe/printing/page/print_format_builder/print_format_builder.js:509
#: frappe/printing/page/print_format_builder/print_format_builder.js:680
#: frappe/printing/page/print_format_builder/print_format_builder.js:767
-#: frappe/public/js/frappe/form/grid_row.js:413
+#: frappe/public/js/frappe/form/grid_row.js:415
msgid "Update"
msgstr ""
@@ -29118,6 +29205,10 @@ msgstr ""
msgid "Update Amendment Naming"
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/about.js:119
+msgid "Update Available"
+msgstr ""
+
#. Option for the 'Import Type' (Select) field in DocType 'Data Import'
#: frappe/core/doctype/data_import/data_import.json
msgid "Update Existing Records"
@@ -29175,19 +29266,23 @@ msgstr ""
msgid "Update Value"
msgstr ""
-#: frappe/utils/change_log.py:381
+#: frappe/utils/change_log.py:393
msgid "Update from Frappe Cloud"
msgstr ""
-#: frappe/public/js/frappe/list/bulk_operations.js:387
+#: frappe/public/js/frappe/list/bulk_operations.js:396
msgid "Update {0} records"
msgstr ""
+#: frappe/public/js/frappe/form/grid.js:1166
+msgid "Update {0} rows"
+msgstr ""
+
#. Option for the 'Comment Type' (Select) field in DocType 'Comment'
#. Option for the 'Status' (Select) field in DocType 'Permission Log'
#: frappe/core/doctype/comment/comment.json
#: frappe/core/doctype/permission_log/permission_log.json
-#: frappe/public/js/frappe/web_form/web_form.js:449
+#: frappe/public/js/frappe/web_form/web_form.js:452
msgid "Updated"
msgstr ""
@@ -29199,11 +29294,15 @@ msgstr ""
msgid "Updated To A New Version 🎉"
msgstr ""
-#: frappe/public/js/frappe/list/bulk_operations.js:384
+#: frappe/public/js/frappe/list/bulk_operations.js:393
msgid "Updated successfully"
msgstr ""
-#: frappe/utils/response.py:342
+#: frappe/public/js/frappe/form/grid.js:1159
+msgid "Updated {0} selected {1}. Save the form to keep changes."
+msgstr ""
+
+#: frappe/utils/response.py:350
msgid "Updating"
msgstr ""
@@ -29220,7 +29319,7 @@ msgstr ""
msgid "Updating counter may lead to document name conflicts if not done properly"
msgstr ""
-#: frappe/desk/page/setup_wizard/setup_wizard.py:23
+#: frappe/desk/page/setup_wizard/setup_wizard.py:24
msgid "Updating global settings"
msgstr ""
@@ -29232,6 +29331,10 @@ msgstr ""
msgid "Updating related fields..."
msgstr ""
+#: frappe/desk/doctype/global_search_settings/global_search_settings.js:38
+msgid "Updating search index"
+msgstr ""
+
#: frappe/desk/doctype/bulk_update/bulk_update.py:118
msgid "Updating {0}"
msgstr ""
@@ -29242,8 +29345,7 @@ msgstr ""
#: frappe/public/js/frappe/file_uploader/file_uploader.bundle.js:152
#: frappe/public/js/frappe/file_uploader/file_uploader.bundle.js:153
-#: frappe/public/js/frappe/form/grid.js:113
-#: frappe/public/js/frappe/form/templates/form_sidebar.html:12
+#: frappe/public/js/frappe/form/grid.js:117
msgid "Upload"
msgstr ""
@@ -29347,6 +29449,10 @@ msgstr ""
msgid "Use a few words, avoid common phrases."
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/search.js:134
+msgid "Use ampersand to match multiple terms"
+msgstr ""
+
#. Label of the login_id_is_different (Check) field in DocType 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "Use different Email ID"
@@ -29581,7 +29687,7 @@ msgstr ""
#. Label of a Link in the Users Workspace
#: frappe/core/page/permission_manager/permission_manager_help.html:97
#: frappe/core/workspace/users/users.json
-#: frappe/public/js/frappe/views/reports/query_report.js:2094
+#: frappe/public/js/frappe/views/reports/query_report.js:2098
#: frappe/public/js/frappe/views/reports/report_view.js:1863
msgid "User Permissions"
msgstr ""
@@ -29683,15 +29789,15 @@ msgstr ""
msgid "User with email: {0} does not exist in the system. Please ask 'System Administrator' to create the user for you."
msgstr ""
-#: frappe/core/doctype/user/user.py:609
+#: frappe/core/doctype/user/user.py:613
msgid "User {0} cannot be deleted"
msgstr ""
-#: frappe/core/doctype/user/user.py:378
+#: frappe/core/doctype/user/user.py:382
msgid "User {0} cannot be disabled"
msgstr ""
-#: frappe/core/doctype/user/user.py:682
+#: frappe/core/doctype/user/user.py:686
msgid "User {0} cannot be renamed"
msgstr ""
@@ -29712,11 +29818,11 @@ msgstr ""
msgid "User {0} has requested for data deletion"
msgstr ""
-#: frappe/core/doctype/user/user.py:1509
+#: frappe/core/doctype/user/user.py:1513
msgid "User {0} has started an impersonation session as you. Reason provided: {1}"
msgstr ""
-#: frappe/core/doctype/user/user.py:1492
+#: frappe/core/doctype/user/user.py:1496
msgid "User {0} impersonated as {1}"
msgstr ""
@@ -29728,7 +29834,7 @@ msgstr ""
msgid "User {0} is disabled. Please contact your System Manager."
msgstr ""
-#: frappe/desk/form/assign_to.py:104
+#: frappe/desk/form/assign_to.py:108
msgid "User {0} is not permitted to access this document."
msgstr ""
@@ -29745,11 +29851,12 @@ msgstr ""
msgid "Username"
msgstr ""
-#: frappe/core/doctype/user/user.py:771
+#: frappe/core/doctype/user/user.py:775
msgid "Username {0} already exists"
msgstr ""
#. Label of the users (Table MultiSelect) field in DocType 'Assignment Rule'
+#. Label of the weighted_users (Table) field in DocType 'Assignment Rule'
#. Name of a Workspace
#. Label of the users_section (Section Break) field in DocType 'System Health
#. Report'
@@ -29829,7 +29936,7 @@ msgstr ""
msgid "Validate SSL Certificate"
msgstr ""
-#: frappe/public/js/frappe/web_form/web_form.js:382
+#: frappe/public/js/frappe/web_form/web_form.js:385
msgid "Validation Error"
msgstr ""
@@ -29857,8 +29964,10 @@ msgstr ""
#: frappe/email/doctype/auto_email_report/auto_email_report.js:95
#: frappe/integrations/doctype/query_parameters/query_parameters.json
#: frappe/integrations/doctype/webhook_header/webhook_header.json
-#: frappe/public/js/frappe/list/bulk_operations.js:336
-#: frappe/public/js/frappe/list/bulk_operations.js:412
+#: frappe/public/js/frappe/form/grid.js:1136
+#: frappe/public/js/frappe/form/grid.js:1187
+#: frappe/public/js/frappe/list/bulk_operations.js:345
+#: frappe/public/js/frappe/list/bulk_operations.js:421
#: frappe/public/js/frappe/list/list_view_permission_restrictions.html:4
#: frappe/website/doctype/web_form/web_form.js:247
#: frappe/website/doctype/website_meta_tag/website_meta_tag.json
@@ -29885,11 +29994,11 @@ msgstr ""
msgid "Value To Be Set"
msgstr ""
-#: frappe/model/base_document.py:830
+#: frappe/model/base_document.py:833
msgid "Value Too Long"
msgstr ""
-#: frappe/model/base_document.py:1186 frappe/model/document.py:880
+#: frappe/model/base_document.py:1189 frappe/model/document.py:880
msgid "Value cannot be changed for {0}"
msgstr ""
@@ -29905,7 +30014,7 @@ msgstr ""
msgid "Value for a check field can be either 0 or 1"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:620
+#: frappe/custom/doctype/customize_form/customize_form.py:621
msgid "Value for field {0} is too long in {1}. Length should be lesser than {2} characters"
msgstr ""
@@ -29940,7 +30049,7 @@ msgstr ""
msgid "Value to set when this workflow state is applied. Use plain text (e.g. Approved) or an expression if “Evaluate as Expression” is enabled."
msgstr ""
-#: frappe/model/base_document.py:1256
+#: frappe/model/base_document.py:1259
msgid "Value too big"
msgstr ""
@@ -30041,11 +30150,15 @@ msgstr ""
msgid "View Doctype Permissions"
msgstr ""
+#: frappe/public/js/frappe/form/info_card.js:48
+msgid "View Documentation"
+msgstr ""
+
#: frappe/core/doctype/file/file.js:4
msgid "View File"
msgstr ""
-#: frappe/public/js/frappe/ui/notifications/notifications.js:258
+#: frappe/public/js/frappe/ui/notifications/notifications.js:260
msgid "View Full Log"
msgstr ""
@@ -30113,7 +30226,7 @@ msgstr ""
msgid "View this in your browser"
msgstr ""
-#: frappe/public/js/frappe/web_form/web_form.js:478
+#: frappe/public/js/frappe/web_form/web_form.js:481
msgctxt "Button in web form"
msgid "View your response"
msgstr ""
@@ -30149,7 +30262,7 @@ msgstr ""
msgid "Virtual DocType {} requires overriding an instance method called {} found {}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1692
+#: frappe/core/doctype/doctype/doctype.py:1693
msgid "Virtual tables must be virtual fields"
msgstr ""
@@ -30158,7 +30271,7 @@ msgstr ""
msgid "Visibility"
msgstr ""
-#: frappe/public/js/frappe/form/templates/timeline_message_box.html:41
+#: frappe/public/js/frappe/form/templates/timeline_message_box.html:42
msgid "Visible to website/portal users."
msgstr ""
@@ -30171,6 +30284,10 @@ msgstr ""
msgid "Visit Desktop"
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/about.js:50
+msgid "Visit Frappe Support Portal"
+msgstr ""
+
#: frappe/website/doctype/website_route_meta/website_route_meta.js:7
msgid "Visit Web Page"
msgstr ""
@@ -30206,7 +30323,7 @@ msgstr ""
msgid "Warning: DATA LOSS IMMINENT! Proceeding will permanently delete following database columns from doctype {0}:"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1149
+#: frappe/core/doctype/doctype/doctype.py:1150
msgid "Warning: Naming is not set"
msgstr ""
@@ -30219,7 +30336,7 @@ msgstr ""
msgid "Warning: Updating counter may lead to document name conflicts if not done properly"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:458
+#: frappe/core/doctype/doctype/doctype.py:459
msgid "Warning: Usage of 'format:' is discouraged."
msgstr ""
@@ -30289,7 +30406,7 @@ msgstr ""
msgid "Web Page Block"
msgstr ""
-#: frappe/public/js/frappe/utils/utils.js:1997
+#: frappe/public/js/frappe/utils/utils.js:2012
msgid "Web Page URL"
msgstr ""
@@ -30394,7 +30511,7 @@ msgstr ""
#: frappe/core/doctype/security_settings_contact/security_settings_contact.json
#: frappe/desktop_icon/website.json
#: frappe/public/js/frappe/ui/sidebar/sidebar_header.js:29
-#: frappe/public/js/frappe/ui/toolbar/about.js:16
+#: frappe/public/js/frappe/ui/toolbar/about.js:18
#: frappe/website/workspace/website/website.json
#: frappe/workspace_sidebar/website.json
msgid "Website"
@@ -30451,7 +30568,7 @@ msgstr ""
msgid "Website Search Field"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1557
+#: frappe/core/doctype/doctype/doctype.py:1558
msgid "Website Search Field must be a valid fieldname"
msgstr ""
@@ -30593,6 +30710,16 @@ msgstr ""
msgid "Weekly Long"
msgstr ""
+#. Label of the weight (Int) field in DocType 'Assignment Rule User'
+#: frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json
+msgid "Weight"
+msgstr ""
+
+#. Option for the 'Rule' (Select) field in DocType 'Assignment Rule'
+#: frappe/automation/doctype/assignment_rule/assignment_rule.json
+msgid "Weighted Distribution"
+msgstr ""
+
#: frappe/desk/page/setup_wizard/setup_wizard.js:403
msgid "Welcome"
msgstr ""
@@ -30615,7 +30742,7 @@ msgstr ""
msgid "Welcome Workspace"
msgstr ""
-#: frappe/core/doctype/user/user.py:475
+#: frappe/core/doctype/user/user.py:479
msgid "Welcome email sent"
msgstr ""
@@ -30623,15 +30750,15 @@ msgstr ""
msgid "Welcome to Frappe!"
msgstr ""
-#: frappe/public/js/frappe/views/workspace/workspace.js:710
+#: frappe/public/js/frappe/views/workspace/workspace.js:711
msgid "Welcome to the {0} workspace"
msgstr ""
-#: frappe/core/doctype/user/user.py:542
+#: frappe/core/doctype/user/user.py:546
msgid "Welcome to {0}"
msgstr ""
-#: frappe/public/js/frappe/ui/notifications/notifications.js:76
+#: frappe/public/js/frappe/ui/notifications/notifications.js:78
msgid "What's New"
msgstr ""
@@ -30933,7 +31060,7 @@ msgstr ""
msgid "Workspace added to desktop"
msgstr ""
-#: frappe/public/js/frappe/views/workspace/workspace.js:589
+#: frappe/public/js/frappe/views/workspace/workspace.js:590
msgid "Workspace {0} created"
msgstr ""
@@ -30950,7 +31077,7 @@ msgstr ""
msgid "Would you like to unpublish this comment? This means it will no longer be visible to website/portal users."
msgstr ""
-#: frappe/desk/page/setup_wizard/setup_wizard.py:41
+#: frappe/desk/page/setup_wizard/setup_wizard.py:42
msgid "Wrapping up"
msgstr ""
@@ -30967,7 +31094,7 @@ msgstr ""
msgid "Write"
msgstr ""
-#: frappe/model/base_document.py:1082
+#: frappe/model/base_document.py:1085
msgid "Wrong Fetch From value"
msgstr ""
@@ -31000,7 +31127,7 @@ msgstr ""
#. Label of the y_field (Select) field in DocType 'Dashboard Chart Field'
#: frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.json
-#: frappe/public/js/frappe/views/reports/query_report.js:1285
+#: frappe/public/js/frappe/views/reports/query_report.js:1286
msgid "Y Field"
msgstr ""
@@ -31139,13 +31266,13 @@ msgstr ""
msgid "You are not allowed to delete a standard Website Theme"
msgstr ""
-#: frappe/core/doctype/report/report.py:414
+#: frappe/core/doctype/report/report.py:437
msgid "You are not allowed to edit the report."
msgstr ""
#: frappe/core/doctype/data_import/exporter.py:121
#: frappe/core/doctype/data_import/exporter.py:125
-#: frappe/desk/reportview.py:448 frappe/desk/reportview.py:451
+#: frappe/desk/reportview.py:455 frappe/desk/reportview.py:458
#: frappe/permissions.py:651
msgid "You are not allowed to export {} doctype"
msgstr ""
@@ -31162,7 +31289,7 @@ msgstr ""
msgid "You are not allowed to update attendance for another user."
msgstr ""
-#: frappe/website/doctype/web_form/web_form.py:648
+#: frappe/website/doctype/web_form/web_form.py:652
msgid "You are not allowed to update this Web Form Document"
msgstr ""
@@ -31243,7 +31370,7 @@ msgstr ""
msgid "You can disable this {0} instead of deleting it."
msgstr ""
-#: frappe/core/doctype/file/file.py:832
+#: frappe/core/doctype/file/file.py:835
msgid "You can increase the limit from System Settings."
msgstr ""
@@ -31255,7 +31382,7 @@ msgstr ""
msgid "You can only edit your own replies."
msgstr ""
-#: frappe/desk/form/document_follow.py:56
+#: frappe/desk/form/document_follow.py:59
msgid "You can only follow documents for yourself."
msgstr ""
@@ -31267,11 +31394,11 @@ msgstr ""
msgid "You can only print upto {0} documents at a time"
msgstr ""
-#: frappe/desk/form/document_follow.py:75
+#: frappe/desk/form/document_follow.py:78
msgid "You can only unfollow documents for yourself."
msgstr ""
-#: frappe/handler.py:188
+#: frappe/handler.py:198
msgid "You can only upload JPG, PNG, GIF, PDF, TXT, CSV or Microsoft documents."
msgstr ""
@@ -31289,7 +31416,7 @@ msgstr ""
msgid "You can set a high value here if multiple users will be logging in from the same network."
msgstr ""
-#: frappe/desk/query_report.py:401
+#: frappe/desk/query_report.py:406
msgid "You can try changing the filters of your report."
msgstr ""
@@ -31297,15 +31424,15 @@ msgstr ""
msgid "You can use Customize Form to set levels on fields."
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:394
+#: frappe/custom/doctype/customize_form/customize_form.py:395
msgid "You can't set 'Options' for field {0}"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:398
+#: frappe/custom/doctype/customize_form/customize_form.py:399
msgid "You can't set 'Translatable' for field {0}"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:405
+#: frappe/custom/doctype/customize_form/customize_form.py:406
msgid "You can't set standard field {0} as virtual"
msgstr ""
@@ -31319,15 +31446,15 @@ msgctxt "Form timeline"
msgid "You cancelled this document {1}"
msgstr ""
-#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:417
+#: frappe/desk/doctype/dashboard_chart/dashboard_chart.py:419
msgid "You cannot create a dashboard chart from single DocTypes"
msgstr ""
-#: frappe/share.py:241
+#: frappe/share.py:243
msgid "You cannot share `{0}` on {1} `{2}` as you do not have `{0}` permission on `{1}`"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:390
+#: frappe/custom/doctype/customize_form/customize_form.py:391
msgid "You cannot unset 'Read Only' for field {0}"
msgstr ""
@@ -31373,22 +31500,30 @@ msgstr ""
msgid "You do not have import permission for {0}"
msgstr ""
-#: frappe/database/query.py:976
+#: frappe/database/query.py:978
msgid "You do not have permission to access child table field: {0}"
msgstr ""
-#: frappe/database/query.py:989
+#: frappe/database/query.py:991
msgid "You do not have permission to access field: {0}"
msgstr ""
-#: frappe/core/doctype/file/file.py:229
+#: frappe/core/doctype/file/file.py:232
msgid "You do not have permission to access this file"
msgstr ""
-#: frappe/desk/query_report.py:1106
+#: frappe/desk/query_report.py:1111
msgid "You do not have permission to access {0}: {1}."
msgstr ""
+#: frappe/core/doctype/deleted_document/deleted_document.py:52
+msgid "You do not have permission to create or restore documents of type {0}."
+msgstr ""
+
+#: frappe/core/doctype/deleted_document/deleted_document.py:57
+msgid "You do not have permission to restore this document."
+msgstr ""
+
#: frappe/public/js/frappe/form/form.js:1000
msgid "You do not have permissions to cancel all linked documents."
msgstr ""
@@ -31397,19 +31532,31 @@ msgstr ""
msgid "You don't have access to Report: {0}"
msgstr ""
-#: frappe/website/doctype/web_form/web_form.py:855
+#: frappe/website/doctype/web_form/web_form.py:877
msgid "You don't have permission to access the {0} DocType."
msgstr ""
-#: frappe/utils/response.py:291 frappe/utils/response.py:295
+#: frappe/utils/response.py:299 frappe/utils/response.py:303
msgid "You don't have permission to access this file"
msgstr ""
+#: frappe/desk/reportview.py:337
+msgid "You don't have permission to create Report records."
+msgstr ""
+
+#: frappe/desk/reportview.py:340
+msgid "You don't have permission to create report for {0}"
+msgstr ""
+
#: frappe/desk/query_report.py:51
msgid "You don't have permission to get a report on: {0}"
msgstr ""
-#: frappe/website/doctype/web_form/web_form.py:191
+#: frappe/utils/file_manager.py:411
+msgid "You don't have permission to read/attach the file {0}."
+msgstr ""
+
+#: frappe/website/doctype/web_form/web_form.py:192
msgid "You don't have the permissions to access this document"
msgstr ""
@@ -31421,11 +31568,12 @@ msgstr ""
msgid "You have been successfully logged out"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:248
+#: frappe/custom/doctype/customize_form/customize_form.py:249
msgid "You have hit the row size limit on database table: {0}"
msgstr ""
-#: frappe/public/js/frappe/list/bulk_operations.js:428
+#: frappe/public/js/frappe/form/grid.js:1212
+#: frappe/public/js/frappe/list/bulk_operations.js:439
msgid "You have not entered a value. The field will be set to empty."
msgstr ""
@@ -31461,11 +31609,11 @@ msgstr ""
msgid "You must add atleast one link."
msgstr ""
-#: frappe/website/doctype/web_form/web_form.py:851
+#: frappe/website/doctype/web_form/web_form.py:873
msgid "You must be logged in to use this form."
msgstr ""
-#: frappe/website/doctype/web_form/web_form.py:692
+#: frappe/website/doctype/web_form/web_form.py:696
msgid "You must login to submit this form"
msgstr ""
@@ -31486,11 +31634,11 @@ msgstr ""
msgid "You need to be a system user to access this page."
msgstr ""
-#: frappe/website/doctype/web_form/web_form.py:93
+#: frappe/website/doctype/web_form/web_form.py:94
msgid "You need to be in developer mode to edit a Standard Web Form"
msgstr ""
-#: frappe/utils/response.py:280
+#: frappe/utils/response.py:281
msgid "You need to be logged in and have System Manager Role to be able to access backups."
msgstr ""
@@ -31498,7 +31646,7 @@ msgstr ""
msgid "You need to be logged in to access this page"
msgstr ""
-#: frappe/website/doctype/web_form/web_form.py:180
+#: frappe/website/doctype/web_form/web_form.py:181
msgid "You need to be logged in to access this {0}."
msgstr ""
@@ -31518,7 +31666,7 @@ msgstr ""
msgid "You need to have \"Share\" permission"
msgstr ""
-#: frappe/utils/print_format.py:322
+#: frappe/utils/print_format.py:326
msgid "You need to install pycups to use this feature!"
msgstr ""
@@ -31530,14 +31678,18 @@ msgstr ""
msgid "You need to set one IMAP folder for {0}"
msgstr ""
-#: frappe/model/rename_doc.py:391
+#: frappe/model/rename_doc.py:380
msgid "You need write permission on {0} {1} to merge"
msgstr ""
-#: frappe/model/rename_doc.py:386
+#: frappe/model/rename_doc.py:375
msgid "You need write permission on {0} {1} to rename"
msgstr ""
+#: frappe/utils/file_manager.py:402
+msgid "You need write permissions to add attachments to this record."
+msgstr ""
+
#: frappe/client.py:501
msgid "You need {0} permission to fetch values from {1} {2}"
msgstr ""
@@ -31601,10 +31753,6 @@ msgstr ""
msgid "You've logged in as another user from another tab. Refresh this page to continue using system."
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/about.js:54
-msgid "YouTube"
-msgstr ""
-
#: frappe/core/doctype/prepared_report/prepared_report.js:57
msgid "Your CSV file is being generated and will appear in the Attachments section once ready. Additionally, you will get notified when the file is available for download."
msgstr ""
@@ -31638,7 +31786,7 @@ msgstr ""
msgid "Your account has been locked and will resume after {0} seconds"
msgstr ""
-#: frappe/desk/form/assign_to.py:279
+#: frappe/desk/form/assign_to.py:287
msgid "Your assignment on {0} {1} has been removed by {2}"
msgstr ""
@@ -31662,7 +31810,7 @@ msgstr ""
msgid "Your exported report: {0}"
msgstr ""
-#: frappe/public/js/frappe/web_form/web_form.js:450
+#: frappe/public/js/frappe/web_form/web_form.js:453
msgid "Your form has been successfully updated"
msgstr ""
@@ -31692,7 +31840,7 @@ msgstr ""
msgid "Your organization name and address for the email footer."
msgstr ""
-#: frappe/core/doctype/user/user.py:396
+#: frappe/core/doctype/user/user.py:400
msgid "Your password has been changed and you might have been logged out of all systems. Please contact the Administrator for further assistance."
msgstr ""
@@ -31700,7 +31848,7 @@ msgstr ""
msgid "Your query has been received. We will reply back shortly. If you have any additional information, please reply to this mail."
msgstr ""
-#: frappe/desk/query_report.py:352 frappe/desk/reportview.py:399
+#: frappe/desk/query_report.py:357 frappe/desk/reportview.py:406
msgid "Your report is being generated in the background. You will receive an email on {0} with a download link once it is ready."
msgstr ""
@@ -31712,7 +31860,7 @@ msgstr ""
msgid "Your verification code is {0}"
msgstr ""
-#: frappe/utils/data.py:1557
+#: frappe/utils/data.py:1561
msgid "Zero"
msgstr ""
@@ -31749,10 +31897,14 @@ msgstr ""
msgid "amend"
msgstr ""
-#: frappe/public/js/frappe/utils/utils.js:396 frappe/utils/data.py:1570
+#: frappe/public/js/frappe/utils/utils.js:396 frappe/utils/data.py:1574
msgid "and"
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/search.js:858
+msgid "and {0} more"
+msgstr ""
+
#: frappe/public/js/frappe/ui/sort_selector.html:5
#: frappe/public/js/frappe/ui/sort_selector.js:48
msgid "ascending"
@@ -31800,7 +31952,7 @@ msgstr ""
msgid "clear"
msgstr ""
-#: frappe/public/js/frappe/form/templates/timeline_message_box.html:34
+#: frappe/public/js/frappe/form/templates/timeline_message_box.html:35
msgid "commented"
msgstr ""
@@ -31879,8 +32031,8 @@ msgstr ""
msgid "descending"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:229
-msgid "document type..., e.g. customer"
+#: frappe/public/js/frappe/ui/toolbar/search.js:134
+msgid "e.g."
msgstr ""
#. Description of the 'Email Account Name' (Data) field in DocType 'Email
@@ -31889,10 +32041,6 @@ msgstr ""
msgid "e.g. \"Support\", \"Sales\", \"Jerry Yang\""
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:234
-msgid "e.g. (55 + 434) / 4"
-msgstr ""
-
#. Description of the 'Incoming Server' (Data) field in DocType 'Email Account'
#. Description of the 'Incoming Server' (Data) field in DocType 'Email Domain'
#: frappe/email/doctype/email_account/email_account.json
@@ -31972,6 +32120,10 @@ msgstr ""
msgid "finished"
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/search.js:1118
+msgid "found"
+msgstr ""
+
#. Option for the 'Background Color' (Select) field in DocType 'Desktop Icon'
#. Option for the 'Indicator Color' (Select) field in DocType 'Workspace'
#: frappe/desk/doctype/desktop_icon/desktop_icon.json
@@ -32036,7 +32188,7 @@ msgstr ""
msgid "just now"
msgstr ""
-#: frappe/desk/desktop.py:254 frappe/desk/query_report.py:301
+#: frappe/desk/desktop.py:251 frappe/desk/query_report.py:306
msgid "label"
msgstr ""
@@ -32072,7 +32224,7 @@ msgctxt "Minutes (Field: Duration)"
msgid "m"
msgstr ""
-#: frappe/model/rename_doc.py:215
+#: frappe/model/rename_doc.py:204
msgid "merged {0} into {1}"
msgstr ""
@@ -32090,18 +32242,10 @@ msgstr ""
msgid "mm/dd/yyyy"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:232
-msgid "module name..."
-msgstr ""
-
#: frappe/public/js/frappe/ui/toolbar/search_utils.js:171
msgid "new"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:228
-msgid "new type of document"
-msgstr ""
-
#. Label of the no_failed (Int) field in DocType 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "no failed attempts"
@@ -32213,7 +32357,7 @@ msgstr ""
msgid "red"
msgstr ""
-#: frappe/model/rename_doc.py:217
+#: frappe/model/rename_doc.py:206
msgid "renamed from {0} to {1}"
msgstr ""
@@ -32228,10 +32372,30 @@ msgstr ""
msgid "response"
msgstr ""
-#: frappe/core/doctype/deleted_document/deleted_document.py:61
+#: frappe/core/doctype/deleted_document/deleted_document.py:89
msgid "restored {0} as {1}"
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/search.js:951
+#: frappe/public/js/frappe/ui/toolbar/search.js:1097
+#: frappe/public/js/frappe/ui/toolbar/search.js:1116
+msgid "result"
+msgstr ""
+
+#: frappe/public/js/frappe/ui/toolbar/search.js:951
+#: frappe/public/js/frappe/ui/toolbar/search.js:1097
+#: frappe/public/js/frappe/ui/toolbar/search.js:1116
+msgid "results"
+msgstr ""
+
+#: frappe/public/js/frappe/form/grid.js:1157
+msgid "row"
+msgstr ""
+
+#: frappe/public/js/frappe/form/grid.js:1157
+msgid "rows"
+msgstr ""
+
#: frappe/public/js/frappe/form/controls/duration.js:222
#: frappe/public/js/frappe/utils/utils.js:1204
msgctxt "Seconds (Field: Duration)"
@@ -32321,14 +32485,6 @@ msgstr ""
msgid "submit"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:231
-msgid "tag name..., e.g. #tag"
-msgstr ""
-
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:230
-msgid "text in document type"
-msgstr ""
-
#: frappe/public/js/frappe/form/controls/data.js:36
msgid "this form"
msgstr ""
@@ -32345,6 +32501,14 @@ msgstr ""
msgid "to navigate"
msgstr ""
+#: frappe/public/js/frappe/ui/toolbar/search.js:138
+msgid "to open Awesome Bar"
+msgstr ""
+
+#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:57
+msgid "to open Global Search"
+msgstr ""
+
#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:49
msgid "to select"
msgstr ""
@@ -32376,7 +32540,7 @@ msgstr ""
msgid "version_table"
msgstr ""
-#: frappe/automation/doctype/assignment_rule/assignment_rule.py:382
+#: frappe/automation/doctype/assignment_rule/assignment_rule.py:411
msgid "via Assignment Rule"
msgstr ""
@@ -32460,7 +32624,7 @@ msgstr ""
msgid "yyyy-mm-dd"
msgstr ""
-#: frappe/desk/doctype/event/event.js:87
+#: frappe/desk/doctype/event/event.js:85
#: frappe/public/js/frappe/form/footer/form_timeline.js:547
msgid "{0}"
msgstr ""
@@ -32486,8 +32650,8 @@ msgstr ""
msgid "{0} ({1}) - {2}%"
msgstr ""
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:428
-#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:432
+#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:411
+#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:415
msgid "{0} = {1}"
msgstr ""
@@ -32506,7 +32670,7 @@ msgstr ""
msgid "{0} Dashboard"
msgstr ""
-#: frappe/public/js/frappe/form/grid_row.js:472
+#: frappe/public/js/frappe/form/grid_row.js:474
#: frappe/public/js/frappe/list/list_settings.js:230
#: frappe/public/js/frappe/views/kanban/kanban_settings.js:178
msgid "{0} Fields"
@@ -32544,7 +32708,7 @@ msgstr ""
msgid "{0} Name"
msgstr ""
-#: frappe/model/base_document.py:1286
+#: frappe/model/base_document.py:1289
msgid "{0} Not allowed to change {1} after submission from {2} to {3}"
msgstr ""
@@ -32552,7 +32716,7 @@ msgstr ""
msgid "{0} Report"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:997
+#: frappe/public/js/frappe/views/reports/query_report.js:998
msgid "{0} Reports"
msgstr ""
@@ -32593,7 +32757,7 @@ msgstr ""
msgid "{0} already unsubscribed for {1} {2}"
msgstr ""
-#: frappe/utils/data.py:1778
+#: frappe/utils/data.py:1782
msgid "{0} and {1}"
msgstr ""
@@ -32605,7 +32769,7 @@ msgstr ""
msgid "{0} are required"
msgstr ""
-#: frappe/desk/form/assign_to.py:286
+#: frappe/desk/form/assign_to.py:294
msgid "{0} assigned a new task {1} {2} to you"
msgstr ""
@@ -32618,7 +32782,7 @@ msgctxt "Form timeline"
msgid "{0} attached {1}"
msgstr ""
-#: frappe/core/doctype/system_settings/system_settings.py:160
+#: frappe/core/doctype/system_settings/system_settings.py:161
msgid "{0} can not be more than {1}"
msgstr ""
@@ -32660,7 +32824,7 @@ msgctxt "Form timeline"
msgid "{0} changed {1} to {2}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1640
+#: frappe/core/doctype/doctype/doctype.py:1641
msgid "{0} contains an invalid Fetch From expression, Fetch From can't be self-referential."
msgstr ""
@@ -32702,11 +32866,11 @@ msgstr ""
msgid "{0} equals {1}"
msgstr ""
-#: frappe/database/mariadb/schema.py:141 frappe/database/postgres/schema.py:187
+#: frappe/database/mariadb/schema.py:172 frappe/database/postgres/schema.py:258
msgid "{0} field cannot be set as unique in {1}, as there are non-unique existing values"
msgstr ""
-#: frappe/core/doctype/data_import/importer.py:1075
+#: frappe/core/doctype/data_import/importer.py:1081
msgid "{0} format could not be determined from the values in this column. Defaulting to {1}."
msgstr ""
@@ -32726,7 +32890,7 @@ msgstr ""
msgid "{0} has already assigned default value for {1}."
msgstr ""
-#: frappe/database/query.py:1269
+#: frappe/database/query.py:1271
msgid "{0} has invalid backtick notation: {1}"
msgstr ""
@@ -32751,11 +32915,11 @@ msgstr ""
msgid "{0} is a descendant of {1}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:952
+#: frappe/core/doctype/doctype/doctype.py:953
msgid "{0} is a mandatory field"
msgstr ""
-#: frappe/core/doctype/file/file.py:640
+#: frappe/core/doctype/file/file.py:643
msgid "{0} is a not a valid zip file"
msgstr ""
@@ -32767,7 +32931,7 @@ msgstr ""
msgid "{0} is an ancestor of {1}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1653
+#: frappe/core/doctype/doctype/doctype.py:1654
msgid "{0} is an invalid Data field."
msgstr ""
@@ -32892,7 +33056,7 @@ msgstr ""
msgid "{0} is not a valid report format. Report format should one of the following {1}"
msgstr ""
-#: frappe/core/doctype/file/file.py:620
+#: frappe/core/doctype/file/file.py:623
msgid "{0} is not a zip file"
msgstr ""
@@ -32948,6 +33112,10 @@ msgstr ""
msgid "{0} is required"
msgstr ""
+#: frappe/public/js/frappe/form/save.js:196
+msgid "{0} is required."
+msgstr ""
+
#: frappe/public/js/frappe/form/controls/link.js:724
#: frappe/public/js/frappe/views/reports/report_view.js:1585
msgid "{0} is set"
@@ -32966,7 +33134,7 @@ msgstr ""
msgid "{0} items selected"
msgstr ""
-#: frappe/core/doctype/user/user.py:1501
+#: frappe/core/doctype/user/user.py:1505
msgid "{0} just impersonated as you. They gave this reason: {1}"
msgstr ""
@@ -33018,11 +33186,11 @@ msgstr ""
msgid "{0} must be one of {1}"
msgstr ""
-#: frappe/model/base_document.py:1004
+#: frappe/model/base_document.py:1007
msgid "{0} must be set first"
msgstr ""
-#: frappe/model/base_document.py:859
+#: frappe/model/base_document.py:862
msgid "{0} must be unique"
msgstr ""
@@ -33034,15 +33202,15 @@ msgstr ""
msgid "{0} must begin and end with a letter and can only contain letters, hyphen or underscore."
msgstr ""
-#: frappe/workflow/doctype/workflow/workflow.py:91
+#: frappe/workflow/doctype/workflow/workflow.py:92
msgid "{0} not a valid State"
msgstr ""
-#: frappe/model/rename_doc.py:394
+#: frappe/model/rename_doc.py:383
msgid "{0} not allowed to be renamed"
msgstr ""
-#: frappe/core/doctype/report/report.py:463
+#: frappe/core/doctype/report/report.py:486
#: frappe/public/js/frappe/list/list_view.js:1280
msgid "{0} of {1}"
msgstr ""
@@ -33055,12 +33223,12 @@ msgstr ""
msgid "{0} of {1} records match (filtered on visible rows only)"
msgstr ""
-#: frappe/utils/data.py:1579
+#: frappe/utils/data.py:1583
msgctxt "Money in words"
msgid "{0} only."
msgstr ""
-#: frappe/utils/data.py:1760
+#: frappe/utils/data.py:1764
msgid "{0} or {1}"
msgstr ""
@@ -33127,7 +33295,7 @@ msgctxt "User added rows to child table"
msgid "{0} rows to {1}"
msgstr ""
-#: frappe/desk/query_report.py:838
+#: frappe/desk/query_report.py:843
msgid "{0} saved successfully"
msgstr ""
@@ -33135,7 +33303,7 @@ msgstr ""
msgid "{0} self assigned this task: {1}"
msgstr ""
-#: frappe/share.py:257
+#: frappe/share.py:259
msgid "{0} shared a document {1} {2} with you"
msgstr ""
@@ -33147,7 +33315,7 @@ msgstr ""
msgid "{0} shared this document with {1}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:318
+#: frappe/core/doctype/doctype/doctype.py:319
msgid "{0} should be indexed because it's referred in dashboard connections"
msgstr ""
@@ -33183,11 +33351,11 @@ msgstr ""
msgid "{0} un-shared this document with {1}"
msgstr ""
-#: frappe/custom/doctype/customize_form/customize_form.py:257
+#: frappe/custom/doctype/customize_form/customize_form.py:258
msgid "{0} updated"
msgstr ""
-#: frappe/public/js/frappe/form/controls/multiselect_list.js:213
+#: frappe/public/js/frappe/form/controls/multiselect_list.js:237
msgid "{0} values selected"
msgstr ""
@@ -33219,15 +33387,15 @@ msgstr ""
msgid "{0} {1} added"
msgstr ""
-#: frappe/public/js/frappe/utils/dashboard_utils.js:269
+#: frappe/public/js/frappe/utils/dashboard_utils.js:282
msgid "{0} {1} added to Dashboard {2}"
msgstr ""
-#: frappe/model/base_document.py:778 frappe/model/rename_doc.py:110
+#: frappe/model/base_document.py:781 frappe/model/rename_doc.py:99
msgid "{0} {1} already exists"
msgstr ""
-#: frappe/model/base_document.py:1115
+#: frappe/model/base_document.py:1118
msgid "{0} {1} cannot be \"{2}\". It should be one of \"{3}\""
msgstr ""
@@ -33235,7 +33403,7 @@ msgstr ""
msgid "{0} {1} cannot be a leaf node as it has children"
msgstr ""
-#: frappe/model/rename_doc.py:376
+#: frappe/model/rename_doc.py:365
msgid "{0} {1} does not exist, select a new target to merge"
msgstr ""
@@ -33251,11 +33419,11 @@ msgstr ""
msgid "{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first."
msgstr ""
-#: frappe/model/base_document.py:1247
+#: frappe/model/base_document.py:1250
msgid "{0}, Row {1}"
msgstr ""
-#: frappe/utils/data.py:1578
+#: frappe/utils/data.py:1582
msgctxt "Money in words"
msgid "{0}."
msgstr ""
@@ -33264,7 +33432,7 @@ msgstr ""
msgid "{0}/{1} complete | Please leave this tab open until completion."
msgstr ""
-#: frappe/model/base_document.py:1252
+#: frappe/model/base_document.py:1255
msgid "{0}: '{1}' ({3}) will get truncated, as max characters allowed is {2}"
msgstr ""
@@ -33300,24 +33468,24 @@ msgstr ""
msgid "{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1461
+#: frappe/core/doctype/doctype/doctype.py:1462
msgid "{0}: Field '{1}' cannot be set as Unique as it has non-unique values"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1369
-#: frappe/website/doctype/web_form/web_form.py:120
+#: frappe/core/doctype/doctype/doctype.py:1370
+#: frappe/website/doctype/web_form/web_form.py:121
msgid "{0}: Field {1} in row {2} cannot be hidden and mandatory without default"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1328
+#: frappe/core/doctype/doctype/doctype.py:1329
msgid "{0}: Field {1} of type {2} cannot be mandatory"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1316
+#: frappe/core/doctype/doctype/doctype.py:1317
msgid "{0}: Fieldname {1} appears multiple times in rows {2}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1448
+#: frappe/core/doctype/doctype/doctype.py:1449
msgid "{0}: Fieldtype {1} for {2} cannot be unique"
msgstr ""
@@ -33329,15 +33497,15 @@ msgstr ""
msgid "{0}: Only one rule allowed with the same Role, Level and {1}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1350
+#: frappe/core/doctype/doctype/doctype.py:1351
msgid "{0}: Options must be a valid DocType for field {1} in row {2}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1339
+#: frappe/core/doctype/doctype/doctype.py:1340
msgid "{0}: Options required for Link or Table type field {1} in row {2}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1357
+#: frappe/core/doctype/doctype/doctype.py:1358
msgid "{0}: Options {1} must be the same as doctype name {2} for the field {3}"
msgstr ""
@@ -33353,11 +33521,11 @@ msgstr ""
msgid "{0}: You can increase the limit for the field if required via {1}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1303
+#: frappe/core/doctype/doctype/doctype.py:1304
msgid "{0}: fieldname cannot be set to reserved field {1} in DocType"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1294
+#: frappe/core/doctype/doctype/doctype.py:1295
msgid "{0}: fieldname cannot be set to reserved keyword {1}"
msgstr ""
@@ -33370,11 +33538,11 @@ msgstr ""
msgid "{0}: {1} is set to state {2}"
msgstr ""
-#: frappe/public/js/frappe/views/reports/query_report.js:1343
+#: frappe/public/js/frappe/views/reports/query_report.js:1344
msgid "{0}: {1} vs {2}"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1469
+#: frappe/core/doctype/doctype/doctype.py:1470
msgid "{0}:Fieldtype {1} for {2} cannot be indexed"
msgstr ""
@@ -33398,7 +33566,7 @@ msgstr ""
msgid "{count} rows selected"
msgstr ""
-#: frappe/core/doctype/doctype/doctype.py:1523
+#: frappe/core/doctype/doctype/doctype.py:1524
msgid "{{{0}}} is not a valid fieldname pattern. It should be {{field_name}}."
msgstr ""
@@ -33406,11 +33574,11 @@ msgstr ""
msgid "{} Complete"
msgstr ""
-#: frappe/utils/data.py:2639
+#: frappe/utils/data.py:2643
msgid "{} Invalid python code on line {}"
msgstr ""
-#: frappe/utils/data.py:2648
+#: frappe/utils/data.py:2652
msgid "{} Possibly invalid python code. {}"
msgstr ""
diff --git a/frappe/migrate.py b/frappe/migrate.py
index 87287408bc5e..d2f4ea782a2b 100644
--- a/frappe/migrate.py
+++ b/frappe/migrate.py
@@ -160,6 +160,12 @@ def post_schema_updates(self):
"""
print("Syncing jobs...")
sync_jobs()
+
+ from frappe.database.sequence import create_missing_sequences
+
+ if recreated := create_missing_sequences():
+ print(f"Recreated missing sequences for: {', '.join(recreated)}")
+
if not self.skip_fixtures:
print("Syncing fixtures...")
sync_fixtures()
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index f268e314ba66..59eb2aa9ec33 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -552,6 +552,9 @@ def get_valid_dict(
elif fieldtype in float_like_fields and not isinstance(value, float):
value = flt(value)
+ elif fieldtype == "Read Only" and not isinstance(value, str):
+ value = cstr(value)
+
elif (fieldtype in datetime_fields and value == "") or (
getattr(df, "unique", False) and cstr(value).strip() == ""
):
@@ -618,7 +621,7 @@ def get_valid_columns(self) -> list[str]:
return valid_columns_cache[self.doctype]
def is_new(self) -> bool:
- return self.get("__islocal")
+ return bool(self.get("__islocal"))
@property
def docstatus(self) -> DocStatus:
@@ -1573,7 +1576,9 @@ def _filter(data, filters, limit=None):
return out
-CACHED_PROPERTIES = (prop for prop, value in vars(BaseDocument).items() if isinstance(value, cached_property))
+CACHED_PROPERTIES = tuple(
+ prop for prop, value in vars(BaseDocument).items() if isinstance(value, cached_property)
+)
UNPICKLABLE_KEYS = frozenset(
(
diff --git a/frappe/model/document.py b/frappe/model/document.py
index c8ce1821a3ca..f5ad926d2964 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -1,11 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
+import functools
import hashlib
+import inspect
import itertools
import json
import time
import warnings
-from collections.abc import Generator, Iterable
+from collections.abc import Callable, Generator, Iterable
from contextlib import contextmanager
from functools import wraps
from types import MappingProxyType
@@ -40,6 +42,23 @@
DOCUMENT_LOCK_EXPIRY = 3 * 60 * 60 # All locks expire in 3 hours automatically
DOCUMENT_LOCK_SOFT_EXPIRY = 30 * 60 # Let users force-unlock after 30 minutes
+_POSITIONAL_PARAM_KINDS = frozenset(
+ (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
+)
+
+
+@functools.cache
+def _accepts_method_argument(f: Callable) -> bool:
+ """Return True if the doc event handler expects the `method` argument."""
+ signature = inspect.signature(f)
+ kinds = [p.kind for p in signature.parameters.values()]
+ if any(kind == inspect.Parameter.VAR_POSITIONAL for kind in kinds):
+ return True
+
+ if sum(1 for kind in kinds if kind in _POSITIONAL_PARAM_KINDS) > 1:
+ return True
+
+ return False
type _SingleDocument = "Document"
@@ -1560,7 +1579,11 @@ def runner(self, method, *args, **kwargs):
for f in hooks:
try:
frappe.db._disable_transaction_control += 1
- add_to_return_value(self, f(self, method, *args, **kwargs))
+ # Allow handlers to be defined without method arg, e.g. `handler(doc)`
+ if not args and not _accepts_method_argument(f):
+ add_to_return_value(self, f(self, **kwargs))
+ else:
+ add_to_return_value(self, f(self, method, *args, **kwargs))
finally:
frappe.db._disable_transaction_control -= 1
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index d564deb4e705..44ac692770e4 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -1022,7 +1022,7 @@ def _update_field_order_based_on_insert_after(field_order, insert_after_map):
def _serialize(doc, no_nulls=False, *, is_child=False):
- out = {}
+ out = frappe._dict()
for key, value in doc.__dict__.items():
if not is_child:
if key in CACHE_PROPERTIES:
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index 5e1f6e427639..6f7b818c08fb 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -75,17 +75,6 @@ def update_document_title(
transformed_name = transformed_name.get("new")
transformed_name = transformed_name or updated_name
- # run rename validations before queueing
- # use savepoints to avoid partial renames / commits
- validate_rename(
- doctype=doctype,
- old=current_name,
- new=transformed_name,
- meta=doc.meta,
- merge=merge,
- save_point=True,
- )
-
doc.queue_action("rename", name=transformed_name, merge=merge, queue=queue, timeout=36000)
else:
doc.rename(updated_name, merge=merge)
diff --git a/frappe/model/sync.py b/frappe/model/sync.py
index 3fe383f162b8..453d53515778 100644
--- a/frappe/model/sync.py
+++ b/frappe/model/sync.py
@@ -199,8 +199,8 @@ def remove_orphan_doctypes():
print()
-def remove_orphan_entities():
- entites = ["Workspace", "Dashboard", "Page", "Report", "Notification"]
+def remove_orphan_entities(entity_types=None):
+ entities = ["Workspace", "Dashboard", "Page", "Report", "Notification"]
app_level_entities = ["Workspace Sidebar", "Desktop Icon"]
entity_filter_map = {
"Workspace": [{"public": 1, "module": ["is", "set"], "app": ["is", "set"]}],
@@ -211,9 +211,14 @@ def remove_orphan_entities():
"Desktop Icon": {"standard": True},
"Notification": {"is_standard": True},
}
- entity_file_map = create_entity_file_map(entites)
+ entity_file_map = create_entity_file_map(entities)
+ if entity_types:
+ if isinstance(entity_types, list):
+ entities = entity_types
+ else:
+ entities = [entity_types]
- for entity in entites:
+ for entity in entities:
print(f"Removing orphan {entity}s")
all_enitities = frappe.get_all(
entity, filters=entity_filter_map.get(entity), fields=["name", "module"]
@@ -234,6 +239,8 @@ def remove_orphan_entities():
# save the deleted icons
frappe.db.commit() # nosemgrep
# Remove app level entities
+ if entity_types and not set(entity_types).issubset(set(app_level_entities)):
+ return
for app_entity in app_level_entities:
print(f"Removing orphan {app_entity}s")
all_enitities = frappe.get_all(
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 5f7dc2f3ee41..e0dc3ed9dfbc 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -191,7 +191,6 @@ frappe.patches.v14_0.setup_likes_from_feedback
frappe.patches.v14_0.update_webforms
frappe.patches.v14_0.delete_payment_gateways
frappe.patches.v15_0.remove_event_streaming
-frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report
execute:frappe.reload_doc("desk", "doctype", "Form Tour")
execute:frappe.delete_doc('Page', 'recorder', ignore_missing=True, force=True)
frappe.patches.v14_0.modify_value_column_size_for_singles
@@ -224,6 +223,7 @@ execute:frappe.delete_doc("Page", "activity", force=1)
frappe.patches.v14_0.disable_email_accounts_with_oauth
execute:frappe.delete_doc("Page", "translation-tool", force=1)
frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings
+frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report
frappe.patches.v14_0.remove_manage_subscriptions_from_navbar
frappe.patches.v15_0.remove_background_jobs_from_dropdown
frappe.desk.doctype.form_tour.patches.introduce_ui_tours
@@ -260,3 +260,4 @@ execute:from frappe.email.doctype.notification.notification import install_notif
execute:frappe.db.set_value("Email Account", {}, "add_x_original_from", 1)
execute:frappe.db.set_value("Email Account", {}, "add_reply_to_header", 1)
frappe.patches.v16_0.set_reply_to_header
+frappe.patches.v16_0.sync_installed_apps_to_site_config
diff --git a/frappe/patches/v15_0/copy_disable_prepared_report_to_prepared_report.py b/frappe/patches/v15_0/copy_disable_prepared_report_to_prepared_report.py
index f1e4afb9a401..1c3d76241e32 100644
--- a/frappe/patches/v15_0/copy_disable_prepared_report_to_prepared_report.py
+++ b/frappe/patches/v15_0/copy_disable_prepared_report_to_prepared_report.py
@@ -3,4 +3,6 @@
def execute():
table = frappe.qb.DocType("Report")
- frappe.qb.update(table).set(table.prepared_report, 0).where(table.disable_prepared_report == 1)
+ frappe.qb.update(table).set(table.prepared_report, 0).set(
+ table.disable_prepared_report_automation, 1
+ ).where(table.disable_prepared_report == 1).run()
diff --git a/frappe/patches/v16_0/auto_generate_desktop_icon_and_sidebar.py b/frappe/patches/v16_0/auto_generate_desktop_icon_and_sidebar.py
index 794438375aae..a0bdbded4ff1 100644
--- a/frappe/patches/v16_0/auto_generate_desktop_icon_and_sidebar.py
+++ b/frappe/patches/v16_0/auto_generate_desktop_icon_and_sidebar.py
@@ -1,6 +1,8 @@
+from frappe.model.sync import remove_orphan_entities
from frappe.utils.install import auto_generate_icons_and_sidebar
def execute():
"""Auto Create desktop icons and workspace sidebars."""
+ remove_orphan_entities("Workspace")
auto_generate_icons_and_sidebar()
diff --git a/frappe/patches/v16_0/sync_installed_apps_to_site_config.py b/frappe/patches/v16_0/sync_installed_apps_to_site_config.py
new file mode 100644
index 000000000000..8307799be825
--- /dev/null
+++ b/frappe/patches/v16_0/sync_installed_apps_to_site_config.py
@@ -0,0 +1,12 @@
+import frappe
+from frappe.installer import update_site_config
+
+
+def execute():
+ """Backfill installed_apps into site_config.json for existing sites.
+
+ bench-cli (and other tools) can read the list without a DB round-trip by
+ checking site_config.json first. Going forward, install_app / remove_app
+ keep this key in sync automatically.
+ """
+ update_site_config("installed_apps", frappe.get_installed_apps())
diff --git a/frappe/permissions.py b/frappe/permissions.py
index d719bfe6dded..65ab460c76bb 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -498,8 +498,8 @@ def has_controller_permissions(doc, ptype, user=None, debug=False) -> bool:
return True
-def get_doctypes_with_read():
- return list({cstr(p.parent) for p in get_valid_perms() if p.parent and p.read})
+def get_doctypes_with_read(user: str | None = None):
+ return list({cstr(p.parent) for p in get_valid_perms(user=user) if p.parent and p.read})
def get_valid_perms(doctype=None, user=None):
@@ -938,3 +938,10 @@ def _get_parent_and_ancestors(doctype, parent):
from frappe.utils.nestedset import get_ancestors_of
yield from get_ancestors_of(doctype, parent)
+
+
+def check_app_permission():
+ is_system_manager = "System Manager" in frappe.get_roles(frappe.session.user)
+ if is_system_user() and is_system_manager:
+ return True
+ return False
diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.json b/frappe/printing/doctype/network_printer_settings/network_printer_settings.json
index 87cdc1c2bfde..469eb6f9fda5 100644
--- a/frappe/printing/doctype/network_printer_settings/network_printer_settings.json
+++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.json
@@ -41,7 +41,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-03-23 16:03:30.772015",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Printing",
"name": "Network Printer Settings",
@@ -59,15 +59,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Desk User",
- "share": 1
}
],
"sort_field": "creation",
diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json
index fa4b9313d330..a22461b805e0 100644
--- a/frappe/printing/doctype/print_format/print_format.json
+++ b/frappe/printing/doctype/print_format/print_format.json
@@ -294,7 +294,7 @@
"icon": "fa fa-print",
"idx": 1,
"links": [],
- "modified": "2026-02-11 13:17:55.662780",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Format",
@@ -313,8 +313,8 @@
"write": 1
},
{
- "role": "Desk User",
- "select": 1
+ "read": 1,
+ "role": "Desk User"
}
],
"row_format": "Dynamic",
diff --git a/frappe/printing/doctype/print_heading/print_heading.json b/frappe/printing/doctype/print_heading/print_heading.json
index 07177a838835..4b8079958895 100644
--- a/frappe/printing/doctype/print_heading/print_heading.json
+++ b/frappe/printing/doctype/print_heading/print_heading.json
@@ -37,7 +37,7 @@
"icon": "fa fa-font",
"idx": 1,
"links": [],
- "modified": "2025-06-26 05:40:55.559700",
+ "modified": "2026-05-28",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Heading",
@@ -54,10 +54,6 @@
"role": "System Manager",
"share": 1,
"write": 1
- },
- {
- "read": 1,
- "role": "Desk User"
}
],
"quick_entry": 1,
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index e0d021ce3e59..93aae10cac15 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -25,7 +25,6 @@ frappe.pages["print"].on_page_load = function (wrapper) {
: frappe.route_options.frm.frm;
frappe.route_options.frm = null;
let meta = print_view.frm.meta;
- meta.module && frappe.app.sidebar.show_sidebar_for_module(meta.module);
print_view.show(print_view.frm);
}
});
diff --git a/frappe/public/images/frappe-comp-logo.svg b/frappe/public/images/frappe-comp-logo.svg
new file mode 100644
index 000000000000..28e90fccba5f
--- /dev/null
+++ b/frappe/public/images/frappe-comp-logo.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frappe/public/js/billing.bundle.js b/frappe/public/js/billing.bundle.js
index 824f1c12e195..86a13fc8b806 100644
--- a/frappe/public/js/billing.bundle.js
+++ b/frappe/public/js/billing.bundle.js
@@ -1,12 +1,12 @@
let frappeCloudBaseEndpoint = "https://frappecloud.com";
-let isFCUser = false;
+let isFCUser = true;
$(document).ready(function () {
const site_info = frappe.boot.site_info;
if (site_info) {
const trial_end_date = new Date(site_info.trial_end_date);
frappeCloudBaseEndpoint = site_info.base_url;
- isFCUser = site_info.is_fc_user;
+ // isFCUser = site_info.is_fc_user;
const today = new Date();
const diffTime = trial_end_date - today;
@@ -33,7 +33,8 @@ $(document).ready(function () {
!frappe.is_mobile() &&
frappe.user.has_role("System Manager");
if (visiblity_condition && isFCUser) {
- if (site_info.trial_end_date && trial_end_date > new Date()) {
+ let chat_bubble_visiblity = false;
+ if (chat_bubble_visiblity && site_info.trial_end_date && trial_end_date > new Date()) {
addChatBubble();
toggleChatBubble(true);
}
diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js
index 9d9a24d4a95b..6b132dca9785 100644
--- a/frappe/public/js/frappe/data_import/import_preview.js
+++ b/frappe/public/js/frappe/data_import/import_preview.js
@@ -69,12 +69,14 @@ frappe.data_import.ImportPreview = class ImportPreview {
column_width += 50;
}
let column_title = `
- ${col.header_title || `${__("Untitled Column")} `}
+ ${frappe.utils.escape_html(col.header_title) || `${__("Untitled Column")} `}
${!col.df ? show_warnings_button : ""}
`;
return {
id: frappe.utils.get_random(6),
- name: col.header_title || (df ? df.label : "Untitled Column"),
+ name:
+ frappe.utils.escape_html(col.header_title) ||
+ (df ? df.label : "Untitled Column"),
content: column_title,
skip_import: true,
editable: false,
@@ -98,13 +100,13 @@ frappe.data_import.ImportPreview = class ImportPreview {
: null;
let column_title = `
- ${col.header_title || df.label}
+ ${frappe.utils.escape_html(col.header_title) || df.label}
${date_format ? `(${date_format})` : ""}
`;
return {
id: df.fieldname,
- name: col.header_title,
+ name: frappe.utils.escape_html(col.header_title),
content: column_title,
df: df,
editable: false,
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index d1804fb5c32d..37f16fed3b33 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -93,7 +93,7 @@ frappe.Application = class Application {
setup_theme() {
frappe.ui.keys.add_shortcut({
shortcut: "shift+ctrl+g",
- description: __("Switch Theme"),
+ description: __("Switch theme"),
action: () => {
if (frappe.theme_switcher && frappe.theme_switcher.dialog.is_visible) {
frappe.theme_switcher.hide();
@@ -459,7 +459,7 @@ frappe.Application = class Application {
}
show_update_available() {
- if (!frappe.boot.has_app_updates) return;
+ if (!frappe.boot.has_app_updates || !frappe.boot.setup_complete) return;
frappe.xcall("frappe.utils.change_log.show_update_popup");
}
diff --git a/frappe/public/js/frappe/file_uploader/file_uploader.bundle.js b/frappe/public/js/frappe/file_uploader/file_uploader.bundle.js
index 44e9d9add633..d00ec4954b24 100644
--- a/frappe/public/js/frappe/file_uploader/file_uploader.bundle.js
+++ b/frappe/public/js/frappe/file_uploader/file_uploader.bundle.js
@@ -28,7 +28,7 @@ class FileUploader {
allow_toggle_optimize,
allow_google_drive,
} = {}) {
- frm && frm.attachments.max_reached(true);
+ frm?.attachments?.max_reached?.(true);
if (allow_toggle_private === undefined) {
allow_toggle_private = true;
diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js
index b7fb99072e68..35e16d6d8ef2 100644
--- a/frappe/public/js/frappe/form/controls/attach.js
+++ b/frappe/public/js/frappe/form/controls/attach.js
@@ -125,8 +125,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro
.attr("href", dataurl || this.value);
}
} else {
- this.$input.toggle(true);
- this.$value.toggle(false);
+ this.$input?.toggle(true);
+ this.$value?.toggle(false);
}
}
diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js
index 01eed9157250..6819ae4bf6c9 100644
--- a/frappe/public/js/frappe/form/controls/autocomplete.js
+++ b/frappe/public/js/frappe/form/controls/autocomplete.js
@@ -40,7 +40,7 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
get_awesomplete_settings() {
var me = this;
return {
- tabSelect: true,
+ tabSelect: false,
minChars: 0,
maxItems: this.df.max_items || 99,
autoFirst: true,
diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js
index 733749f20972..0374b9db601d 100644
--- a/frappe/public/js/frappe/form/controls/link.js
+++ b/frappe/public/js/frappe/form/controls/link.js
@@ -375,7 +375,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
this.$input.on("awesomplete-selectcomplete", function (e) {
let o = e.originalEvent;
- if (o.text.value.indexOf("__link_option") !== -1) {
+ if (cstr(o.text.value).indexOf("__link_option") !== -1) {
me.$input.val("");
}
});
@@ -850,9 +850,13 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
}
apply_link_field_filters() {
- let link_filters = JSON.parse(this.df.link_filters);
- let filters = this.parse_filters(link_filters);
+ let filters = this.parse_filters(JSON.parse(this.df.link_filters));
// take filters from the link field and add to the query
+ const query_filters = this.get_query?.()?.filters || {};
+ if (query_filters) {
+ filters = { ...filters, ...query_filters };
+ }
+
this.get_query = function () {
return {
filters,
diff --git a/frappe/public/js/frappe/form/controls/multiselect_list.js b/frappe/public/js/frappe/form/controls/multiselect_list.js
index f77dddec5d70..88c824c7de33 100644
--- a/frappe/public/js/frappe/form/controls/multiselect_list.js
+++ b/frappe/public/js/frappe/form/controls/multiselect_list.js
@@ -49,6 +49,30 @@ frappe.ui.form.ControlMultiSelectList = class ControlMultiSelectList extends (
let $target = $(e.currentTarget);
this.toggle_select_item($target);
});
+
+ // open dropdown on tab focus
+ const $toggle = this.$list_wrapper.find('[data-toggle="dropdown"]');
+ let focus_triggered_by_mouse = false;
+ $toggle.on("mousedown", () => {
+ focus_triggered_by_mouse = true;
+ });
+ $toggle.on("focus", () => {
+ if (focus_triggered_by_mouse) {
+ focus_triggered_by_mouse = false;
+ return;
+ }
+ $toggle.dropdown("show");
+ });
+
+ // prevent input text focus loss when clicking items or buttons
+ this.$list_wrapper.on(
+ "mousedown",
+ ".selectable-item, .select-all-options, .clear-selections",
+ (e) => {
+ e.preventDefault();
+ }
+ );
+
this.$list_wrapper.on(
"input",
"input",
diff --git a/frappe/public/js/frappe/form/controls/multiselect_pills.js b/frappe/public/js/frappe/form/controls/multiselect_pills.js
index 5bc89e3a8c82..19637becf803 100644
--- a/frappe/public/js/frappe/form/controls/multiselect_pills.js
+++ b/frappe/public/js/frappe/form/controls/multiselect_pills.js
@@ -90,9 +90,9 @@ frappe.ui.form.ControlMultiSelectPills = class ControlMultiSelectPills extends (
const label = this.get_label(value);
const encoded_value = encodeURIComponent(value);
return `
-
+
${__(label || frappe.utils.escape_html(value))}
- ${frappe.utils.icon("close")}
+ ${frappe.utils.icon("x")}
`;
}
diff --git a/frappe/public/js/frappe/form/controls/table_multiselect.js b/frappe/public/js/frappe/form/controls/table_multiselect.js
index 44b4ef432544..f3006857399f 100644
--- a/frappe/public/js/frappe/form/controls/table_multiselect.js
+++ b/frappe/public/js/frappe/form/controls/table_multiselect.js
@@ -31,24 +31,35 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends (
});
this.$input_area.on("click", ".btn-remove", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
const $target = $(e.currentTarget);
const $value = $target.closest(".tb-selected-value");
const value = decodeURIComponent($value.data().value);
const link_field = this.get_link_field();
- const rows = this._get_rows().filter((row) => {
- if (row[link_field.fieldname] !== value) return row;
+ const current_rows = this._get_rows() || [];
+ const removed_row = current_rows.find((row) => row[link_field.fieldname] === value);
+ const rows = current_rows.filter((row) => row[link_field.fieldname] !== value);
+
+ if (!this.frm) {
+ this._update_rows(rows);
+ this.set_model_value(rows);
+ return;
+ }
+ if (removed_row) {
frappe.run_serially([
() => {
return this.frm?.script_manager.trigger(
`before_${this.df.fieldname}_remove`,
this.df.options,
- row.name
+ removed_row.name
);
},
() => {
- frappe.model.clear_doc(this.df.options, row.name);
+ frappe.model.clear_doc(this.df.options, removed_row.name);
this.frm?.dirty();
this.refresh();
@@ -56,11 +67,11 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends (
return this.frm?.script_manager.trigger(
`${this.df.fieldname}_remove`,
this.df.options,
- row.name
+ removed_row.name
);
},
]);
- });
+ }
this._update_rows(rows);
});
this.$input_area.on("click", ".btn-link-to-form", (e) => {
@@ -101,7 +112,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends (
}
const link_field = this.get_link_field();
- value = value?.trim();
+ value = cstr(value).trim();
if (!value) return rows;
// clear input to prevent multiple additions
@@ -189,9 +200,9 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends (
const pill_name = frappe.utils.get_link_title(link_field.options, value) || value;
return `
-
+
${__(frappe.utils.escape_html(pill_name))}
- ${frappe.utils.icon("close")}
+ ${frappe.utils.icon("x")}
`;
}
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index 291e6839809d..621e7f7fb1ec 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -1282,12 +1282,6 @@ frappe.ui.form.Form = class FrappeForm {
show_report_bug_link() {
if (this.meta.beta) {
- this.add_web_link(
- "https://github.com/frappe/" +
- frappe.boot.module_app[frappe.scrub(this.meta.module)] +
- "/issues/new",
- __("Report bug")
- );
}
}
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index 04da43fee146..28148030c635 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -398,6 +398,11 @@ frappe.form.formatters = {
Icon: (value) => {
if (!value) return "";
let escaped_value = frappe.utils.escape_html(value);
+ if (frappe.utils.is_emoji(value)) {
+ return `
+ ${escaped_value}
+
`;
+ }
return `
${frappe.utils.icon(escaped_value, "md")}
${escaped_value}
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index 897af1459d6f..90e19cc8a5ae 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -31,6 +31,10 @@ export default class Grid {
this.meta = frappe.get_meta(this.doctype);
}
this.fields_map = {};
+ // per-grid column visibility overrides set via `set_column_disp`. Kept
+ // grid-local (rather than mutating the shared meta docfield) so two grids
+ // of the same child doctype on the same form don't affect each other.
+ this.column_disp_overrides = {};
this.template = null;
this.multiple_set = false;
if (
@@ -87,6 +91,10 @@ export default class Grid {
data-action="delete_rows">
${__("Delete")}
+
+ ${__("Edit")}
+
${__("Delete all")}
@@ -135,6 +143,7 @@ export default class Grid {
this.grid_buttons = this.wrapper.find(".grid-buttons");
this.grid_custom_buttons = this.wrapper.find(".grid-custom-buttons");
this.remove_rows_button = this.grid_buttons.find(".grid-remove-rows");
+ this.edit_rows_button = this.grid_buttons.find(".grid-edit-rows");
this.duplicate_rows_button = this.grid_buttons.find(".grid-duplicate-rows");
this.remove_all_rows_button = this.grid_buttons.find(".grid-remove-all-rows");
@@ -224,7 +233,7 @@ export default class Grid {
// toggle "Add row" button
this.wrapper
- .find(".grid-add-row")
+ .find(".grid-add-row, .grid-add-multiple-rows")
.toggleClass(
"hidden",
num_selected_rows > 0 ||
@@ -235,13 +244,16 @@ export default class Grid {
// update "Delete" and "Duplicate" button labels
if (num_selected_rows == 1) {
this.remove_rows_button.text(__("Delete row"));
+ this.edit_rows_button.text(__("Edit row"));
this.duplicate_rows_button.text(__("Duplicate row"));
} else {
this.remove_rows_button.text(__("Delete {0} rows", [num_selected_rows]));
+ this.edit_rows_button.text(__("Edit {0} rows", [num_selected_rows]));
this.duplicate_rows_button.text(__("Duplicate {0} rows", [num_selected_rows]));
}
this.refresh_remove_rows_button();
+ this.refresh_edit_rows_button();
this.refresh_duplicate_rows_button();
});
}
@@ -368,6 +380,18 @@ export default class Grid {
}
}
+ refresh_edit_rows_button() {
+ if (!this.meta?.allow_bulk_edit) {
+ this.edit_rows_button.toggleClass("hidden", true);
+ return;
+ }
+
+ const show_button = this.wrapper.find(".grid-body .grid-row-check:checked:first").length
+ ? true
+ : false;
+ this.edit_rows_button.toggleClass("hidden", !show_button);
+ }
+
debounced_refresh_remove_rows_button = frappe.utils.debounce(
this.refresh_remove_rows_button,
100
@@ -390,13 +414,7 @@ export default class Grid {
);
get_selected() {
- return (this.grid_rows || [])
- .map((row) => {
- return row.doc.__checked ? row.doc.name : null;
- })
- .filter((d) => {
- return d;
- });
+ return (this.data || []).filter((doc) => doc.__checked).map((doc) => doc.name);
}
get_selected_children() {
@@ -529,6 +547,7 @@ export default class Grid {
this.form_grid.toggleClass("error", !!(this.df.reqd && !(this.data && this.data.length)));
this.refresh_remove_rows_button();
+ this.refresh_edit_rows_button();
this.refresh_duplicate_rows_button();
this.wrapper.trigger("change");
@@ -686,11 +705,27 @@ export default class Grid {
this.docfields = this.df.fields;
}
+ this._apply_column_disp_overrides();
+
this.docfields.forEach((df) => {
this.fields_map[df.fieldname] = df;
});
}
+ _apply_column_disp_overrides() {
+ const fieldnames = Object.keys(this.column_disp_overrides || {});
+ if (!fieldnames.length) return;
+
+ // Replace overridden fields with a shallow copy carrying the grid-local
+ // `hidden` value. The base docfield comes from `frappe.meta` and is shared
+ // across every grid of the same child doctype on this form, so it must not
+ // be mutated in place.
+ this.docfields = this.docfields.map((df) => {
+ if (!(df.fieldname in this.column_disp_overrides)) return df;
+ return Object.assign({}, df, { hidden: this.column_disp_overrides[df.fieldname] });
+ });
+ }
+
refresh_row(docname) {
this.grid_rows_by_docname[docname] && this.grid_rows_by_docname[docname].refresh();
}
@@ -829,6 +864,31 @@ export default class Grid {
this.debounced_refresh();
}
+ set_column_disp_in_list_view(fieldname, show) {
+ // Show/hide a column in this grid's list view (the static, read-only row
+ // rendering). Unlike `set_column_disp`, the change is kept as a grid-local
+ // override and never mutates the shared meta docfield, so other grids of
+ // the same child doctype on the same form are unaffected. The override is
+ // applied to a grid-local docfield copy in `_apply_column_disp_overrides`
+ // (called from `setup_fields`).
+ const fieldnames = Array.isArray(fieldname) ? fieldname : [fieldname];
+ for (let field of fieldnames) {
+ this.column_disp_overrides[field] = show ? 0 : 1;
+ }
+
+ // Tear down the cached column layout and the rendered rows so the new
+ // column set is rebuilt with consistent widths. Just clearing
+ // `visible_columns` is not enough: the header is rebuilt with redistributed
+ // `col-N` widths while already-rendered rows keep their old widths, leaving
+ // the grid misaligned. This mirrors `reset_grid()` (also used by the
+ // Configure Columns dialog).
+ this.visible_columns = [];
+ this.grid_rows = [];
+ $(this.parent).find(".grid-body .grid-row").remove();
+
+ this.debounced_refresh();
+ }
+
set_editable_grid_column_disp(fieldname, show) {
//Hide columns for editable grids
if (this.meta.editable_grid && this.grid_rows) {
@@ -841,8 +901,17 @@ export default class Grid {
//Show the static area and hide field area if it is not the editable row
if (row != frappe.ui.form.editable_row) {
- column.static_area.show();
- column.field_area && column.field_area.toggle(false);
+ if (
+ row.should_show_button_in_idle_grid_cell &&
+ row.should_show_button_in_idle_grid_cell(column)
+ ) {
+ row.make_control(column);
+ column.static_area.hide();
+ column.field_area && column.field_area.toggle(true);
+ } else {
+ column.static_area.show();
+ column.field_area && column.field_area.toggle(false);
+ }
}
//Hide the static area and show field area if it is the editable row
else {
@@ -1031,6 +1100,171 @@ export default class Grid {
return d;
}
+ bulk_edit_rows() {
+ if (!this.meta?.allow_bulk_edit) return;
+
+ const selected_children = this.get_selected_children();
+ if (!selected_children.length) {
+ frappe.show_alert({ message: __("No rows selected"), indicator: "orange" });
+ return;
+ }
+
+ const is_field_editable = (field_doc) => {
+ const parent_docstatus = this.frm?.doc?.docstatus;
+ const is_submitted_or_cancelled = [1, 2].includes(parent_docstatus);
+
+ return (
+ field_doc.fieldname &&
+ frappe.model.is_value_type(field_doc) &&
+ field_doc.fieldtype !== "Read Only" &&
+ !field_doc.hidden &&
+ !field_doc.read_only &&
+ !field_doc.is_virtual &&
+ (!is_submitted_or_cancelled || field_doc.allow_on_submit)
+ );
+ };
+
+ const editable_fields = (this.docfields || []).filter((field_doc) =>
+ is_field_editable(field_doc)
+ );
+ if (!editable_fields.length) {
+ frappe.msgprint(__("No editable fields available for bulk edit."));
+ return;
+ }
+
+ const grid = this;
+
+ const field_mappings = {};
+ editable_fields.forEach((field_doc) => {
+ const field_key = `${field_doc.label}`;
+ field_mappings[field_key] = Object.assign({}, field_doc);
+ });
+
+ const field_options = Object.keys(field_mappings).sort((a, b) =>
+ __(cstr(field_mappings[a].label)).localeCompare(cstr(__(field_mappings[b].label)))
+ );
+ const field_autocomplete_options = field_options.map((key) => ({
+ label: __(cstr(field_mappings[key].label)),
+ value: key,
+ }));
+ const status_regex = /status/i;
+ const default_field =
+ field_options.find((value) => status_regex.test(value)) ||
+ field_options.find((value) => field_mappings[value]?.fieldtype === "Select");
+
+ // One child row drives Link get_query(cb, doc, cdt, cdn) / locals lookups in bulk-edit dialog.
+ // Multiple rows selected may diverge — filters follow the first selected row only.
+ const bulk_edit_reference_row = selected_children[0];
+
+ const dialog = new frappe.ui.Dialog({
+ title: __("Bulk Edit"),
+ ...(bulk_edit_reference_row && {
+ frm: this.frm,
+ doc: bulk_edit_reference_row,
+ doctype: bulk_edit_reference_row.doctype,
+ }),
+ fields: [
+ {
+ fieldtype: "Autocomplete",
+ options: field_autocomplete_options,
+ max_items: Infinity,
+ default: default_field,
+ label: __("Field"),
+ fieldname: "field",
+ reqd: 1,
+ onchange: () => {
+ set_value_field(dialog);
+ },
+ },
+ {
+ fieldtype: "Data",
+ label: __("Value"),
+ fieldname: "value",
+ onchange() {
+ show_help_text();
+ },
+ },
+ ],
+ primary_action: ({ value }) => {
+ const selected_field = field_mappings[dialog.get_value("field")];
+ const { fieldname } = selected_field;
+ dialog.disable_primary_action();
+
+ const update_value = value || null;
+ const tasks = selected_children.map((doc) =>
+ frappe.model.set_value(doc.doctype, doc.name, fieldname, update_value)
+ );
+
+ Promise.all(tasks).then(() => {
+ this.frm && this.frm.dirty();
+ this.refresh();
+ dialog.hide();
+ const row_label = selected_children.length === 1 ? __("row") : __("rows");
+ frappe.show_alert(
+ __("Updated {0} selected {1}. Save the form to keep changes.", [
+ selected_children.length,
+ row_label,
+ ])
+ );
+ });
+ },
+ primary_action_label: __("Update {0} rows", [selected_children.length]),
+ });
+
+ if (default_field) set_value_field(dialog);
+ show_help_text();
+
+ function set_value_field(dialogObj) {
+ const field_value = dialogObj.get_value("field");
+ if (!field_value || !field_mappings[field_value]) return;
+ const new_df = Object.assign({}, field_mappings[field_value]);
+ if (
+ new_df.label?.match(status_regex) &&
+ new_df.fieldtype === "Select" &&
+ !new_df.default
+ ) {
+ let options = [];
+ if (typeof new_df.options === "string") {
+ options = new_df.options.split("\n");
+ }
+ new_df.default = options[0] || options[1];
+ }
+ new_df.label = __("Value");
+ new_df.onchange = show_help_text;
+ delete new_df.depends_on;
+
+ const grid_field = grid.get_field(new_df.fieldname);
+ if (grid_field?.get_query) {
+ new_df.get_query = grid_field.get_query;
+ }
+
+ dialogObj.replace_field("value", new_df);
+ // replace_field does not re-run attach_doc; Link needs docname + doctype for set_query third arg.
+ if (bulk_edit_reference_row) {
+ dialogObj.attach_doc_and_docfields(true);
+ }
+ show_help_text();
+ }
+
+ function show_help_text() {
+ if (dialog.get_primary_btn().is(":focus, :active")) return;
+
+ let value = dialog.get_value("value");
+ if (value == null || value === "") {
+ dialog.set_df_property(
+ "value",
+ "description",
+ __("You have not entered a value. The field will be set to empty.")
+ );
+ } else {
+ dialog.set_df_property("value", "description", "");
+ }
+ }
+
+ dialog.refresh();
+ dialog.show();
+ }
+
set_focus_on_row(idx) {
if (!idx && idx !== 0) {
idx = this.grid_rows.length - 1;
@@ -1145,7 +1379,9 @@ export default class Grid {
if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) {
this.user_defined_columns = user_settings[this.doctype]
.map((row) => {
- let column = frappe.meta.get_docfield(this.doctype, row.fieldname);
+ let column =
+ this.docfields?.find((d) => d.fieldname === row.fieldname) ||
+ frappe.meta.get_docfield(this.doctype, row.fieldname);
if (column) {
column.in_list_view = 1;
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js
index c1f4670fe237..f63a4fa528db 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -292,10 +292,12 @@ export default class GridRow {
delete this.grid.filter["row-index"];
}
- this.grid.grid_sortable.option(
- "disabled",
- Object.keys(this.grid.filter).length !== 0
- );
+ if (this.grid.grid_sortable) {
+ this.grid.grid_sortable.option(
+ "disabled",
+ Object.keys(this.grid.filter).length !== 0
+ );
+ }
this.grid.prevent_build = true;
me.grid.refresh();
@@ -790,6 +792,30 @@ export default class GridRow {
// last empty column
$(`
`).appendTo(this.row);
}
+
+ // Button has no stored value; static_area stays empty while field_area is hidden until the row
+ // becomes "editable". Keep the control visible for Button columns in editable grids.
+ this.columns_list.forEach((column) => {
+ if (this.should_show_button_in_idle_grid_cell(column)) {
+ this.make_control(column);
+ column.static_area.toggle(false);
+ column.field_area.toggle(true);
+ }
+ });
+ }
+
+ /**
+ * Button fields only: show the real control even when this row is not the active editable row.
+ * Scope is intentionally narrow to avoid changing behaviour of value fields.
+ */
+ should_show_button_in_idle_grid_cell(column) {
+ return (
+ column.df.fieldtype === "Button" &&
+ this.grid.allow_on_grid_editing() &&
+ this.grid.is_editable() &&
+ this.doc &&
+ !column.df.hidden
+ );
}
set_dependant_property(df) {
@@ -1165,11 +1191,17 @@ export default class GridRow {
this.refresh_field(df.fieldname, txt);
}
- if (!column.df.hidden) {
- column.static_area.toggle(true);
- }
+ if (this.should_show_button_in_idle_grid_cell(column)) {
+ this.make_control(column);
+ column.static_area.toggle(false);
+ column.field_area.toggle(true);
+ } else {
+ if (!column.df.hidden) {
+ column.static_area.toggle(true);
+ }
- column.field_area && column.field_area.toggle(false);
+ column.field_area && column.field_area.toggle(false);
+ }
});
frappe.ui.form.editable_row = null;
}
diff --git a/frappe/public/js/frappe/form/info_card.js b/frappe/public/js/frappe/form/info_card.js
index a5165c0b07cc..2d3a501d2bf1 100644
--- a/frappe/public/js/frappe/form/info_card.js
+++ b/frappe/public/js/frappe/form/info_card.js
@@ -58,7 +58,8 @@ export class InfoCard {
me.card.toggle();
});
$(document).on("click", function (e) {
- if (!e.originalEvent.composedPath().includes(me.label_area)) {
+ const path = e.originalEvent?.composedPath();
+ if (!path || !path.includes(me.label_area)) {
me.card.hide();
}
});
diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js
index bef75dae906d..22ed74798717 100644
--- a/frappe/public/js/frappe/form/multi_select_dialog.js
+++ b/frappe/public/js/frappe/form/multi_select_dialog.js
@@ -18,6 +18,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
this.make();
this.selected_fields = new Set();
+ this.selected_items = {};
}
get_fields() {
@@ -343,8 +344,11 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
let name = $(this).attr("data-item-name").trim();
if ($(this).find(":checkbox").is(":checked")) {
me.selected_fields.add(name);
+ const item = me.results.find((r) => r.name === name);
+ if (item) me.selected_items[name] = item;
} else {
me.selected_fields.delete(name);
+ delete me.selected_items[name];
}
});
@@ -355,8 +359,11 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
const name = $(this).closest(".list-item-container").attr("data-item-name").trim();
if (checked) {
me.selected_fields.add(name);
+ const item = me.results.find((r) => r.name === name);
+ if (item) me.selected_items[name] = item;
} else {
me.selected_fields.delete(name);
+ delete me.selected_items[name];
}
});
});
@@ -539,13 +546,11 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
}
empty_list() {
- // Store all checked items
- let checked = this.results
- .filter((result) => this.selected_fields.has(result.name))
- .map((item) => ({
- ...item,
- checked: true,
- }));
+ // Store all checked items using selected_items map (persists across searches)
+ let checked = [...this.selected_fields].map((name) => ({
+ ...(this.selected_items[name] || { name }),
+ checked: true,
+ }));
// Remove **all** items
this.$results.find(".list-item-container").remove();
@@ -556,45 +561,58 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
get_filters_from_setters() {
let me = this;
- let filters = (this.get_query ? this.get_query().filters : {}) || {};
+ let query_filters = (this.get_query ? this.get_query().filters : {}) || {};
+ let filters_list = Object.entries(query_filters).map(([key, value]) => {
+ if (Array.isArray(value)) {
+ return [me.doctype, key, value[0], value[1]];
+ }
+ return [me.doctype, key, "=", value];
+ });
let filter_fields = [];
if ($.isArray(this.setters)) {
for (let df of this.setters) {
- filters[df.fieldname] =
- me.dialog.fields_dict[df.fieldname].get_value() || undefined;
- me.args[df.fieldname] = filters[df.fieldname];
+ let value =
+ me.dialog.fields_dict[df.fieldname].get_value() || df.default || undefined;
+ me.args[df.fieldname] = value;
filter_fields.push(df.fieldname);
+ if (value != null) {
+ filters_list.push([me.doctype, df.fieldname, "=", value]);
+ }
}
} else {
Object.keys(this.setters).forEach(function (setter) {
- var value = me.dialog.fields_dict[setter].get_value() || me.setters[setter];
+ let value = me.dialog.fields_dict[setter].get_value() || me.setters[setter];
if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) {
- filters[setter] = ["like", "%" + value + "%"];
+ filters_list.push([me.doctype, setter, "like", "%" + value + "%"]);
} else {
- filters[setter] = value || undefined;
- me.args[setter] = filters[setter];
+ value = value || undefined;
+ me.args[setter] = value;
filter_fields.push(setter);
+ if (value != null) {
+ filters_list.push([me.doctype, setter, "=", value]);
+ }
}
});
}
- return [filters, filter_fields];
+ return [filters_list, filter_fields];
}
get_args_for_search() {
- let [filters, filter_fields] = this.get_filters_from_setters();
-
- let custom_filters = this.get_custom_filters();
- Object.assign(filters, custom_filters);
+ let [filters_list, filter_fields] = this.get_filters_from_setters();
+ if (this.add_filters_group && this.filter_group) {
+ filters_list.push(...this.filter_group.get_filters());
+ }
return {
doctype: this.doctype,
txt: this.dialog.fields_dict["search_term"].get_value(),
- filters: filters,
+ filters: filters_list,
filter_fields: filter_fields,
page_length: this.page_length + 5,
query: this.get_query ? this.get_query().query : "",
+ query_filters_as_dict: true,
as_dict: 1,
};
}
diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js
index 5640514eec3f..0a6e7a222ad0 100644
--- a/frappe/public/js/frappe/form/save.js
+++ b/frappe/public/js/frappe/form/save.js
@@ -123,25 +123,27 @@ frappe.ui.form.check_mandatory = function (frm) {
if (frm.doc.docstatus == 2) return true; // don't check for cancel
+ const ROW_LIMIT = 10;
+ const parent_errors = [];
+ const table_errors = {};
+
$.each(frappe.model.get_all_docs(frm.doc), function (i, doc) {
var error_fields = [];
var folded = false;
+ const fields_dict = frappe.meta.get_docfield_copy(doc.doctype, doc.name) || {};
$.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);
+ const df = fields_dict[docfield.fieldname];
+ if (!df) return;
- // skip fields that don't hold data
- if (
- ["Section Break", "Column Break", "Tab Break", "HTML", "Heading"].includes(
- df.fieldtype
- )
- ) {
+ if (!df.reqd && !df.mandatory_depends_on && df.fieldtype !== "Fold") {
return;
}
if (df.fieldtype === "Fold") {
folded = frm.layout.folded;
+ return;
}
if (
@@ -170,31 +172,97 @@ frappe.ui.form.check_mandatory = function (frm) {
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,
- ]);
+ const parentfield = doc.parentfield;
+ if (!table_errors[parentfield]) {
+ const table_field = frappe.meta.docfield_map[doc.parenttype][parentfield];
+ const table_label = __(
+ table_field.label || frappe.unscrub(table_field.fieldname)
+ ).bold();
+ table_errors[parentfield] = {
+ label: table_label,
+ fields: {},
+ total_rows: (frm.doc[parentfield] || []).length,
+ };
+ }
+ error_fields.forEach(function (field_label) {
+ if (!table_errors[parentfield].fields[field_label]) {
+ table_errors[parentfield].fields[field_label] = [];
+ }
+ table_errors[parentfield].fields[field_label].push(doc.idx);
+ });
} else {
- message = __("Mandatory fields required in {0}", [__(doc.doctype)]);
+ error_fields.forEach(function (field_label) {
+ parent_errors.push(__("{0} is required.", [field_label.bold()]));
+ });
}
- message = message + "" + error_fields.join(" ") + " ";
- frappe.msgprint({
- message: message,
- indicator: "red",
- title: __("Missing Fields"),
- });
- frm.refresh();
}
});
+ const lines = [...parent_errors];
+ Object.values(table_errors).forEach(function (te) {
+ Object.entries(te.fields).forEach(function (entry) {
+ const field_label = entry[0];
+ const rows = entry[1].sort((a, b) => a - b);
+
+ const ranges = [];
+ let start = rows[0];
+ let prev = rows[0];
+ for (let i = 1; i < rows.length; i++) {
+ if (rows[i] === prev + 1) {
+ prev = rows[i];
+ } else {
+ ranges.push(start === prev ? `${start}` : `${start}-${prev}`);
+ start = prev = rows[i];
+ }
+ }
+ ranges.push(start === prev ? `${start}` : `${start}-${prev}`);
+
+ if (rows.length === te.total_rows) {
+ lines.push(
+ __("In {0}, {1} is required in every row.", [te.label, field_label.bold()])
+ );
+ } else if (rows.length === 1) {
+ lines.push(
+ __("In {0}, {1} is required in row {2}.", [
+ te.label,
+ field_label.bold(),
+ rows[0],
+ ])
+ );
+ } else if (ranges.length <= ROW_LIMIT) {
+ lines.push(
+ __("In {0}, {1} is required in rows {2}.", [
+ te.label,
+ field_label.bold(),
+ frappe.utils.comma_and(ranges),
+ ])
+ );
+ } else {
+ lines.push(
+ __("In {0}, {1} is required in {2} rows.", [
+ te.label,
+ field_label.bold(),
+ rows.length,
+ ])
+ );
+ }
+ });
+ });
+
+ if (lines.length) {
+ frappe.msgprint({
+ message:
+ __("Please fill the following mandatory fields before saving:") +
+ "" +
+ lines.join(" ") +
+ " ",
+ indicator: "red",
+ title: __("Missing Fields"),
+ });
+ frm.refresh();
+ }
+
return !has_errors;
function is_docfield_mandatory(doc, df) {
diff --git a/frappe/public/js/frappe/form/sidebar/assign_to.js b/frappe/public/js/frappe/form/sidebar/assign_to.js
index 6b867fdf51e6..d483c4d72e12 100644
--- a/frappe/public/js/frappe/form/sidebar/assign_to.js
+++ b/frappe/public/js/frappe/form/sidebar/assign_to.js
@@ -204,6 +204,7 @@ frappe.ui.form.AssignToDialog = class AssignToDialog {
label: __("Assign To User Group"),
fieldtype: "Link",
fieldname: "assign_to_user_group",
+ hidden: 1,
options: "User Group",
onchange: () => me.user_group_list(),
},
diff --git a/frappe/public/js/frappe/form/sidebar/user_image.js b/frappe/public/js/frappe/form/sidebar/user_image.js
index b022b157b940..c1ef7b76ce2d 100644
--- a/frappe/public/js/frappe/form/sidebar/user_image.js
+++ b/frappe/public/js/frappe/form/sidebar/user_image.js
@@ -17,12 +17,9 @@ frappe.ui.form.set_user_image = function (frm) {
image = window.cordova && image.indexOf("http") === -1 ? frappe.base_url + image : image;
image_section.find(".sidebar-image").attr("src", image).removeClass("hide");
-
image_section.find(".sidebar-standard-image").addClass("hide");
-
title_image.css("background-image", `url("${image}")`).html("");
-
- image_actions.find(".sidebar-image-change, .sidebar-image-remove").show();
+ image_actions.find(".sidebar-image-remove").show();
} else {
image_section.find(".sidebar-image").attr("src", null).addClass("hide");
@@ -35,8 +32,6 @@ frappe.ui.form.set_user_image = function (frm) {
.html(frappe.get_abbr(title));
title_image.css("background-image", "").html(frappe.get_abbr(title));
-
- image_actions.find(".sidebar-image-change").show();
image_actions.find(".sidebar-image-remove").hide();
}
};
@@ -50,40 +45,26 @@ frappe.ui.form.setup_user_image_event = function (frm) {
}
if (frm.meta.image_field && !frm.fields_dict[frm.meta.image_field].df.read_only) {
- frm.sidebar.image_wrapper.on("click", ":not(.sidebar-image-actions)", (e) => {
- let $target = $(e.currentTarget);
- if ($target.is("a.dropdown-toggle, .dropdown")) {
+ // clicking anywhere on the image wrapper triggers upload
+ frm.sidebar.image_wrapper.on("click", function (e) {
+ if ($(e.target).closest(".sidebar-image-remove").length) {
return;
}
- let dropdown = frm.sidebar.image_wrapper.find(".sidebar-image-actions .dropdown");
- dropdown.toggleClass("open");
- e.stopPropagation();
+ var field = frm.get_field(frm.meta.image_field);
+ if (!field.$input) {
+ field.make_input();
+ }
+ field.$input.trigger("attach_doc_image");
+ frm.page.close_sidebar?.();
});
}
- // bind click on image_wrapper
- frm.sidebar.image_wrapper.on(
- "click",
- ".sidebar-image-change, .sidebar-image-remove",
- function (e) {
- let $target = $(e.currentTarget);
- var field = frm.get_field(frm.meta.image_field);
- if ($target.is(".sidebar-image-change")) {
- if (!field.$input) {
- field.make_input();
- }
- field.$input.trigger("attach_doc_image");
- // close sidebar
- frm.page.close_sidebar?.();
- } else {
- /// on remove event for a sidebar image wrapper remove attach file.
- frm.attachments.remove_attachment_by_filename(
- frm.doc[frm.meta.image_field],
- function () {
- field.set_value("").then(() => frm.save());
- }
- );
- }
- }
- );
+ // remove button
+ frm.sidebar.image_wrapper.on("click", ".sidebar-image-remove", function (e) {
+ e.stopPropagation();
+ var field = frm.get_field(frm.meta.image_field);
+ frm.attachments.remove_attachment_by_filename(frm.doc[frm.meta.image_field], function () {
+ field.set_value("").then(() => frm.save());
+ });
+ });
};
diff --git a/frappe/public/js/frappe/form/templates/form_links.html b/frappe/public/js/frappe/form/templates/form_links.html
index ed6256eaf091..24447c8784ee 100644
--- a/frappe/public/js/frappe/form/templates/form_links.html
+++ b/frappe/public/js/frappe/form/templates/form_links.html
@@ -20,7 +20,7 @@
{% if !internal_links[doctype] %}
-
+
{% endif %}
diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html
index 5fdd5176b4c5..0403e9c15c3e 100644
--- a/frappe/public/js/frappe/form/templates/form_sidebar.html
+++ b/frappe/public/js/frappe/form/templates/form_sidebar.html
@@ -6,13 +6,12 @@
{% if can_write %}
@@ -32,7 +31,7 @@
{% let title = frm.get_title(); %}
-
+
{%= frappe.utils.escape_html(frappe.utils.html2text(title)) %}
{% if frm.meta.beta %}
@@ -41,8 +40,8 @@
{% endif %}
{% if (title && title !== frm.doc.name) { %}
-
diff --git a/frappe/public/js/frappe/form/templates/timeline_message_box.html b/frappe/public/js/frappe/form/templates/timeline_message_box.html
index ebf8cc5cea5d..650ca40ddab5 100644
--- a/frappe/public/js/frappe/form/templates/timeline_message_box.html
+++ b/frappe/public/js/frappe/form/templates/timeline_message_box.html
@@ -1,5 +1,6 @@
{% is_mini = frappe.is_mobile() ? true : false %}
+ {% var show_absolute_datetime = cint(frappe.boot.user.show_absolute_datetime_in_timeline) || cint(frappe.boot.sysdefaults.show_absolute_datetime_in_timeline) %}
{% if (doc.communication_type && doc.communication_type == "Automated Message") { %}
@@ -24,7 +25,7 @@
{% } %}
{% } else if (doc.comment_type && doc.comment_type == "Comment") { %}
@@ -34,7 +35,7 @@
·
{% if (doc.published) { %}
·
@@ -52,7 +53,7 @@
.
- {{ comment_when(doc.communication_date || doc.creation, is_mini) }}
+ {{ show_absolute_datetime ? frappe.datetime.str_to_user(doc.communication_date || doc.creation) : comment_when(doc.communication_date || doc.creation, is_mini) }}
{% if (doc.subject) { %}
{{doc.subject}}
diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js
index 9196e85027f3..24f34c02c7fb 100644
--- a/frappe/public/js/frappe/form/toolbar.js
+++ b/frappe/public/js/frappe/form/toolbar.js
@@ -423,7 +423,7 @@ frappe.ui.form.Toolbar = class Toolbar {
this.show_jump_to_field_dialog();
},
true,
- "Ctrl+J"
+ { shortcut: "Ctrl+J", ignore_inputs: true }
);
}
@@ -630,8 +630,9 @@ frappe.ui.form.Toolbar = class Toolbar {
) {
let doctype = is_doctype_form ? this.frm.docname : this.frm.doctype;
let is_doctype_custom = is_doctype_form ? this.frm.doc.custom : false;
+ let is_core_doctype = frappe.model.core_doctypes_list.includes(doctype);
- if (doctype != "DocType" && !is_doctype_custom && this.frm.meta.issingle === 0) {
+ if (!is_core_doctype && !is_doctype_custom && this.frm.meta.issingle === 0) {
this.page.add_menu_item(
__("Customize"),
() => {
@@ -867,7 +868,11 @@ frappe.ui.form.Toolbar = class Toolbar {
!f.df.hidden &&
f.disp_status !== "None";
- let fields = this.frm.fields
+ // if a child table row is open in the detail editor, search its fields instead
+ let grid_row = this.frm.open_grid_row();
+ let grid_form = grid_row?.grid_form;
+
+ let fields = (grid_form ? grid_form.layout.fields_list : this.frm.fields)
.filter(visible_fields_filter)
.map((f) => ({ label: __(f.df.label), value: f.df.fieldname }));
@@ -882,10 +887,15 @@ frappe.ui.form.Toolbar = class Toolbar {
reqd: 1,
},
],
+ keep_grid_form_open: !!grid_form,
primary_action_label: __("Go"),
primary_action: ({ fieldname }) => {
dialog.hide();
- this.frm.scroll_to_field(fieldname);
+ if (grid_form) {
+ this.scroll_to_grid_field(grid_form, fieldname);
+ } else {
+ this.frm.scroll_to_field(fieldname);
+ }
},
animate: false,
});
@@ -893,6 +903,50 @@ frappe.ui.form.Toolbar = class Toolbar {
dialog.show();
}
+ scroll_to_grid_field(grid_form, fieldname, focus = true) {
+ let field = grid_form.fields_dict[fieldname];
+ if (!field) return false;
+
+ let $el = field.$wrapper;
+ if (!$el || !$el.length) return false;
+
+ // set tab as active
+ if (field.tab && !field.tab.is_active()) {
+ field.tab.set_active();
+ }
+
+ // uncollapse section
+ if (field.section?.is_collapsed()) {
+ field.section.collapse(false);
+ }
+
+ // scroll to input
+ let scroll_container = grid_form.wrapper.find(".grid-form-body");
+ frappe.utils.scroll_to(
+ $el,
+ true,
+ 15,
+ scroll_container.length ? scroll_container : $(".main-section")
+ );
+
+ // focus if text field
+ if (focus) {
+ setTimeout(() => {
+ $el.find("input, select, textarea").focus();
+ }, 500);
+ }
+
+ // highlight control inside field
+ let control_element = $el.closest(".frappe-control");
+ if (control_element.length) {
+ control_element.addClass("highlight");
+ setTimeout(() => {
+ control_element.removeClass("highlight");
+ }, 2000);
+ }
+ return true;
+ }
+
setup_sidebar_toggle(sidebar_wrapper) {
if (frappe.utils.is_xs() || frappe.utils.is_sm()) {
this.setup_overlay_sidebar(sidebar_wrapper);
@@ -944,7 +998,7 @@ frappe.ui.form.Toolbar = class Toolbar {
}
get_follow_text(follow) {
- if (follow === null) {
+ if (follow == null) {
follow = this.frm.get_docinfo().is_document_followed;
}
return follow ? __("Unfollow") : __("Follow");
diff --git a/frappe/public/js/frappe/icon_picker/icon_picker.js b/frappe/public/js/frappe/icon_picker/icon_picker.js
index 15c25c7f8e10..25860bbcf332 100644
--- a/frappe/public/js/frappe/icon_picker/icon_picker.js
+++ b/frappe/public/js/frappe/icon_picker/icon_picker.js
@@ -35,63 +35,20 @@ class Picker {
}
}
setup_emojis() {
- // setup tab
this.setup_tab();
- // setup emoji container
this.setup_emoji_container();
- // emojis
this.emoji_wrapper = this.icon_picker_wrapper.find(".emojis");
- gemoji.forEach((emoji, i) => {
- let $icon = $(
- `${gemoji[i].emoji}
`
- );
+ frappe.utils.get_emojis().forEach((emoji) => {
+ const $icon = $(`${emoji}
`);
this.emoji_wrapper.append($icon);
- const set_values = () => {
- this.set_icon(gemoji[i].emoji);
- this.update_icon_selected();
- };
$icon.on("click", () => {
- set_values();
+ this.set_icon(emoji);
+ this.update_icon_selected();
});
- // $icon.keydown((e) => {
- // const key_code = e.keyCode;
- // if ([13, 32].includes(key_code)) {
- // e.preventDefault();
- // set_values();
- // }
- // });
- });
- this.search_input.on("input", (e) => {
- e.preventDefault();
- this.filter_emojis();
});
}
filter_emojis() {
- let value = this.search_input.val();
- let filtered_emoji_names = [];
- if (value) {
- gemoji.forEach((g) => {
- g.tags.forEach((t) => {
- if (t.includes(value)) {
- filtered_emoji_names.push(g);
- }
- });
- g.names.forEach((t) => {
- if (t.includes(value)) {
- filtered_emoji_names.push(g);
- }
- });
- });
- }
-
- if (filtered_emoji_names.length == 0) {
- this.emoji_wrapper.find(".emoji-wrapper").removeClass("hidden");
- } else {
- this.emoji_wrapper.find(".emoji-wrapper").addClass("hidden");
- filtered_emoji_names.forEach((g) => {
- this.emoji_wrapper.find(`.emoji-wrapper[id*='${g.emoji}']`).removeClass("hidden");
- });
- }
+ this.emoji_wrapper.find(".emoji-wrapper").removeClass("hidden");
}
setup_emoji_container() {
this.icon_picker_wrapper.find(".icon-section")
diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js
index 9738f165dd96..ff2cce5844e7 100644
--- a/frappe/public/js/frappe/list/bulk_operations.js
+++ b/frappe/public/js/frappe/list/bulk_operations.js
@@ -308,21 +308,30 @@ export default class BulkOperations {
}
edit(docnames, field_mappings, done) {
- let field_options = Object.keys(field_mappings).sort(function (a, b) {
+ const field_options = Object.keys(field_mappings).sort(function (a, b) {
return __(cstr(field_mappings[a].label)).localeCompare(
cstr(__(field_mappings[b].label))
);
});
+ // Same strings as legacy Select (`options`: sorted mapping keys)—parent `Label (Doctype)`,
+ // child `Child Label (Table column)`, so labels stay distinguishable after Autocomplete swap.
+ const field_autocomplete_options = field_options.map((key) => ({
+ label: __(cstr(key)),
+ value: key,
+ }));
const status_regex = /status/i;
- const default_field = field_options.find((value) => status_regex.test(value));
+ const default_field =
+ field_options.find((value) => status_regex.test(value)) ||
+ field_options.find((value) => field_mappings[value]?.fieldtype === "Select");
const dialog = new frappe.ui.Dialog({
title: __("Bulk Edit"),
fields: [
{
- fieldtype: "Select",
- options: field_options,
+ fieldtype: "Autocomplete",
+ options: field_autocomplete_options,
+ max_items: Infinity,
default: default_field,
label: __("Field"),
fieldname: "field",
@@ -420,6 +429,8 @@ export default class BulkOperations {
}
function show_help_text() {
+ if (dialog.get_primary_btn().is(":focus, :active")) return;
+
let value = dialog.get_value("value");
if (value == null || value === "") {
dialog.set_df_property(
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index e58a1bf852e4..791b2924ecdd 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -2003,7 +2003,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
if (
frappe.model.can_create("Custom Field") &&
- frappe.model.can_create("Property Setter")
+ frappe.model.can_create("Property Setter") &&
+ !frappe.model.core_doctypes_list.includes(doctype)
) {
items.push({
label: __("Customize", null, "Button in list view menu"),
@@ -2247,21 +2248,14 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return {
label: __("Clear Assignment", null, "Button in list view actions menu"),
action: () => {
- frappe.confirm(
- __("Are you sure you want to clear the assignments?"),
- () => {
- this.disable_list_update = true;
- bulk_operations.clear_assignment(this.get_checked_items(true), () => {
- this.disable_list_update = false;
- this.clear_checked_items();
- this.refresh();
- });
- },
- () => {
+ frappe.confirm(__("Are you sure you want to clear the assignments?"), () => {
+ this.disable_list_update = true;
+ bulk_operations.clear_assignment(this.get_checked_items(true), () => {
+ this.disable_list_update = false;
this.clear_checked_items();
this.refresh();
- }
- );
+ });
+ });
},
standard: true,
};
diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js
index 614ce84b15ff..14f55d192a4e 100644
--- a/frappe/public/js/frappe/model/create_new.js
+++ b/frappe/public/js/frappe/model/create_new.js
@@ -212,6 +212,12 @@ $.extend(frappe.model, {
} else if (df.fieldname === meta.title_field) {
// ignore defaults for title field
value = "";
+ } else if (default_val.startsWith("eval:")) {
+ try {
+ value = frappe.utils.eval(default_val.substring(5), { doc });
+ } catch (e) {
+ frappe.throw(__('Invalid "Default" expression'));
+ }
} else {
// is this default value is also allowed as per user permissions?
var is_allowed_default =
diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js
index 036dc0c1cd05..4fce520bb671 100644
--- a/frappe/public/js/frappe/ui/dialog.js
+++ b/frappe/public/js/frappe/ui/dialog.js
@@ -14,7 +14,17 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
this.is_dialog = true;
this.last_focus = null;
- $.extend(this, { animate: true, size: null, auto_make: true, centered: false }, opts);
+ $.extend(
+ this,
+ {
+ animate: true,
+ size: null,
+ auto_make: true,
+ centered: false,
+ keep_grid_form_open: false,
+ },
+ opts
+ );
if (this.auto_make) {
this.make();
}
@@ -94,8 +104,10 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
me.display = false;
me.is_minimized = false;
me.hide_scrollbar(false);
- // hide any grid row form if open
- frappe.ui.form.get_open_grid_form?.()?.hide_form();
+ if (!me.keep_grid_form_open) {
+ // hide any grid row form if open
+ frappe.ui.form.get_open_grid_form?.()?.hide_form();
+ }
if (frappe.ui.open_dialogs[frappe.ui.open_dialogs.length - 1] === me) {
frappe.ui.open_dialogs.pop();
@@ -131,12 +143,28 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
})
.on("keydown", function (e) {
if (e.key === "Escape" || e.keyCode === 27) {
- // when dialog is open and contains an awesomplete dropdown - do not close the dialog on escape key press
- if (me.display && me.$wrapper.find(".awesomplete").length) {
+ // _awesomplete_was_open is set in the capture-phase listener below, before
+ // Awesomplete's own keydown handler closes the dropdown and clears aria-expanded.
+ if (me._awesomplete_was_open) {
+ me._awesomplete_was_open = false;
e.stopImmediatePropagation();
}
}
});
+
+ // Runs in capture phase, before Awesomplete's bubble-phase keydown handler on the input,
+ // so aria-expanded is still "true" when the dropdown is open.
+ me.$wrapper[0].addEventListener(
+ "keydown",
+ function (e) {
+ if (e.key === "Escape" || e.keyCode === 27) {
+ me._awesomplete_was_open =
+ me.display &&
+ !!me.$wrapper.find(".awesomplete input[aria-expanded='true']").length;
+ }
+ },
+ true // capture phase
+ );
}
set_modal_size() {
diff --git a/frappe/public/js/frappe/ui/keyboard.js b/frappe/public/js/frappe/ui/keyboard.js
index dcb41b1545f8..f398a20ab8f5 100644
--- a/frappe/public/js/frappe/ui/keyboard.js
+++ b/frappe/public/js/frappe/ui/keyboard.js
@@ -22,6 +22,13 @@ frappe.ui.keys.setup = function () {
let standard_shortcuts = [];
frappe.ui.keys.standard_shortcuts = standard_shortcuts;
+frappe.ui.keys.get_shortcut_label = function (shortcut) {
+ let label = shortcut.split("+").map(frappe.utils.to_title_case).join("+");
+ if (frappe.utils.is_mac()) {
+ label = label.replace("Ctrl", "⌘").replace("Alt", "⌥");
+ }
+ return label.replace("Shift", "⇧");
+};
frappe.ui.keys.add_shortcut = ({
shortcut,
action,
@@ -90,22 +97,29 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => {
if (!shortcuts.length) {
return "";
}
- let html = shortcuts
+ let deduped = [];
+ let seen = {};
+ shortcuts
.filter((s) => (s.condition ? s.condition() : true))
.filter((s) => !!s.description)
- .map((shortcut) => {
- let shortcut_label = shortcut.shortcut
- .split("+")
- .map(frappe.utils.to_title_case)
- .join("+");
- if (frappe.utils.is_mac()) {
- shortcut_label = shortcut_label.replace("Ctrl", "⌘").replace("Alt", "⌥");
+ .forEach((shortcut) => {
+ if (seen[shortcut.description] !== undefined) {
+ deduped[seen[shortcut.description]].keys.push(shortcut.shortcut);
+ } else {
+ seen[shortcut.description] = deduped.length;
+ deduped.push({ ...shortcut, keys: [shortcut.shortcut] });
}
-
- shortcut_label = shortcut_label.replace("Shift", "⇧");
-
+ });
+ let html = deduped
+ .map((shortcut) => {
+ let shortcut_label = shortcut.keys
+ .map((k) => {
+ let label = frappe.ui.keys.get_shortcut_label(k);
+ return `${label} `;
+ })
+ .join(" / ");
return `
- ${shortcut_label}
+ ${shortcut_label}
${shortcut.description || ""}
`;
})
@@ -193,16 +207,14 @@ frappe.ui.keys.add_shortcut({
e.preventDefault();
return false;
},
- description: __("Trigger Primary Action"),
+ description: __("Trigger primary action"),
ignore_inputs: true,
});
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+k",
action: function (e) {
- $("#navbar-modal-search").click();
- e.preventDefault();
- return false;
+ return frappe.search.open_awesomebar_from_global_search_shortcut?.(e);
},
description: __("Open Awesomebar"),
ignore_inputs: true,
@@ -211,38 +223,18 @@ frappe.ui.keys.add_shortcut({
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+g",
action: function (e) {
- $("#navbar-modal-search").click();
- e.preventDefault();
- return false;
+ return frappe.search.open_global_search_from_navbar_shortcut?.(e);
},
- description: __("Open Awesomebar"),
+ description: __("Open Global Search"),
ignore_inputs: true,
});
-frappe.ui.keys.add_shortcut({
- shortcut: "alt+s",
- action: function (e) {
- e.preventDefault();
- $(".dropdown-navbar-user button").eq(0).click();
- },
- description: __("Open Settings"),
-});
-
frappe.ui.keys.add_shortcut({
shortcut: "shift+/",
action: function () {
frappe.ui.keys.show_keyboard_shortcut_dialog();
},
- description: __("Show Keyboard Shortcuts"),
-});
-
-frappe.ui.keys.add_shortcut({
- shortcut: "alt+h",
- action: function (e) {
- e.preventDefault();
- $(".dropdown-help button").eq(0).click();
- },
- description: __("Open Help"),
+ description: __("Show keyboard shortcuts"),
});
frappe.ui.keys.on("escape", function (e) {
@@ -286,7 +278,7 @@ frappe.ui.keys.add_shortcut({
action: function () {
frappe.ui.toolbar.clear_cache();
},
- description: __("Clear Cache and Reload"),
+ description: __("Clear cache and reload"),
});
frappe.ui.keys.key_map = {
diff --git a/frappe/public/js/frappe/ui/menu.js b/frappe/public/js/frappe/ui/menu.js
index aae005ce72ef..366a5dc6c0c1 100644
--- a/frappe/public/js/frappe/ui/menu.js
+++ b/frappe/public/js/frappe/ui/menu.js
@@ -103,13 +103,20 @@ frappe.ui.menu = class ContextMenu {
item.action ? `return ${item.action}` : ""
}">
-
+
diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js
index 3c8a24430e5f..e679b2b078af 100644
--- a/frappe/public/js/frappe/ui/sidebar/sidebar.js
+++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js
@@ -21,8 +21,6 @@ frappe.ui.Sidebar = class Sidebar {
this.items = [];
this.cards = [];
this.setup_events();
- this.sidebar_module_map = {};
- this.build_sidebar_module_map();
this.standard_items_setup = false;
this.preferred_sidebars = [];
}
@@ -42,16 +40,6 @@ frappe.ui.Sidebar = class Sidebar {
console.log(e);
}
}
- build_sidebar_module_map() {
- for (const [key, value] of Object.entries(frappe.boot.workspace_sidebar_item)) {
- if (value.module && !value.label.includes("My Workspaces")) {
- if (!this.sidebar_module_map[value.module]) {
- this.sidebar_module_map[value.module] = [];
- }
- this.sidebar_module_map[value.module].push(value.label);
- }
- }
- }
choose_app_name() {
if (frappe.boot.app_name_style === "Default") return;
@@ -160,8 +148,8 @@ frappe.ui.Sidebar = class Sidebar {
this.promotional_banners.forEach((banner) => {
let banner_html = $(`
-
- ${banner.title}
+
+ ${banner.title}
`);
@@ -276,6 +264,7 @@ frappe.ui.Sidebar = class Sidebar {
this.setup_onboarding();
});
+ this.store_last_show_sidebar_for_item();
}
add_card(card) {
if (this.cards && this.cards.find((i) => i.title === card.title)) return;
@@ -298,20 +287,15 @@ frappe.ui.Sidebar = class Sidebar {
}
setup_events() {
const me = this;
+ this.setup_reload();
frappe.router.on("change", function (router) {
- if (frappe.route_options.sidebar) {
+ if (frappe.route_options && frappe.route_options.sidebar) {
frappe.app.sidebar.setup(frappe.route_options.sidebar);
frappe.route_options = null;
} else {
frappe.app.sidebar.set_workspace_sidebar(router);
}
});
- $(document).on("page-change", function () {
- frappe.app.sidebar.toggle();
- });
- $(document).on("form-refresh", function () {
- frappe.app.sidebar.toggle();
- });
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+/",
@@ -320,13 +304,24 @@ frappe.ui.Sidebar = class Sidebar {
});
}
- toggle() {
+ // Fired on page-change / form-refresh. Handles visibility, then runs the
+ // same resolver as the router so every navigation event picks a sidebar.
+ // set_workspace_sidebar is idempotent, so re-running it here is a no-op
+ // unless the route actually warrants a different sidebar.
+ refresh() {
if (!frappe.container.page.page) return;
if (frappe.container.page.page.hide_sidebar) {
this.wrapper.hide();
+ return;
+ }
+ this.wrapper.show();
+ this.set_workspace_sidebar();
+ }
+ toggle(hide) {
+ if (hide) {
+ this.wrapper.hide();
} else {
this.wrapper.show();
- this.set_sidebar_for_page();
}
}
make_dom() {
@@ -505,14 +500,6 @@ frappe.ui.Sidebar = class Sidebar {
this.setup_notifications();
this.standard_items_setup = true;
}
- get_workspace_for_module(module) {
- for (let i = 0; i < frappe.boot.workspaces.pages.length; i++) {
- const workspace = frappe.boot.workspaces.pages[i];
- if (workspace.module == module && !workspace.parent_page) {
- return workspace.name;
- }
- }
- }
setup_awesomebar() {
if (frappe.boot.desk_settings.search_bar) {
let awesome_bar = new frappe.search.AwesomeBar();
@@ -560,17 +547,15 @@ frappe.ui.Sidebar = class Sidebar {
}
expand_sidebar() {
- let direction;
const is_rtl = frappe.utils.is_rtl();
if (this.sidebar_expanded) {
this.wrapper.addClass("expanded");
- direction = is_rtl ? "left" : "right";
$('[data-toggle="tooltip"]').tooltip("dispose");
this.wrapper.find(".avatar-name-email").show();
this.wrapper.find(".onboarding-sidebar span").show();
+ this.wrapper.find(".promotional-banner-title").show();
} else {
this.wrapper.removeClass("expanded");
- direction = is_rtl ? "right" : "left";
$('[data-toggle="tooltip"]').tooltip({
boundary: "window",
container: "body",
@@ -578,13 +563,21 @@ frappe.ui.Sidebar = class Sidebar {
});
this.wrapper.find(".avatar-name-email").hide();
this.wrapper.find(".onboarding-sidebar span").hide();
+ this.wrapper.find(".promotional-banner-title").hide();
}
localStorage.setItem("sidebar-expanded", this.sidebar_expanded);
+ const chevron_icon = this.sidebar_expanded
+ ? is_rtl
+ ? "chevron-right"
+ : "chevron-left"
+ : is_rtl
+ ? "chevron-left"
+ : "chevron-right";
this.wrapper
.find(".body-sidebar .collapse-sidebar-link")
.find("use")
- .attr("href", `#icon-panel-${direction}-open`);
+ .attr("href", `#icon-${chevron_icon}`);
this.sidebar_header.toggle_width(this.sidebar_expanded);
$(document).trigger("sidebar-expand", {
sidebar_expand: this.sidebar_expanded,
@@ -630,114 +623,103 @@ frappe.ui.Sidebar = class Sidebar {
set_workspace_sidebar(router) {
try {
- let route = frappe.get_route();
- let view, entity_name;
- switch (route.length) {
- case 1:
- view = "Page";
- entity_name = route[0];
- break;
- case 2:
- view = route[0];
- entity_name = route[1];
-
- if (frappe.boot.workspace_sidebar_item[entity_name.toLowerCase()]) {
- frappe.app.sidebar.setup(entity_name);
- return;
- }
- break;
- case 3:
- view = route[0];
- entity_name = route[1];
- if (route[0] == "Workspaces" && route[1] == "private") {
- entity_name = route[2];
- }
- break;
- default:
- entity_name = route[1];
+ const route = frappe.get_route();
+ let target;
+
+ if (route.length === 2 && frappe.boot.workspace_sidebar_item[route[1].toLowerCase()]) {
+ // route points directly at a workspace, e.g. List/
+ target = route[1];
+ } else {
+ const entity = this.entity_from_route(route);
+ const module = router?.meta?.module;
+ target = this.resolve_sidebar(entity, module);
}
- let sidebars = this.get_workspace_sidebars(entity_name);
- this.preferred_sidebars = sidebars;
- let module = router?.meta?.module;
- if (this.sidebar_title && sidebars.includes(this.sidebar_title)) {
- this.set_active_workspace_item();
- return;
+
+ // only rebuild when the target differs from the current sidebar, so
+ // this stays a cheap no-op when re-run by page-change / form-refresh
+ if (target && target !== this.sidebar_title) {
+ frappe.app.sidebar.setup(target);
}
+ } catch (e) {
+ console.error(e);
+ }
+
+ this.set_active_workspace_item();
+ }
+
+ entity_from_route(route) {
+ switch (route.length) {
+ case 1:
+ return route[0];
+ case 3:
+ return route[0] === "Workspaces" && route[1] === "private" ? route[2] : route[1];
+ default:
+ return route[0];
+ }
+ }
+
+ // Pick which workspace sidebar to show for the current route.
+ // Returns a workspace title (or null). Rules are ordered by priority:
+ // the first one that yields a sidebar wins.
+ resolve_sidebar(entity, module) {
+ let candidates = this.get_workspace_sidebars(entity);
+ this.preferred_sidebars = candidates;
+
+ const remembered = JSON.parse(localStorage.getItem("sidebar_item_map") || "{}");
+
+ let sidebar_name = null;
+
+ if (this.sidebar_title && candidates.includes(this.sidebar_title)) {
+ // 1. current sidebar already links to this entity -> keep it
+ sidebar_name = this.sidebar_title;
+ } else if (remembered[entity]?.length) {
+ // 2. previously remembered choice for this entity
+ sidebar_name = remembered[entity][0];
+ } else {
+ // 3. narrow candidates to the active app
if (module) {
- sidebars = this.filter_sidebars_from_app(
- sidebars,
+ candidates = this.filter_sidebars_from_app(
+ candidates,
frappe.boot.module_app[module.toLowerCase().replace(/[ -]/g, "_")]
);
}
- if (sidebars.length == 1) {
- frappe.app.sidebar.setup(sidebars[0]);
- } else if (sidebars.length > 1) {
- let sidebar = this.get_workspace_for_module(module);
- if (sidebars.includes(this.get_workspace_for_module(module))) {
- frappe.app.sidebar.setup(sidebar);
- }
+
+ // 4. resolve by what is left
+ if (candidates.length === 1) {
+ sidebar_name = candidates[0];
+ } else if (candidates.length > 1) {
+ sidebar_name = candidates.find((c) => c.toLowerCase() === module?.toLowerCase());
} else if (module) {
- this.show_sidebar_for_module(module);
+ sidebar_name = this.resolve_module_sidebar(module);
}
- } catch (e) {
- console.log(e);
}
-
- this.set_active_workspace_item();
+ if (!sidebar_name && candidates.length > 0) {
+ sidebar_name = candidates[0];
+ }
+ return sidebar_name;
}
filter_sidebars_from_app(sidebars, app) {
let filter_sidebars = [];
sidebars.forEach((sidebar) => {
- if (
- !filter_sidebars.includes(sidebar) &&
- frappe.boot.workspace_sidebar_item[sidebar.toLowerCase()].app === app
- ) {
+ const config = frappe.boot.workspace_sidebar_item[sidebar.toLowerCase()];
+ if (config && config.app === app && !filter_sidebars.includes(sidebar)) {
filter_sidebars.push(sidebar);
}
});
return filter_sidebars;
}
+ // Public entry point used by page/report views to switch the sidebar
+ // to the one for a module. Resolution itself lives in resolve_module_sidebar.
show_sidebar_for_module(module) {
if (this.sidebar_title && this.preferred_sidebars.includes(this.sidebar_title)) {
this.set_active_workspace_item();
return;
}
- if (this.sidebar_fixes && this.sidebar_title != module) return;
- let workspace_name = this.get_workspace_for_module(module);
- if (frappe.boot.workspace_sidebar_item[module.toLowerCase()]) {
- frappe.app.sidebar.setup(module);
- } else if (
- workspace_name &&
- frappe.boot.workspace_sidebar_item[workspace_name.toLowerCase()]
- ) {
- frappe.app.sidebar.setup(workspace_name);
- } else {
- let sidebars =
- this.sidebar_module_map[module] &&
- this.sidebar_module_map[module].sort((a, b) => {
- return a.localeCompare(b);
- });
- if (frappe.get_route())
- if (sidebars && sidebars.length) {
- frappe.app.sidebar.setup(sidebars[0]);
- }
- }
+ const target = this.resolve_module_sidebar(module);
+ if (target) frappe.app.sidebar.setup(target);
}
- set_sidebar_for_page() {
- let route = frappe.get_route();
- let views = ["List", "Form", "Workspaces", "query-report"];
- let matches = views.some((view) => route.includes(view));
- if (matches) return;
- let workspace_title;
- if (route.length == 2) {
- workspace_title = this.get_workspace_sidebars(route[1]);
- } else {
- workspace_title = this.get_workspace_sidebars(route[0]);
- }
- let module_name = workspace_title[0];
- if (module_name) {
- frappe.app.sidebar.setup(module_name || this.sidebar_title);
- }
+ resolve_module_sidebar(module) {
+ return frappe.boot.workspace_sidebar_item[module.toLowerCase()] ? module : null;
}
get_workspace_sidebars(link_to) {
@@ -752,4 +734,22 @@ frappe.ui.Sidebar = class Sidebar {
});
return sidebars;
}
+ setup_reload() {
+ const me = this;
+ this.item_sidebar_map = {};
+ $(window).on("beforeunload", function () {
+ me.store_last_show_sidebar_for_item();
+ });
+ }
+ store_last_show_sidebar_for_item() {
+ const me = this;
+ if (frappe.app.sidebar.active_item) {
+ let active_item = frappe.app.sidebar.active_item.parent().data("id");
+ if (!me.item_sidebar_map[active_item]) {
+ me.item_sidebar_map[active_item] = [];
+ }
+ me.item_sidebar_map[active_item].push(me.sidebar_title);
+ localStorage.setItem("sidebar_item_map", JSON.stringify(me.item_sidebar_map));
+ }
+ }
};
diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_header.js b/frappe/public/js/frappe/ui/sidebar/sidebar_header.js
index d1ade2eb9695..dcc0d91d5370 100644
--- a/frappe/public/js/frappe/ui/sidebar/sidebar_header.js
+++ b/frappe/public/js/frappe/ui/sidebar/sidebar_header.js
@@ -76,6 +76,7 @@ frappe.ui.SidebarHeader = class SidebarHeader {
action: "frappe.ui.toolbar.clear_cache()",
is_standard: 1,
icon: "rotate-ccw",
+ shortcut: "Shift+Ctrl+R",
},
{
name: "help",
@@ -213,6 +214,9 @@ frappe.ui.SidebarHeader = class SidebarHeader {
name: element.name,
label: element.item_label,
};
+ if (element.action?.includes("frappe.ui.toolbar.show_shortcuts")) {
+ dropdown_children.shortcut = "Shift+/";
+ }
if (element.item_type === "Route") {
dropdown_children.url = element.route;
}
@@ -234,6 +238,7 @@ frappe.ui.SidebarHeader = class SidebarHeader {
name: "toggle-theme",
label: __("Toggle Theme"),
icon: is_dark ? "sun" : "moon",
+ shortcut: "Shift+Ctrl+G",
onClick: function () {
new frappe.ui.ThemeSwitcher().show();
},
@@ -250,6 +255,7 @@ frappe.ui.SidebarHeader = class SidebarHeader {
name: "toggle-sidebar",
label: __("Toggle Sidebar"),
icon: "panel-right-open",
+ shortcut: "Ctrl+/",
onClick: function () {
sidebar.toggle_width();
},
@@ -357,6 +363,13 @@ frappe.ui.SidebarHeader = class SidebarHeader {
}
+ ${
+ item.shortcut
+ ? ``
+ : ""
+ }
`).appendTo(this.dropdown_menu);
}
diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js
index 77d9691e3188..4b745bfbaa4c 100644
--- a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js
+++ b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js
@@ -33,7 +33,7 @@ frappe.ui.sidebar_item.TypeLink = class SidebarItem {
path = frappe.utils.generate_route(args);
} else if (this.item.link_type == "Workspace") {
let workspaces = frappe.workspaces[frappe.router.slug(this.item.link_to)];
- if (workspaces.public) {
+ if (workspaces && workspaces.public) {
path = "/desk/" + frappe.router.slug(this.item.link_to);
} else {
path = "/desk/private/" + frappe.router.slug(this.item.link_to);
@@ -60,16 +60,29 @@ frappe.ui.sidebar_item.TypeLink = class SidebarItem {
let filters_json = JSON.parse(
frappe.utils.get_filter_as_json(JSON.parse(this.item.filters))
);
+ filters_json = this.transform_filters(filters_json);
if (this.item.link_type == "DocType") {
args.doc_view = "List";
args.route_options = filters_json;
}
+ } else if (this.item.route_options && this.item.link_type == "DocType") {
+ args.doc_view = "List";
+ args.route_options = JSON.parse(this.item.route_options);
}
path = frappe.utils.generate_route(args);
}
}
return path;
}
+ transform_filters(filters_json) {
+ for (const [key, value] of Object.entries(filters_json)) {
+ if (Array.isArray(value)) {
+ filters_json[key] = value[1];
+ }
+ }
+ return filters_json;
+ }
+
prepare() {}
make() {
this.path = this.get_path();
@@ -98,9 +111,7 @@ frappe.ui.sidebar_item.TypeLink = class SidebarItem {
}
}
get_shortcut_html(shortcut) {
- if (frappe.utils.is_mac()) {
- shortcut = shortcut.replace("Ctrl+", "⌘");
- }
+ shortcut = frappe.ui.keys.get_shortcut_label(shortcut);
return `
${shortcut} `;
}
setup_editing_controls() {
diff --git a/frappe/public/js/frappe/ui/toolbar/about.js b/frappe/public/js/frappe/ui/toolbar/about.js
index 242546f76262..216c762699be 100644
--- a/frappe/public/js/frappe/ui/toolbar/about.js
+++ b/frappe/public/js/frappe/ui/toolbar/about.js
@@ -5,88 +5,70 @@ frappe.ui.misc.about = function () {
return;
}
- const dialog = new frappe.ui.Dialog({ title: __("Frappe Framework") });
+ const dialog = new frappe.ui.Dialog({ title: __("About") });
+ $(dialog.wrapper).addClass("about-dialog");
$(dialog.body).html(
- `
-
${__("Open Source Applications for the Web")}
-
-
-
- ${__("Website")}:
- https://frappe.io/
-
-
-
-
- ${__("Source Code")}:
- https://github.com/frappe
-
-
-
-
- ${__("Frappe Blog")}:
- https://frappe.io/blog
-
-
-
-
- ${__("Frappe Forum")}:
- https://discuss.frappe.io
-
-
-
-
- ${__("LinkedIn")}:
- https://linkedin.com/company/frappe-tech
-
-
-
-
- X:
- https://x.com/frappetech
-
-
-
-
- ${__("YouTube")}:
- https://www.youtube.com/@frappetech
-
-
-
-
- ${__("Instagram")}:
- https://www.instagram.com/frappetech
-
-
-
-
-
-
${__("Installed Apps")}
-
- ${frappe.utils.icon("clipboard")}
-
+ `
+
+
+
+
+
+
${__("Frappe Framework Version")}
+
+ ${__("Loading...")}
+
+
+
+ ${
+ frappe.boot.is_fc_site
+ ? `
+
`
+ : ""
+ }
+
+
+
${__("Installed Apps")}
+
+
+
`
);
+ $(dialog.footer)
+ .removeClass("hide")
+ .prepend(
+ ``
+ );
+
frappe.ui.misc.about_dialog = dialog;
frappe.ui.misc.about_dialog.on_page_show = function () {
@@ -102,55 +84,61 @@ frappe.ui.misc.about = function () {
}
};
- const show_versions = function (versions) {
- const $wrap = $("#about-app-versions").empty();
- let app = {};
-
- function get_version_text(app) {
- if (app.branch) {
- return `v${app.branch_version || app.version} (${app.branch})`;
- } else {
- return `v${app.version}`;
- }
+ const get_version_text = function (app) {
+ const is_pr_branch = app.branch && /^pr-\d+/i.test(app.branch);
+ if (app.branch && !is_pr_branch) {
+ return `${app.version} (${app.branch})`;
}
+ return app.version;
+ };
- for (const app_name in versions) {
- app = versions[app_name];
- const title = `${app_name}: ${app.branch_version || app.version}`;
- const text = `
- ${app.title}: ${get_version_text(app)}
-
`;
- $(text).appendTo($wrap);
+ const render_app_icon = function (app_name, app) {
+ const first_letter = (app.title || app_name).charAt(0).toUpperCase();
+ if (app.logo) {
+ return `
`;
}
-
- frappe.versions = versions;
-
- if (frappe.versions) {
- $(dialog.body).find("#copy-apps-info").removeClass("hidden");
+ if (app.color) {
+ return `
${first_letter}
`;
}
+ const palette = frappe.get_palette(app_name);
+ return `
${first_letter}
`;
};
- const code_block = (snippet, lang = "") => "```" + lang + "\n" + snippet + "\n```";
-
- // Listener for copying installed apps info
- $(dialog.body).on("click", "#copy-apps-info", function () {
- if (!frappe.versions) return;
+ const show_versions = function (versions) {
+ if (versions.frappe) {
+ $("#about-framework-version").text(`frappe: ${get_version_text(versions.frappe)}`);
+ }
- const versions = Object.entries(frappe.versions).reduce((acc, [key, app]) => {
- acc[key] = app.branch_version || app.version;
- return acc;
- }, {});
+ // Show update button on Frappe Cloud sites when updates are available
+ const $version_row = $("#about-framework-version").closest(".about-info-row");
+ $version_row.find(".about-update-indicator").remove();
+ if (frappe.boot.has_app_updates && frappe.boot.is_fc_site) {
+ $(`
+ ${__("Update Available")}
+ `).appendTo($version_row);
+ }
- frappe.utils.copy_to_clipboard(code_block(JSON.stringify(versions, null, "\t"), "json"));
- });
+ const $wrap = $("#about-app-versions").empty();
- // Listener for copy app version
- $(dialog.body).on("click", ".app-version", function () {
- const title = $(this).attr("title");
- if (title) {
- frappe.utils.copy_to_clipboard(title);
+ for (const app_name in versions) {
+ if (app_name === "frappe") continue;
+ const app = versions[app_name];
+ const version_text = get_version_text(app);
+ const title = `${app_name}: ${app.version}`;
+
+ $(`
+ ${render_app_icon(app_name, app)}
+
+
${__(app.title)}
+
${app_name}: ${version_text}
+
+
`).appendTo($wrap);
}
- });
+
+ frappe.versions = versions;
+ };
frappe.ui.misc.about_dialog.show();
};
diff --git a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js
index 54395363e583..1b15f0957565 100644
--- a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js
+++ b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js
@@ -52,6 +52,10 @@ frappe.search.AwesomeBar = class AwesomeBar {
${frappe.utils.is_mac() ? "⌘K" : "Ctrl+K"}
${__("to close")}
+
+ ${frappe.utils.is_mac() ? "⌘G" : "Ctrl+G"}
+ ${__("to open Global Search")}
+
`;
diff --git a/frappe/public/js/frappe/ui/toolbar/search.html b/frappe/public/js/frappe/ui/toolbar/search.html
index 618046149324..976fa18c61c3 100644
--- a/frappe/public/js/frappe/ui/toolbar/search.html
+++ b/frappe/public/js/frappe/ui/toolbar/search.html
@@ -1,8 +1,9 @@
-
+
-
diff --git a/frappe/public/js/frappe/ui/toolbar/search.js b/frappe/public/js/frappe/ui/toolbar/search.js
index 4c9d5fe6d5ae..22c387b49a2e 100644
--- a/frappe/public/js/frappe/ui/toolbar/search.js
+++ b/frappe/public/js/frappe/ui/toolbar/search.js
@@ -1,5 +1,14 @@
frappe.provide("frappe.search");
+/** First N Global Search Settings rows used as implicit default pins. Toolbar shows every pinned DocType. */
+const GLOBAL_SEARCH_VISIBLE_DT_LIMIT = 5;
+const GLOBAL_SEARCH_PIN_STORAGE_KEY = "global-search-pinned-doctypes";
+
+/** “All” summary: per-doctype “Show more” at or above this count when browsing all DocTypes. */
+const GLOBAL_SEARCH_SUMMARY_SHOW_MORE_MIN = 5;
+/** Extra field values beyond this show as “and n more”. */
+const GLOBAL_SEARCH_FIELD_INLINE_PREVIEW_LIMIT = 3;
+
frappe.search.SearchDialog = class {
constructor(opts) {
$.extend(this, opts);
@@ -8,8 +17,10 @@ frappe.search.SearchDialog = class {
make() {
this.search_dialog = new frappe.ui.Dialog({
+ animate: false,
minimizable: true,
- size: "large",
+ size: "extra-large",
+ on_page_show: () => this.focus_global_search_input(),
});
this.set_header();
this.$wrapper = $(this.search_dialog.$wrapper).addClass("search-dialog");
@@ -18,6 +29,14 @@ frappe.search.SearchDialog = class {
this.setup();
}
+ /** Header `.search-input` is outside `Dialog.fields_list`; base `focus_on_first_input` skips it. */
+ focus_global_search_input() {
+ const input = this.$input?.get?.(0);
+ if (!input) return;
+ input.focus({ preventScroll: true });
+ if ((input.value || "").length) input.select();
+ }
+
set_header() {
this.search_dialog.header
.addClass("search-header")
@@ -30,6 +49,31 @@ frappe.search.SearchDialog = class {
${frappe.utils.icon("search")}
`
);
+ const $actions = this.search_dialog.$wrapper.find(".modal-actions");
+ $actions.find(".btn-open-global-search-settings").remove();
+ const $gs = this.make_global_search_settings_btn();
+ if ($gs) {
+ $actions.prepend($gs);
+ }
+ }
+
+ make_global_search_settings_btn() {
+ if (!frappe.model.can_read("Global Search Settings")) return null;
+ const label = __("Configure search settings");
+ return $(
+ `
`
+ )
+ .attr({
+ title: label,
+ "aria-label": label,
+ })
+ .append(frappe.utils.icon("settings", "sm"))
+ .on("click", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.search_dialog.hide();
+ frappe.set_route("Form", "Global Search Settings", "Global Search Settings");
+ });
}
setup() {
@@ -38,30 +82,24 @@ frappe.search.SearchDialog = class {
this.more_count = 20;
this.full_lists = {};
this.nav_lists = {};
+ this.global_doctype_filter = "";
+ this._global_search_settings_promise = null;
this.init_search_objects();
this.bind_input();
this.bind_events();
}
init_search_objects() {
+ const log_err = (err) => console.error(err);
this.searches = {
global_search: {
input_placeholder: __("Search"),
empty_state_text: __("Search for anything"),
no_results_status: () => __("No Results found"),
get_results: (keywords, callback) => {
- let start = 0,
- limit = 100;
- let results = frappe.search.utils.get_nav_results(keywords);
- frappe.search.utils.get_global_results(keywords, start, limit).then(
- (global_results) => {
- results = results.concat(global_results);
- callback(results, keywords);
- },
- (err) => {
- console.error(err);
- }
- );
+ frappe.search.utils
+ .get_global_results(keywords, 0, null, this.global_doctype_filter || "")
+ .then((global_results) => callback(global_results, keywords), log_err);
},
},
tags: {
@@ -70,16 +108,9 @@ frappe.search.SearchDialog = class {
no_results_status: (keyword) =>
"" + __("No documents found tagged with {0}", [keyword]) + "
",
get_results: (keywords, callback) => {
- var results = frappe.search.utils.get_nav_results(keywords);
- frappe.tags.utils.get_tag_results(keywords).then(
- (global_results) => {
- results = results.concat(global_results);
- callback(results, keywords);
- },
- (err) => {
- console.error(err);
- }
- );
+ frappe.tags.utils
+ .get_tag_results(keywords)
+ .then((global_results) => callback(global_results, keywords), log_err);
},
},
};
@@ -98,42 +129,60 @@ frappe.search.SearchDialog = class {
}
put_placeholder(status_text) {
- var $placeholder = $(`
-
+ let $shell = $(frappe.render_template("search")).addClass("hide");
+ const tipLine =
+ __("Use ampersand to match multiple terms") + " (" + __("e.g.") + " Marie&John)";
+ const awesomebarShortcut = frappe.utils.is_mac() ? "⌘K" : "Ctrl+K";
+ const awesomebarTipLine = `
${frappe.utils.escape_html(
+ awesomebarShortcut
+ )} ${frappe.utils.escape_html(__("to open Awesome Bar"))}`;
+ const show_ampersand_empty_tip =
+ status_text === __("Search for anything") &&
+ this.search === this.searches["global_search"];
+ const ampersandBlock = show_ampersand_empty_tip
+ ? `
${frappe.utils.escape_html(
+ tipLine
+ )}
+
${awesomebarTipLine}
`
+ : "";
+ $shell.find(".results-area").html(
+ `
-
${status_text}
+
${frappe.utils.escape_html(status_text)}
+ ${ampersandBlock}
-
-
`);
- this.update($placeholder);
+
`
+ );
+ this.update($shell);
+ this.sync_global_search_filter_bar();
}
-
+ /** Binds input event to the search input and debounces the search input. */
bind_input() {
- this.$input.on("input", (e) => {
- const $el = $(e.currentTarget);
- clearTimeout($el.data("timeout"));
- $el.data(
- "timeout",
- setTimeout(() => {
- if (this.$input.val() === this.current_keyword) return;
- let keywords = this.$input.val();
- if (keywords.length > 1) {
- this.get_results(keywords);
- } else {
- this.current_keyword = "";
- this.put_placeholder(this.search.empty_state_text);
- }
- }, 300)
- );
+ const wait_ms = 300;
+ this._debounced_search_input = frappe.utils.debounce(() => {
+ if (this.$input.val() === this.current_keyword) return;
+ const keywords = this.$input.val();
+ if (keywords.length > 1) {
+ this.get_results(keywords);
+ } else {
+ this.current_keyword = "";
+ this.global_doctype_filter = "";
+ this.put_placeholder(this.search.empty_state_text);
+ }
+ }, wait_ms);
+ this.$input.on("input", () => this._debounced_search_input());
+ this.$wrapper.on("hide.bs.modal.globalSearch", () => {
+ this._debounced_search_input?.cancel?.();
});
}
+ /** Click handlers for search UI: sidebar sections, DocType filters/pins, “More” / pagination, and tag→global switch. */
bind_events() {
- // Sidebar
+ /** Sidebar navigation: click a section link to show its results. */
this.$body.on("click", ".list-link", (e) => {
const $link = $(e.currentTarget);
this.$body.find(".search-sidebar").find(".list-link").removeClass("active selected");
@@ -143,17 +192,65 @@ frappe.search.SearchDialog = class {
this.$body.find(".module-section-link").first().focus();
});
- // Summary more links
+ /** “Show more” links: global search or DocType-specific “More”. */
this.$body.on("click", ".section-more", (e) => {
- const $section = $(e.currentTarget);
- const type = $section.attr("data-category");
- this.$body
- .find(".search-sidebar")
- .find('*[data-category="' + type + '"]')
- .trigger("click");
+ e.preventDefault();
+ const type = $(e.currentTarget).attr("data-category");
+ if ($(e.currentTarget).attr("data-fetch-type") === "Global") {
+ this.apply_global_doctype_filter(type || "");
+ return;
+ }
+ this.$body.find(".search-sidebar").find(`*[data-category="${type}"]`).trigger("click");
+ });
+
+ /** DocType filter pills: apply DocType filter and show its results. */
+ this.$body.on("click", ".global-search-filter-pill", (e) => {
+ e.stopPropagation();
+ this.apply_global_doctype_filter($(e.currentTarget).attr("data-doctype") || "");
});
- // Back-links (mobile-view)
+ this.$wrapper.on("click", (e) => {
+ if (!$(e.target).closest(".global-search-more-dropdown-wrap").length)
+ this.close_global_search_more_menus();
+ });
+
+ this.$body.on("click", ".global-search-more-trigger", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const $trigger = $(e.currentTarget);
+ const $dd = $trigger.siblings(".global-search-more-dropdown");
+ const opening = !$dd.hasClass("menu-open");
+ this.close_global_search_more_menus();
+ if (opening) {
+ $dd.addClass("menu-open");
+ this.align_global_search_more_dropdown($dd, $trigger);
+ }
+ });
+
+ this.$body.on("click", ".global-search-more-dropdown", (e) => e.stopPropagation());
+
+ /** DocType selection from “More” dropdown: apply DocType filter and close dropdown. */
+ this.$body.on("click", ".global-search-dd-pick-doctype", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.close_global_search_more_menus();
+ this.apply_global_doctype_filter($(e.currentTarget).attr("data-doctype") || "");
+ });
+
+ /** DocType pin buttons: toggle DocType pin state and sync filter bar. */
+ this.$body.on("click", ".global-search-pin-btn", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const $btn = $(e.currentTarget);
+ const dt = $btn.attr("data-doctype");
+ const action = $btn.attr("data-pin-action") || "";
+ if (!dt) return;
+ if (action === "remove") this.remove_global_search_pin(dt);
+ else if (action === "add") this.add_global_search_pin(dt);
+ this.sync_global_search_filter_bar();
+ });
+
+ /** Back-links (mobile-view): click to show all results. */
this.$body.on("click", ".all-results-link", () => {
this.$body
.find(".search-sidebar")
@@ -161,18 +258,34 @@ frappe.search.SearchDialog = class {
.trigger("click");
});
- // Full list more links
+ /** Full list “More” buttons: fetch more results from the current DocType. */
this.$body.on("click", ".list-more", (e) => {
- const $el = $(e.currentTarget);
- const type = $el.attr("data-category");
- const fetch_type = $el.attr("data-search");
- var current_count = this.$body.find(".result").length;
+ e.preventDefault();
+ const $trigger = $(e.currentTarget);
+ const type = $trigger.attr("data-category");
+ const fetch_type = $trigger.attr("data-search");
+ const $panel = $trigger.closest(".global-search-table-panel");
+ const $list = $panel.find(".global-search-results-list").first();
+ var current_count = $list.length
+ ? Math.max(0, $list.children(".list-row-container").length - 1)
+ : this.$body.find(".result").length;
if (fetch_type === "Global") {
frappe.search.utils
- .get_global_results(this.current_keyword, current_count, this.more_count, type)
+ .get_global_results(
+ this.current_keyword,
+ current_count,
+ this.more_count,
+ this.global_doctype_filter || type
+ )
.then(
(doctype_results) => {
- doctype_results.length && this.add_more_results(doctype_results);
+ doctype_results.length &&
+ this.add_more_global_table_rows(
+ type,
+ doctype_results,
+ $trigger,
+ $list
+ );
},
(err) => {
console.error(err);
@@ -185,7 +298,7 @@ frappe.search.SearchDialog = class {
}
});
- // Switch to global search link
+ // Switch to global search link (keeps existing DocType toolbar filter).
this.$body.on("click", ".switch-to-global-search", () => {
this.search = this.searches["global_search"];
this.$input.attr("placeholder", this.search.input_placeholder);
@@ -195,13 +308,30 @@ frappe.search.SearchDialog = class {
}
init_search(keywords, search_type) {
- this.search = this.searches[search_type];
- this.$input.attr("placeholder", this.search.input_placeholder);
- this.put_placeholder(this.search.empty_state_text);
- this.get_results(keywords);
+ keywords = (keywords || "").trim();
+ this._reset_global_search_modal_mode(search_type);
+ this.search_dialog.show();
+ this.$input.val(keywords);
+ if (keywords.length > 1) {
+ this.get_results(keywords);
+ } else {
+ this.current_keyword = keywords;
+ this.put_placeholder(this.search.empty_state_text);
+ }
+ }
+
+ /** Keyboard shortcut: open full Global Search panel (not the Awesome Bar). */
+ open_global_search_dialog(keywords) {
+ keywords = (keywords || "").trim();
+ this._reset_global_search_modal_mode("global_search");
this.search_dialog.show();
this.$input.val(keywords);
- setTimeout(() => this.$input.select(), 500);
+ if (keywords.length > 1) {
+ this.get_results(keywords);
+ } else {
+ this.current_keyword = keywords;
+ this.put_placeholder(this.search.empty_state_text);
+ }
}
get_results(keywords) {
@@ -213,6 +343,7 @@ frappe.search.SearchDialog = class {
}
if (this.current_keyword.charAt(0) === "#") {
+ this.global_doctype_filter = "";
this.search = this.searches["tags"];
} else {
this.search = this.searches["global_search"];
@@ -245,26 +376,530 @@ frappe.search.SearchDialog = class {
};
this.nav_lists = {};
+ let global_nonempty = [];
+ let nav_nonempty = [];
result_sets.forEach((set) => {
- $sidebar.append($(__(sidebar_item_html, [set.title, __(set.title)])));
- this.add_section_to_summary(set.title, set.results);
+ if (set.results.length === 0) return;
+ if (set.fetch_type === "Global") {
+ global_nonempty.push(set);
+ } else {
+ nav_nonempty.push(set);
+ }
+ });
+
+ const prepend_all = global_nonempty.length >= 1 || nav_nonempty.length > 1;
+
+ if (prepend_all) {
+ $sidebar.prepend($(__(sidebar_item_html, ["All Results", __("All Results")])));
+ }
+
+ const register_sidebar_section = (set, with_sidebar_entry) => {
+ if (with_sidebar_entry) {
+ $sidebar.append($(__(sidebar_item_html, [set.title, __(set.title)])));
+ }
+ this.add_section_to_summary(set.title, set.results, set.fetch_type);
this.full_lists[set.title] = this.render_full_list(
set.title,
set.results,
set.fetch_type
);
+ };
+
+ nav_nonempty.forEach((set) => register_sidebar_section(set, true));
+ global_nonempty.forEach((set) => register_sidebar_section(set, false));
+
+ const show_global_filters = this.search !== this.searches["tags"];
+
+ const finish_draw = () => {
+ this.update($search_results);
+ const $pill_host = $(this.search_dialog.body).find(".global-search-doctype-filters");
+ this.toggle_global_search_filters($pill_host, show_global_filters);
+ if (show_global_filters) {
+ this.render_doctype_filter_bar_into($pill_host);
+ }
+ $(this.search_dialog.body).find(".list-link").first().trigger("click");
+ };
+
+ if (show_global_filters) {
+ this.ensure_global_search_allowed_doctypes().then(() => finish_draw());
+ } else {
+ finish_draw();
+ }
+ }
+
+ /** Syncs DocType filter bar: shows/hides based on current search mode (global vs. per-DocType). */
+ sync_global_search_filter_bar() {
+ if (this.search === this.searches["tags"]) {
+ const $pill_host = this.$body.find(".global-search-doctype-filters");
+ this.toggle_global_search_filters($pill_host, false);
+ return;
+ }
+ const redraw = () => {
+ const $pill_host = this.$body.find(".global-search-doctype-filters");
+ this.toggle_global_search_filters($pill_host, true);
+ this.render_doctype_filter_bar_into($pill_host);
+ };
+ /** Promise `.then` runs on a microtask — pin/unpin would update one frame late. */
+ if (this._cached_global_search_allowed !== undefined) {
+ redraw();
+ return;
+ }
+ this.ensure_global_search_allowed_doctypes().then(redraw);
+ }
+
+ /** Toggles visibility of the DocType filter bar: shows/hides based on current search mode (global vs. per-DocType). */
+ toggle_global_search_filters($host, visible) {
+ if (visible) {
+ $host.removeClass("hide");
+ } else {
+ $host.empty().addClass("hide");
+ }
+ }
+
+ /** Applies a DocType filter: updates the global_doctype_filter and fetches results. */
+ apply_global_doctype_filter(dt) {
+ this.global_doctype_filter = dt || "";
+ this.get_results(this.current_keyword);
+ }
+
+ /** Clears DocType filter and switches modal to `global_search` or `tags` mode. */
+ _reset_global_search_modal_mode(search_type) {
+ this.global_doctype_filter = "";
+ this.search = this.searches[search_type];
+ this.$input.attr("placeholder", this.search.input_placeholder);
+ }
+
+ /** Shared navigation for list/global rows and classic result tiles (route + stale-hash reroute). */
+ _open_search_result_route(result) {
+ if (!result.route) return;
+ if (result.route_options) {
+ frappe.route_options = result.route_options;
+ }
+ const previous_hash = window.location.hash;
+ frappe.set_route(result.route);
+ if (window.location.hash === previous_hash) {
+ frappe.router.route();
+ }
+ }
+
+ /** Closes the “More” dropdown menus: removes the `menu-open` class from the dropdown. */
+ close_global_search_more_menus() {
+ this.$wrapper.find(".global-search-more-dropdown").removeClass("menu-open align-right");
+ }
+
+ align_global_search_more_dropdown($dropdown, $trigger) {
+ const apply = () => {
+ const b = $trigger[0].getBoundingClientRect();
+ const c = this.$wrapper[0].getBoundingClientRect();
+ const left_edge = Math.max(c.left, 8);
+ const right_edge = Math.min(c.right, window.innerWidth) - 8;
+ const w =
+ $dropdown.get(0).getBoundingClientRect().width ||
+ $dropdown.outerWidth() ||
+ Math.min(384, window.innerWidth * 0.92);
+ const fits_opening_right = b.left + w <= right_edge;
+ const fits_opening_left = b.right - w >= left_edge;
+ let align_right = false;
+ if (!fits_opening_right && fits_opening_left) {
+ align_right = true;
+ } else if (!fits_opening_right && !fits_opening_left) {
+ align_right = b.right - left_edge > right_edge - b.left;
+ }
+ $dropdown.toggleClass("align-right", align_right);
+ };
+ apply();
+ requestAnimationFrame(apply);
+ }
+
+ /** Returns a set of allowed DocTypes for Global Search: based on Global Search Settings. */
+ _global_search_allowed_set() {
+ const allow = Object.create(null);
+ for (const d of this._cached_global_search_allowed || []) if (d) allow[d] = 1;
+ return allow;
+ }
+
+ /** Ensures the Global Search Settings are loaded and cached: fetches from the server if needed. */
+ ensure_global_search_allowed_doctypes() {
+ if (!this._global_search_settings_promise) {
+ this._global_search_settings_promise = new Promise((resolve) => {
+ /** Fetches the Global Search Settings from the server and caches the results. */
+ frappe.call({
+ method: "frappe.client.get",
+ args: {
+ doctype: "Global Search Settings",
+ name: "Global Search Settings",
+ },
+ callback: (res) => {
+ /** If the Global Search Settings are loaded successfully, cache the results. */
+ if (!res.exc && res.message && res.message.allowed_in_global_search) {
+ const rows = (res.message.allowed_in_global_search || []).slice();
+ rows.sort(
+ (a, b) => (parseInt(a.idx, 10) || 0) - (parseInt(b.idx, 10) || 0)
+ );
+ this._cached_global_search_allowed = rows
+ .map((row) => row.document_type)
+ .filter(Boolean);
+ } else {
+ this._cached_global_search_allowed = [];
+ }
+ resolve(this._cached_global_search_allowed);
+ },
+ error: () => {
+ this._cached_global_search_allowed = [];
+ resolve([]);
+ },
+ });
+ });
+ }
+ return this._global_search_settings_promise;
+ }
+
+ /**
+ * Reads the global search pin state from localStorage.
+ */
+ read_global_search_pin_state() {
+ const key = GLOBAL_SEARCH_PIN_STORAGE_KEY;
+ /** Reads the global search pin state from localStorage. */
+ let raw = null;
+ try {
+ raw = localStorage.getItem(key);
+ } catch (_) {
+ raw = null;
+ }
+ if (raw === null) {
+ try {
+ const u = frappe.session && frappe.session.user ? frappe.session.user : "guest";
+ const legacy_key = "frappe_global_search_pins::" + encodeURIComponent(u);
+ const legacy_val = localStorage.getItem(legacy_key);
+ if (legacy_val !== null) {
+ localStorage.setItem(key, legacy_val);
+ localStorage.removeItem(legacy_key);
+ raw = legacy_val;
+ }
+ } catch (_) {
+ /* ignore */
+ }
+ }
+ if (raw === null) return { explicit: false, pins: [] };
+ try {
+ let arr = JSON.parse(raw);
+ if (!Array.isArray(arr)) return { explicit: true, pins: [] };
+ return { explicit: true, pins: arr.filter((d) => !!d) };
+ } catch {
+ return { explicit: true, pins: [] };
+ }
+ }
+
+ /** Writes the global search pin state to localStorage. */
+ write_global_search_pins(pins) {
+ const uniq = [];
+ const seen = {};
+ (pins || []).forEach(function (dt) {
+ if (!dt || seen[dt]) return;
+ seen[dt] = 1;
+ uniq.push(dt);
});
+ try {
+ localStorage.setItem(GLOBAL_SEARCH_PIN_STORAGE_KEY, JSON.stringify(uniq));
+ } catch (e) {
+ /* quota or private mode */
+ }
+ }
- if (result_sets.length > 1) {
- $sidebar.prepend($(__(sidebar_item_html, ["All Results", __("All Results")])));
+ /** Returns the default pinned DocTypes: based on Global Search Settings. */
+ default_pinned_doctypes_from_settings() {
+ const allowed = this._cached_global_search_allowed || [];
+ const lim = GLOBAL_SEARCH_VISIBLE_DT_LIMIT;
+ return allowed.slice(0, Math.min(lim, allowed.length));
+ }
+
+ /** Returns the current effective list of pinned DocTypes: based on Global Search Settings and localStorage. */
+ current_effective_pins_list() {
+ const allow = this._global_search_allowed_set();
+ const st = this.read_global_search_pin_state();
+ const base = !st.explicit ? this.default_pinned_doctypes_from_settings() : st.pins;
+ return base.filter((d) => allow[d]);
+ }
+
+ /** Adds a DocType to the pinned list: updates localStorage. */
+ add_global_search_pin(dt) {
+ const allow = this._global_search_allowed_set();
+ if (!dt || !allow[dt]) return;
+
+ const st = this.read_global_search_pin_state();
+ let next = !st.explicit
+ ? this.default_pinned_doctypes_from_settings().filter((d) => allow[d])
+ : st.pins.filter((d) => allow[d]).slice();
+ if (next.indexOf(dt) === -1) next.push(dt);
+ this.write_global_search_pins(next);
+ }
+
+ /** Removes a DocType from the pinned list: updates localStorage. */
+ remove_global_search_pin(dt) {
+ const allow = this._global_search_allowed_set();
+ const st = this.read_global_search_pin_state();
+ const next = !st.explicit
+ ? this.default_pinned_doctypes_from_settings().filter((d) => allow[d] && d !== dt)
+ : st.pins.filter((d) => allow[d] && d !== dt);
+ this.write_global_search_pins(next);
+ }
+
+ /** Builds the DOM for the “More” dropdown menu: shows pinned and unpinned DocTypes. */
+ build_global_search_more_menu_dom(pinned_ordered, unpinned_ordered) {
+ const pin_icon = frappe.utils.icon("pin", "xs");
+ const unpin_icon = frappe.utils.icon("pin-off", "xs");
+ let html = "";
+ const row = (dt, label, action) => {
+ const esc = frappe.utils.escape_html(dt || "");
+ const safe_label = frappe.utils.escape_html(label || dt || "");
+ let pin_btn = "";
+ if (action === "unpin") {
+ pin_btn = `${unpin_icon} `;
+ } else if (action === "pin") {
+ pin_btn = `${pin_icon} `;
+ }
+ return `
+
${safe_label}
+
${pin_btn}
+
`;
+ };
+
+ html += `${__(
+ "Pinned"
+ )}
`;
+ if (pinned_ordered.length) {
+ pinned_ordered.forEach((dt) => {
+ html += row(dt, __(dt), "unpin");
+ });
+ } else {
+ html += `
${__(
+ "No pinned document types."
+ )}
`;
}
+ html += `
${__(
+ "Other"
+ )}
`;
+ if (unpinned_ordered.length) {
+ unpinned_ordered.forEach((dt) => {
+ html += row(dt, __(dt), "pin");
+ });
+ } else {
+ html += `
${__(
+ "No other document types."
+ )}
`;
+ }
+ html += "
";
+ return html;
+ }
+
+ /** Renders the DocType filter bar into the host element: shows pinned and unpinned DocTypes. */
+ render_doctype_filter_bar_into($host) {
+ const keep_more_dropdown_open =
+ $host.find(".global-search-more-dropdown.menu-open").length > 0;
+
+ const dt_allowed = this._cached_global_search_allowed || [];
+ let pinned_matching = this.current_effective_pins_list();
+ let unpinned_ordered = dt_allowed.filter((dt) => !pinned_matching.includes(dt));
+ /** One chip per pinned DocType (strip wraps); not capped here. */
+ let visible_doctypes = pinned_matching.slice();
+ let active_dt = this.global_doctype_filter || "";
+ /** Selected DocType must appear in the toolbar if it is not among the visible chips. */
+ let show_overflow_chip =
+ !!active_dt &&
+ dt_allowed.indexOf(active_dt) !== -1 &&
+ visible_doctypes.indexOf(active_dt) === -1;
+
+ let $toolbar = $('