Merge branch 'develop' into computed_child_table

This commit is contained in:
Sagar Vora 2025-10-14 12:53:32 +05:30
commit a93530e221
210 changed files with 87632 additions and 18133 deletions

View file

@ -61,3 +61,6 @@ e9bbe03354079cfcef65a77b0c33f57b047a7c93
# replace `frappe.flags.in_test` with `frappe.in_test`
653c80b8483cc41aef25cd7d66b9b6bb188bf5f8
# another ruff update
6ca4d4d167a1a009d99062747711de7a994aa633

View file

@ -108,7 +108,7 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
id: yarn-cache

View file

@ -93,7 +93,7 @@ jobs:
- frappe/hrms
steps:
- name: Dispatch Downstream CI (if supported)
uses: peter-evans/repository-dispatch@v3
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.CI_PAT }}
repository: ${{ matrix.repo }}

View file

@ -1,5 +1,6 @@
exclude: 'node_modules|.git'
default_stages: [pre-commit]
default_install_hook_types: [pre-commit, commit-msg]
fail_fast: false
@ -21,7 +22,7 @@ repos:
exclude: ^frappe/tests/classes/context_managers\.py$
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.1
rev: v0.13.2
hooks:
- id: ruff
name: "Run ruff import sorter"
@ -69,6 +70,13 @@ repos:
frappe/public/js/lib/.*
)$
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.22.0
hooks:
- id: commitlint
stages: [commit-msg]
additional_dependencies: ['conventional-changelog-conventionalcommits']
ci:
autoupdate_schedule: weekly
skip: []

View file

@ -1,6 +1,7 @@
**/hooks.py,frappe.gettext.extractors.navbar.extract
**/doctype/*/*.json,frappe.gettext.extractors.doctype.extract
**/workspace/*/*.json,frappe.gettext.extractors.workspace.extract
**/web_form/*/*.json,frappe.gettext.extractors.web_form.extract
**/onboarding_step/*/*.json,frappe.gettext.extractors.onboarding_step.extract
**/module_onboarding/*/*.json,frappe.gettext.extractors.module_onboarding.extract
**/report/*/*.json,frappe.gettext.extractors.report.extract

1 **/hooks.py frappe.gettext.extractors.navbar.extract
2 **/doctype/*/*.json frappe.gettext.extractors.doctype.extract
3 **/workspace/*/*.json frappe.gettext.extractors.workspace.extract
4 **/web_form/*/*.json frappe.gettext.extractors.web_form.extract
5 **/onboarding_step/*/*.json frappe.gettext.extractors.onboarding_step.extract
6 **/module_onboarding/*/*.json frappe.gettext.extractors.module_onboarding.extract
7 **/report/*/*.json frappe.gettext.extractors.report.extract

View file

@ -75,14 +75,15 @@ context("Form", () => {
cy.get('.frappe-control[data-fieldname="email_ids"]').as("table");
cy.get("@table").find("button.grid-add-row").click();
cy.get("@table").find("button.grid-add-row").click();
cy.get("@table").find('[data-idx="1"]').as("row1");
cy.get("@table").find('[data-idx="2"]').as("row2");
cy.get("@row1").click();
cy.get("@row1").find("input.input-with-feedback.form-control").as("email_input1");
cy.get("@email_input1").type(website_input, { waitForAnimations: false });
cy.get("@table").find("button.grid-add-row").click();
cy.get("@table").find('[data-idx="2"]').as("row2");
cy.get("@row2").click();
cy.get("@row2").find("input.input-with-feedback.form-control").as("email_input2");
cy.get("@email_input2").type(valid_email, { waitForAnimations: false });

View file

@ -49,6 +49,24 @@ context("Utils", () => {
seconds: 0,
});
});
run_util("seconds_to_duration", 60 * 60, { hide_seconds: 1 }).then((duration) => {
expect(duration).to.deep.equal({
days: 0,
hours: 1,
minutes: 0,
seconds: 0,
});
});
run_util("seconds_to_duration", 15 * 60, { hide_seconds: 1 }).then((duration) => {
expect(duration).to.deep.equal({
days: 0,
hours: 0,
minutes: 15,
seconds: 0,
});
});
});
it("should parse days, hours, minutes and seconds", () => {

View file

@ -924,7 +924,7 @@ def get_installed_apps(*, _ensure_on_bench: bool = False) -> list[str]:
def get_doc_hooks():
"""Return hooked methods for given doc. Expand the dict tuple if required."""
if not hasattr(local, "doc_events_hooks"):
if not getattr(local, "doc_events_hooks", None):
hooks = get_hooks("doc_events", {})
out = {}
for key, value in hooks.items():

View file

@ -1,5 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
from contextlib import suppress
from enum import Enum
from werkzeug.exceptions import NotFound
@ -7,8 +8,8 @@ from werkzeug.routing import Map, Submount
from werkzeug.wrappers import Request, Response
import frappe
import frappe.client
from frappe import _
from frappe.pulse.app_heartbeat_event import capture_app_heartbeat
from frappe.utils.response import build_response
@ -63,7 +64,12 @@ def handle(request: Request):
if data is not None:
frappe.response["data"] = data
return build_response("json")
data = build_response("json")
with suppress(Exception):
capture_app_heartbeat(arguments)
return data
# Merge all API version routing rules

View file

@ -321,7 +321,10 @@ def set_authenticate_headers(response: Response):
def make_form_dict(request: Request):
request_data = request.get_data(as_text=True)
if request_data and request.is_json:
args = orjson.loads(request_data)
try:
args = orjson.loads(request_data)
except orjson.JSONDecodeError:
frappe.throw(_("Invalid request body"), frappe.DataError)
else:
args = {}
args.update(request.args or {})

View file

@ -66,6 +66,9 @@ class HTTPRequest:
elif frappe.get_request_header("REMOTE_ADDR"):
frappe.local.request_ip = frappe.get_request_header("REMOTE_ADDR")
elif frappe.request and getattr(frappe.request, "remote_addr", None):
frappe.local.request_ip = frappe.request.remote_addr
else:
frappe.local.request_ip = "127.0.0.1"
@ -666,7 +669,7 @@ def validate_oauth(authorization_header):
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(
get_url_delimiter()
)
valid, oauthlib_request = get_oauth_server().verify_request(
valid, _oauthlib_request = get_oauth_server().verify_request(
uri, http_method, body, headers, required_scopes
)
if valid:

View file

@ -67,7 +67,7 @@
"label": "Assignment Rules"
},
{
"description": "Simple Python Expression, Example: status == 'Open' and type == 'Bug'",
"description": "Simple Python Expression, Example: <code class=\"language-python\">status == 'Open' and issue_type == 'Bug'</code>",
"fieldname": "assign_condition",
"fieldtype": "Code",
"in_list_view": 1,
@ -80,7 +80,7 @@
"fieldtype": "Column Break"
},
{
"description": "Simple Python Expression, Example: Status in (\"Closed\", \"Cancelled\")",
"description": "Simple Python Expression, Example: <code class=\"language-python\">status in (\"Closed\", \"Cancelled\")</code>",
"fieldname": "unassign_condition",
"fieldtype": "Code",
"label": "Unassign Condition",
@ -119,7 +119,7 @@
"fieldtype": "Section Break"
},
{
"description": "Simple Python Expression, Example: Status in (\"Invalid\")",
"description": "Simple Python Expression, Example: <code class=\"language-python\">status == \"Invalid\"</code>",
"fieldname": "close_condition",
"fieldtype": "Code",
"label": "Close Condition",
@ -152,9 +152,10 @@
"mandatory_depends_on": "eval: doc.rule == 'Based on Field'"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-23 16:01:27.590910",
"modified": "2025-08-25 17:09:11.644603",
"modified_by": "Administrator",
"module": "Automation",
"name": "Assignment Rule",
@ -174,8 +175,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -147,17 +147,26 @@ def _clear_doctype_cache_from_redis(doctype: str | None = None):
clear_single(doctype)
# clear all parent doctypes
for dt in frappe.get_all(
"DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=doctype)
):
clear_single(dt.parent)
# clear all parent doctypes
if not frappe.flags.in_install:
try:
for dt in frappe.get_all(
"Custom Field", "dt", dict(fieldtype=["in", frappe.model.table_fields], options=doctype)
"DocField",
"parent",
dict(fieldtype=["in", frappe.model.table_fields], options=doctype),
ignore_ddl=True,
):
clear_single(dt.dt)
clear_single(dt.parent)
# clear all parent doctypes
if not frappe.flags.in_install:
for dt in frappe.get_all(
"Custom Field",
"dt",
dict(fieldtype=["in", frappe.model.table_fields], options=doctype),
ignore_ddl=True,
):
clear_single(dt.dt)
except frappe.DoesNotExistError:
pass # core doctypes getting migrated.
# clear all notifications
delete_notification_count_for(doctype)

View file

@ -437,6 +437,14 @@ def validate_link(doctype: str, docname: str, fields=None):
if not values.name:
return values
if not frappe.has_permission(doctype, "read", doc=values.name):
frappe.throw(
_("You do not have permission to access {0} {1}").format(
frappe.bold(doctype), frappe.bold(docname)
),
frappe.PermissionError,
)
if not fields:
frappe.local.response_headers.set("Cache-Control", "private,max-age=1800,stale-while-revalidate=7200")
return values

View file

@ -61,7 +61,7 @@ def create_rq_users(set_admin_password=False, use_rq_auth=False):
)
click.secho(f"`export {env_key}={user_credentials['default'][1]}`")
click.secho(
"NOTE: Please save the admin password as you " "can not access redis server without the password",
"NOTE: Please save the admin password as you can not access redis server without the password",
fg="yellow",
)

View file

@ -336,7 +336,7 @@ def restore_backup(
# Check if the backup is of an older version of frappe and the user hasn't specified force
if is_downgrade(sql_file_path, verbose=True) and not force:
warn_message = (
"This is not recommended and may lead to unexpected behaviour. " "Do you want to continue anyway?"
"This is not recommended and may lead to unexpected behaviour. Do you want to continue anyway?"
)
click.confirm(warn_message, abort=True)

View file

@ -297,7 +297,7 @@ class TestCommands(BaseTestCommands):
self.execute("bench --site {test_site} backup --exclude 'ToDo'", site_data)
site_data.update({"kw": "\"{'partial':True}\""})
self.execute(
"bench --site {test_site} execute" " frappe.utils.backups.fetch_latest_backups --kwargs {kw}",
"bench --site {test_site} execute frappe.utils.backups.fetch_latest_backups --kwargs {kw}",
site_data,
)
site_data.update({"database": json.loads(self.stdout)["database"]})

View file

@ -435,8 +435,7 @@ def import_doc(context: CliCtxObj, path, force=False):
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
required=True,
help=(
"Path to import file (.csv, .xlsx)."
"Consider that relative paths will resolve from 'sites' directory"
"Path to import file (.csv, .xlsx). Consider that relative paths will resolve from 'sites' directory"
),
)
@click.option("--doctype", type=str, required=True)

View file

@ -57,9 +57,8 @@ class Address(Document):
self.flags.linked = False
def autoname(self):
if not self.address_title:
if self.links:
self.address_title = self.links[0].link_name
if not self.address_title and self.links:
self.address_title = self.links[0].link_title or self.links[0].link_name
if self.address_title:
self.name = cstr(self.address_title).strip() + "-" + cstr(_(self.address_type)).strip()

View file

@ -25,7 +25,7 @@ class TestAuditTrail(IntegrationTestCase):
re_amended_doc = amend_document(amended_doc, changed_fields, {}, 1)
comparator = create_comparator_doc("Test Custom Doctype for Doc Comparator", re_amended_doc.name)
documents, results = comparator.compare_document()
_documents, results = comparator.compare_document()
test_field_values = results["changed"]["Field"]
self.check_expected_values(test_field_values, ["first value", "second value", "third value"])
@ -41,7 +41,7 @@ class TestAuditTrail(IntegrationTestCase):
amended_doc = amend_document(doc, {}, rows_updated, 1)
comparator = create_comparator_doc("Test Custom Doctype for Doc Comparator", amended_doc.name)
documents, results = comparator.compare_document()
_documents, results = comparator.compare_document()
results = frappe._dict(results)
self.check_rows_updated(results.row_changed)

View file

@ -565,11 +565,11 @@ def parse_email(email_strings):
for email in email_string.split(","):
local_part = email.split("@", 1)[0].strip('"')
user, detail = None, None
_user, detail = None, None
if "+" in local_part:
user, detail = local_part.split("+", 1)
_user, detail = local_part.split("+", 1)
elif "--" in local_part:
detail, user = local_part.rsplit("--", 1)
detail, _user = local_part.rsplit("--", 1)
if not detail:
continue

View file

@ -23,6 +23,7 @@
"submit",
"cancel",
"amend",
"mask",
"additional_permissions",
"report",
"export",
@ -153,6 +154,16 @@
"print_width": "32px",
"width": "32px"
},
{
"default": "0",
"fieldname": "mask",
"fieldtype": "Check",
"label": "Mask",
"oldfieldname": "mask",
"oldfieldtype": "Check",
"print_width": "32px",
"width": "32px"
},
{
"fieldname": "additional_permissions",
"fieldtype": "Section Break",
@ -214,11 +225,13 @@
"label": "Select"
}
],
"grid_page_length": 50,
"links": [],
"modified": "2024-03-23 16:02:14.726078",
"modified": "2025-05-22 16:59:35.484376",
"modified_by": "Administrator",
"module": "Core",
"name": "Custom DocPerm",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [
{
@ -235,8 +248,9 @@
}
],
"read_only": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"title_field": "parent"
}
}

View file

@ -21,6 +21,7 @@ class CustomDocPerm(Document):
email: DF.Check
export: DF.Check
if_owner: DF.Check
mask: DF.Check
parent: DF.Data | None
permlevel: DF.Int
print: DF.Check

View file

@ -11,8 +11,8 @@
"label",
"fieldtype",
"fieldname",
"precision",
"length",
"precision",
"non_negative",
"hide_days",
"hide_seconds",
@ -20,6 +20,7 @@
"is_virtual",
"search_index",
"not_nullable",
"mask",
"column_break_18",
"options",
"sort_options",
@ -135,7 +136,7 @@
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"description": "Set non-standard precision for a Float, Currency or Percent field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
@ -143,7 +144,7 @@
"print_hide": 1
},
{
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int', 'Float', 'Currency', 'Percent'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
@ -607,6 +608,13 @@
"fieldname": "sticky",
"fieldtype": "Check",
"label": "Sticky"
},
{
"default": "0",
"depends_on": "eval:[\"Select\", \"Read Only\", \"Phone\", \"Percent\", \"Password\", \"Link\", \"Int\", \"Float\", \"Dynamic Link\", \"Duration\", \"Datetime\", \"Currency\", \"Data\", \"Date\"].includes(doc.fieldtype)",
"fieldname": "mask",
"fieldtype": "Check",
"label": "Mask"
}
],
"grid_page_length": 50,
@ -614,7 +622,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-08-26 22:08:20.940308",
"modified": "2025-09-17 13:20:57.852396",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -90,6 +90,7 @@ class DocField(Document):
link_filters: DF.JSON | None
make_attachment_public: DF.Check
mandatory_depends_on: DF.Code | None
mask: DF.Check
max_height: DF.Data | None
no_copy: DF.Check
non_negative: DF.Check

View file

@ -22,6 +22,7 @@
"submit",
"cancel",
"amend",
"mask",
"additional_permissions",
"report",
"export",
@ -205,18 +206,27 @@
"fieldtype": "Check",
"in_list_view": 1,
"label": "Select"
},
{
"default": "0",
"fieldname": "mask",
"fieldtype": "Check",
"label": "Mask"
}
],
"grid_page_length": 50,
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-23 16:02:18.443496",
"modified": "2025-05-20 16:50:32.679113",
"modified_by": "Administrator",
"module": "Core",
"name": "DocPerm",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": []
}
}

View file

@ -20,6 +20,7 @@ class DocPerm(Document):
email: DF.Check
export: DF.Check
if_owner: DF.Check
mask: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View file

@ -702,7 +702,7 @@
"label": "Protect Attached Files"
},
{
"default": "0",
"default": "20",
"depends_on": "istable",
"fieldname": "rows_threshold_for_grid_search",
"fieldtype": "Int",
@ -792,7 +792,7 @@
"link_fieldname": "document_type"
}
],
"modified": "2025-07-19 12:23:16.296416",
"modified": "2025-09-23 06:48:13.555017",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -1667,6 +1667,18 @@ def validate_fields(meta: Meta):
if docfield.options and (int(docfield.options) > 10 or int(docfield.options) < 3):
frappe.throw(_("Options for Rating field can range from 3 to 10"))
def check_decimal_config(docfield):
if docfield.fieldtype not in ("Currency", "Float", "Percent"):
return
if docfield.length and docfield.precision:
if cint(docfield.precision) > cint(docfield.length):
frappe.throw(
_("Precision ({0}) for {1} cannot be greater than its length ({2}).").format(
docfield.precision, frappe.bold(docfield.label), docfield.length
)
)
fields = meta.get("fields")
fieldname_list = [d.fieldname for d in fields]
@ -1689,6 +1701,7 @@ def validate_fields(meta: Meta):
scrub_options_in_select(d)
validate_fetch_from(d)
validate_data_field_type(d)
check_decimal_config(d)
if not frappe.flags.in_migrate or in_ci:
check_unique_fieldname(meta.get("name"), d.fieldname)

View file

@ -24,6 +24,7 @@ frappe.listview_settings["DocType"] = {
fieldtype: "Data",
reqd: 1,
default: doctype_name,
length: 61,
},
{ fieldtype: "Column Break" },
{

View file

@ -827,6 +827,45 @@ class TestDocType(IntegrationTestCase):
self.assertEqual(get_format(compressed_dt), "COMPRESSED")
self.assertEqual(get_format(dynamic_dt), "DYNAMIC")
def test_decimal_field_configuration(self):
doctype = new_doctype(
"Test Decimal Config",
fields=[
{
"fieldname": "decimal_field",
"fieldtype": "Currency",
"length": 30,
"precision": 3,
}
],
).insert(ignore_if_duplicate=True)
decimal_field_type = frappe.db.get_column_type(doctype.name, "decimal_field")
self.assertIn("(30,3)", decimal_field_type.lower())
def test_decimal_field_precision_exceeds_length(self):
doctype = new_doctype(
"Test Decimal Config 2",
fields=[
{
"fieldname": "decimal_field",
"fieldtype": "Currency",
"length": 10,
"precision": 11,
}
],
)
self.assertRaises(frappe.ValidationError, doctype.insert)
def test_delete_doc_clears_cache(self):
dt = new_doctype(
fields=[{"fieldname": "test_fdname", "fieldtype": "Data", "label": "Test Field"}],
).insert()
frappe.get_meta(dt.name)
frappe.delete_doc("DocType", dt.name, force=1, delete_permanently=False)
frappe.db.commit()
with self.assertRaises(frappe.DoesNotExistError):
frappe.get_meta(dt.name)
def new_doctype(
name: str | None = None,

View file

@ -25,6 +25,7 @@ from frappe.utils import (
get_url,
)
from frappe.utils.file_manager import is_safe_path
from frappe.utils.html_utils import escape_html
from frappe.utils.image import optimize_image, strip_exif_data
from frappe.utils.pdf import pdf_contains_js
@ -37,7 +38,9 @@ from .exceptions import (
from .utils import *
exclude_from_linked_with = True
ImageFile.LOAD_TRUNCATED_IMAGES = True
ImageFile.LOAD_TRUNCATED_IMAGES = True # nosemgrep
URL_PREFIXES = ("http://", "https://", "/api/method/")
FILE_ENCODING_OPTIONS = ("utf-8-sig", "utf-8", "windows-1250", "windows-1252")
@ -139,7 +142,6 @@ class File(Document):
self.validate_file_url()
self.validate_file_on_disk()
self.file_size = frappe.form_dict.file_size or self.file_size
self.check_content()
def validate_attachment_references(self):
if not self.attached_to_doctype:
@ -390,8 +392,8 @@ class File(Document):
)
def check_content(self):
if self.file_type == "PDF" and self._content and not pdf_contains_js(self._content):
frappe.throw(_("PDF cannot be uploaded, It contains unsafe content"))
if self.file_type == "PDF" and self._content and pdf_contains_js(self._content):
frappe.throw(_("This PDF cannot be uploaded as it contains unsafe content."))
def validate_duplicate_entry(self):
if not self.flags.ignore_duplicate_entry_error and not self.is_folder:
@ -783,7 +785,7 @@ class File(Document):
def create_attachment_record(self):
icon = ' <i class="fa fa-lock text-warning"></i>' if self.is_private else ""
file_url = quote(frappe.safe_encode(self.file_url), safe="/:") if self.file_url else self.file_name
file_name = self.file_name or self.file_url
file_name = escape_html(self.file_name or self.file_url)
self.add_comment_in_reference_doc(
"Attachment",

View file

@ -0,0 +1,7 @@
frappe.listview_settings["File"] = {
formatters: {
file_name: function (value) {
return frappe.utils.escape_html(value || "");
},
},
};

View file

@ -164,6 +164,18 @@ class SubmissionQueue(Document):
def queue_submission(doc: Document, action: str, alert: bool = True):
if existing_queue := frappe.db.get_value(
"Submission Queue", {"ref_doctype": doc.doctype, "ref_docname": doc.name, "status": "Queued"}
):
frappe.msgprint(
_(
"This document has already been queued for submission. You can track the progress over {0}."
).format(f"<a href='/app/submission-queue/{existing_queue}'><b>here</b></a>"),
indicator="orange",
alert=True,
)
return
queue = frappe.new_doc("Submission Queue")
queue.ref_doctype = doc.doctype
queue.ref_docname = doc.name

View file

@ -30,6 +30,7 @@
"apply_strict_user_permissions",
"column_break_21",
"allow_older_web_view_links",
"show_external_link_warning",
"security_tab",
"security",
"session_expiry",
@ -744,12 +745,19 @@
"fieldtype": "Int",
"label": "Max signups allowed per hour",
"non_negative": 1
},
{
"default": "Never",
"fieldname": "show_external_link_warning",
"fieldtype": "Select",
"label": "Show External Link Warning",
"options": "Never\nAsk\nAlways"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2025-09-03 10:52:38.096662",
"modified": "2025-09-24 16:04:02.016562",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -97,6 +97,7 @@ class SystemSettings(Document):
session_expiry: DF.Data | None
setup_complete: DF.Check
show_absolute_datetime_in_timeline: DF.Check
show_external_link_warning: DF.Literal["Never", "Ask", "Always"]
store_attached_pdf_document: DF.Check
strip_exif_metadata_from_uploaded_images: DF.Check
time_format: DF.Literal["HH:mm:ss", "HH:mm"]

View file

@ -37,18 +37,6 @@ frappe.ui.form.on("User", {
}
},
role_profiles: function (frm) {
if (frm.doc.role_profiles && frm.doc.role_profiles.length) {
frm.roles_editor.disable = 1;
frm.call("populate_role_profile_roles").then(() => {
frm.roles_editor.show();
});
} else {
frm.roles_editor.disable = 0;
frm.roles_editor.show();
}
},
module_profile: function (frm) {
if (frm.doc.module_profile) {
frappe.call({
@ -431,6 +419,25 @@ frappe.ui.form.on("User Email", {
},
});
frappe.ui.form.on("User Role Profile", {
role_profiles_add: function (frm) {
if (frm.doc.role_profiles.length > 0) {
frm.roles_editor.disable = 1;
frm.call("populate_role_profile_roles").then(() => {
frm.roles_editor.show();
});
$(".deselect-all, .select-all").prop("disabled", true);
}
},
role_profiles_remove: function (frm) {
if (frm.doc.role_profiles.length == 0) {
frm.roles_editor.disable = 0;
frm.roles_editor.show();
$(".deselect-all, .select-all").prop("disabled", false);
}
},
});
function has_access_to_edit_user() {
return has_common(frappe.user_roles, get_roles_for_editing_user());
}

View file

@ -280,7 +280,7 @@ frappe.PermissionEngine = class PermissionEngine {
add_check(cell, d, fieldname, label, description = "") {
if (!label) label = toTitle(fieldname.replace(/_/g, " "));
if (d.permlevel > 0 && ["read", "write"].indexOf(fieldname) == -1) {
if (d.permlevel > 0 && ["read", "write", "mask"].indexOf(fieldname) == -1) {
return;
}
@ -331,6 +331,7 @@ frappe.PermissionEngine = class PermissionEngine {
"import",
"export",
"share",
"mask",
];
}

View file

@ -85,6 +85,7 @@
"fieldtype": "Data",
"in_filter": 1,
"label": "Label",
"length": 255,
"no_copy": 1,
"oldfieldname": "label",
"oldfieldtype": "Data"
@ -468,11 +469,12 @@
"label": "Placeholder"
}
],
"grid_page_length": 50,
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-09-28 20:19:35.935720",
"modified": "2025-10-10 11:10:23.862393",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
@ -501,6 +503,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "dt,label,fieldtype,options",
"sort_field": "creation",
"sort_order": "DESC",

View file

@ -13,6 +13,7 @@
"label",
"search_fields",
"grid_page_length",
"rows_threshold_for_grid_search",
"link_filters",
"column_break_5",
"istable",
@ -43,6 +44,7 @@
"force_re_route_to_default_view",
"column_break_29",
"show_preview_popup",
"show_name_in_global_search",
"email_settings_section",
"default_email_template",
"column_break_26",
@ -422,6 +424,19 @@
"fieldname": "recipient_account_field",
"fieldtype": "Data",
"label": "Recipient Account Field"
},
{
"depends_on": "istable",
"fieldname": "rows_threshold_for_grid_search",
"fieldtype": "Int",
"label": "Rows Threshold for Grid Search",
"non_negative": 1
},
{
"default": "0",
"fieldname": "show_name_in_global_search",
"fieldtype": "Check",
"label": "Make \"name\" searchable in Global Search"
}
],
"hide_toolbar": 1,
@ -430,7 +445,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-07-19 12:23:41.564203",
"modified": "2025-09-23 07:13:52.631903",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",

View file

@ -75,9 +75,11 @@ class CustomizeForm(Document):
queue_in_background: DF.Check
quick_entry: DF.Check
recipient_account_field: DF.Data | None
rows_threshold_for_grid_search: DF.Int
search_fields: DF.Data | None
sender_field: DF.Data | None
sender_name_field: DF.Data | None
show_name_in_global_search: DF.Check
show_preview_popup: DF.Check
show_title_field_in_link: DF.Check
sort_field: DF.Literal[None]
@ -306,6 +308,8 @@ class CustomizeForm(Document):
)
def set_property_setters_for_doctype(self, meta):
if self.get("show_name_in_global_search") != meta.get("show_name_in_global_search"):
self.flags.rebuild_doctype_for_global_search = True
for prop, prop_type in doctype_properties.items():
if self.get(prop) != meta.get(prop):
self.make_property_setter(prop, self.get(prop), prop_type)
@ -735,6 +739,7 @@ doctype_properties = {
"track_views": "Check",
"allow_auto_repeat": "Check",
"allow_import": "Check",
"show_name_in_global_search": "Check",
"show_preview_popup": "Check",
"default_email_template": "Data",
"email_append_to": "Check",
@ -748,6 +753,7 @@ doctype_properties = {
"force_re_route_to_default_view": "Check",
"translated_doctype": "Check",
"grid_page_length": "Int",
"rows_threshold_for_grid_search": "Int",
}
docfield_properties = {

View file

@ -1192,12 +1192,14 @@ class Database:
self.sql("commit")
self.begin()
self.value_cache.clear()
self.after_commit.run()
def rollback(self, *, save_point=None, chain=False):
"""`ROLLBACK` current transaction. Optionally rollback to a known save_point."""
if save_point:
self.sql(f"rollback to savepoint {save_point}")
self.value_cache.clear()
elif not self._disable_transaction_control:
self.before_commit.reset()
self.after_commit.reset()
@ -1206,10 +1208,12 @@ class Database:
if chain:
self.sql("rollback and chain")
self.value_cache.clear()
else:
self.sql("rollback")
self.begin()
self.value_cache.clear()
self.after_rollback.run()
else:
warnings.warn(message=TRANSACTION_DISABLED_MSG, stacklevel=2)

View file

@ -147,7 +147,7 @@ class PostgresTable(DBTable):
if isinstance(default, str):
default = frappe.db.escape(default)
change_nullability.append(
f"ALTER COLUMN \"{col.fieldname}\" {'SET' if col.not_nullable else 'DROP'} NOT NULL"
f'ALTER COLUMN "{col.fieldname}" {"SET" if col.not_nullable else "DROP"} NOT NULL'
)
change_nullability.append(f'ALTER COLUMN "{col.fieldname}" SET DEFAULT {default}')

View file

@ -288,6 +288,8 @@ class Engine:
doctype: str | None = None,
) -> "Criterion | None":
"""Builds a pypika Criterion object for a simple filter condition."""
import operator as builtin_operator
_field = self._validate_and_prepare_filter_field(field, doctype)
_value = convert_to_value(value)
_operator = operator
@ -323,7 +325,7 @@ class Engine:
operator_fn = OPERATOR_MAP[_operator.casefold()]
if _value is None and isinstance(_field, Field):
return _field.isnull()
return _field.isnotnull() if operator_fn == builtin_operator.ne else _field.isnull()
else:
return operator_fn(_field, _value)

View file

@ -10,6 +10,10 @@ SPECIAL_CHAR_PATTERN = re.compile(r"[\W]", flags=re.UNICODE)
VARCHAR_CAST_PATTERN = re.compile(r"varchar\(([\d]+)\)")
CONFIGURABLE_DECIMAL_TYPES = ("Currency", "Float", "Percent")
DEFAULT_DECIMAL_LENGTH = 21
DEFAULT_DECIMAL_PRECISION = 9
class InvalidColumnName(frappe.ValidationError):
pass
@ -429,13 +433,19 @@ def get_definition(fieldtype, precision=None, length=None, *, options=None):
size = d[1] if d[1] else None
if size:
# This check needs to exist for backward compatibility.
# Till V13, default size used for float, currency and percent are (18, 6).
if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6:
size = "21,9"
if fieldtype in CONFIGURABLE_DECIMAL_TYPES:
width = length if length else DEFAULT_DECIMAL_LENGTH
precision_is_set = precision not in (None, "")
precision = precision if precision_is_set else DEFAULT_DECIMAL_PRECISION
if cint(precision) > cint(width):
precision = width
size = f"{cint(width)},{cint(precision)}"
if length:
if coltype == "varchar":
# Reference: https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-row-formats/troubleshooting-row-size-too-large-errors-with-innodb
if cint(length) < 64:
length = 64
size = length
elif coltype == "int" and length < 11:
# allow setting custom length for int if length provided is less than 11

View file

@ -107,6 +107,7 @@ class SQLiteDatabase(SQLiteExceptionUtil, Database):
conn = self.create_connection(read_only)
conn.isolation_level = None
conn.create_function("regexp", 2, regexp)
conn.create_function("regexp_replace", 3, regexp_replace)
pragmas = {
"journal_mode": "WAL",
"synchronous": "NORMAL",
@ -583,3 +584,10 @@ def regexp(expr: str, item: str) -> bool:
Although it works in the CLI - doesn't work through python
"""
return re.search(expr, item) is not None
def regexp_replace(item: str, pattern: str, repl: str) -> str:
"""
Define regexp_replace implementation for SQLite
"""
return re.sub(pattern, repl, item)

View file

@ -125,7 +125,7 @@ class SQLiteTable(DBTable):
if self.meta.sort_field == "modified" and not frappe.db.get_column_index(
self.table_name, "modified", unique=False
):
index_queries.append(f"CREATE INDEX `modified` ON `{self.table_name}` (`modified`)")
index_queries.append(f"CREATE INDEX IF NOT EXISTS `modified` ON `{self.table_name}` (`modified`)")
for query in index_queries:
frappe.db.sql_ddl(query)

View file

@ -899,7 +899,7 @@ def tests_utils_get_dependencies(doctype):
import frappe
from frappe.tests.utils.generators import get_modules
module, test_module = get_modules(doctype)
_module, test_module = get_modules(doctype)
meta = frappe.get_meta(doctype)
link_fields = meta.get_link_fields()

View file

@ -11,7 +11,7 @@ frappe.ui.form.on("Bulk Update", {
],
};
});
frm.trigger("set_field_options");
frm.page.set_primary_action(__("Update"), function () {
if (!frm.doc.update_value) {
frappe.throw(__('Field "value" is mandatory. Please specify value to be updated'));
@ -42,6 +42,9 @@ frappe.ui.form.on("Bulk Update", {
},
document_type: function (frm) {
frm.trigger("set_field_options");
},
set_field_options(frm) {
// set field options
if (!frm.doc.document_type) return;

View file

@ -26,6 +26,7 @@ frappe.ui.form.on("Number Card", {
frm.trigger("render_filters_table");
}
frm.trigger("set_parent_document_type");
frm.trigger("set_document_type_description");
if (!frm.is_new()) {
frm.trigger("create_add_to_dashboard_button");
@ -67,6 +68,8 @@ frappe.ui.form.on("Number Card", {
},
type: function (frm) {
frm.trigger("set_document_type_description");
if (frm.doc.type == "Report") {
frm.set_query("report_name", () => {
return {
@ -202,7 +205,9 @@ frappe.ui.form.on("Number Card", {
let is_dynamic_filter = (f) => ["Date", "DateRange"].includes(f.fieldtype) && f.default;
let wrapper = $(frm.get_field("filters_json").wrapper).empty();
let table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
let table = $(`<table class="table table-bordered" style="cursor:${
frm.has_perm("write") ? "pointer" : "default"
}; margin:0px;">
<thead>
<tr>
<th style="width: 20%">${__("Filter")}</th>
@ -212,7 +217,10 @@ frappe.ui.form.on("Number Card", {
</thead>
<tbody></tbody>
</table>`).appendTo(wrapper);
$(`<p class="text-muted small">${__("Click table to edit")}</p>`).appendTo(wrapper);
if (frm.has_perm("write")) {
$(`<p class="text-muted small">${__("Click table to edit")}</p>`).appendTo(wrapper);
}
let filters = JSON.parse(frm.doc.filters_json || "[]");
let filters_set = false;
@ -273,6 +281,10 @@ frappe.ui.form.on("Number Card", {
}
table.on("click", () => {
if (!frm.has_perm("write")) {
return;
}
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
frappe.throw(__("Cannot edit filters for standard number cards"));
}
@ -332,8 +344,9 @@ frappe.ui.form.on("Number Card", {
let wrapper = $(frm.get_field("dynamic_filters_json").wrapper).empty();
frm.dynamic_filter_table =
$(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
frm.dynamic_filter_table = $(`<table class="table table-bordered" style="cursor:${
frm.has_perm("write") ? "pointer" : "default"
}; margin:0px;">
<thead>
<tr>
<th style="width: 20%">${__("Filter")}</th>
@ -360,6 +373,10 @@ frappe.ui.form.on("Number Card", {
);
frm.dynamic_filter_table.on("click", () => {
if (!frm.has_perm("write")) {
return;
}
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
frappe.throw(__("Cannot edit filters for standard number cards"));
}
@ -431,7 +448,11 @@ frappe.ui.form.on("Number Card", {
let document_type = frm.doc.document_type;
let doc_is_table =
document_type &&
(await frappe.db.get_value("DocType", document_type, "istable")).message.istable;
(await new Promise((resolve) => {
frappe.model.with_doctype(document_type, () => {
resolve(frappe.get_meta(document_type).istable);
});
}));
frm.set_df_property("parent_document_type", "hidden", !doc_is_table);
@ -454,4 +475,20 @@ frappe.ui.form.on("Number Card", {
}
}
},
set_document_type_description: function (frm) {
if (frm.doc.type == "Custom") {
frm.set_df_property(
"document_type",
"description",
__(
"This card is visible only to Administrator and System Managers by default. Set a DocType to share with users who have read access.",
null,
"Number Card"
)
);
} else {
frm.set_df_property("document_type", "description", "");
}
},
});

View file

@ -38,7 +38,7 @@
],
"fields": [
{
"depends_on": "eval: doc.type == 'Document Type'",
"depends_on": "eval: ['Document Type', 'Custom'].includes(doc.type)",
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
@ -229,7 +229,7 @@
}
],
"links": [],
"modified": "2025-05-21 17:33:04.908518",
"modified": "2025-09-17 21:00:11.351605",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",

View file

@ -7,9 +7,10 @@ from frappe.boot import get_allowed_report_names
from frappe.model.document import Document
from frappe.model.naming import append_number_if_name_exists
from frappe.modules.export_file import export_to_files
from frappe.permissions import get_doctypes_with_read
from frappe.query_builder import Criterion
from frappe.query_builder.utils import DocType
from frappe.utils import cint, flt
from frappe.utils import flt
from frappe.utils.modules import get_modules_from_all_apps_for_user
@ -78,51 +79,43 @@ class NumberCard(Document):
def get_permission_query_conditions(user=None):
if not user:
user = frappe.session.user
if user == "Administrator":
# The user param is ignored because `get_allowed_report_names` and `get_doctypes_with_read` don't support it.
if frappe.session.user == "Administrator":
return
roles = frappe.get_roles(user)
if "System Manager" in roles:
return None
if "System Manager" in frappe.get_roles():
return
doctype_condition = False
module_condition = False
allowed_reports = get_allowed_report_names()
allowed_doctypes = get_doctypes_with_read()
allowed_modules = [module.get("module_name") for module in get_modules_from_all_apps_for_user()]
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
allowed_modules = [
frappe.db.escape(module.get("module_name")) for module in get_modules_from_all_apps_for_user()
]
nc = frappe.qb.DocType("Number Card")
conditions = (
((nc.type == "Report") & nc.report_name.isin(allowed_reports))
| ((nc.type == "Custom") & nc.document_type.isin(allowed_doctypes))
| ((nc.type == "Document Type") & nc.document_type.isin(allowed_doctypes))
) & (nc.module.isin(allowed_modules) | nc.module.isnull() | nc.module == "")
if allowed_doctypes:
doctype_condition = "`tabNumber Card`.`document_type` in ({allowed_doctypes})".format(
allowed_doctypes=",".join(allowed_doctypes)
)
if allowed_modules:
module_condition = """`tabNumber Card`.`module` in ({allowed_modules})
or `tabNumber Card`.`module` is NULL""".format(allowed_modules=",".join(allowed_modules))
return f"""
{doctype_condition}
and
{module_condition}
"""
return conditions.get_sql(quote_char="`")
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)
if "System Manager" in roles:
# The user param is ignored because `get_allowed_report_names` and `get_doctypes_with_read` don't support it.
if frappe.session.user == "Administrator":
return True
if doc.type == "Report":
if doc.report_name in get_allowed_report_names():
return True
else:
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
if doc.document_type in allowed_doctypes:
return True
if "System Manager" in frappe.get_roles():
return True
if doc.type == "Report" and doc.report_name in get_allowed_report_names():
return True
if doc.type == "Custom" and doc.document_type in get_doctypes_with_read():
return True
if doc.type == "Document Type" and doc.document_type in get_doctypes_with_read():
return True
return False

View file

@ -622,8 +622,7 @@ def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False):
"DocField", fields=["parent", "options"], filters=child_filters, as_list=1
):
ret[parent] = {"child_doctype": options, "fieldname": links_dict[options]}
if options in ret:
del ret[options]
ret.pop(options, None)
virtual_doctypes = frappe.get_all("DocType", {"is_virtual": 1}, pluck="name")
for dt in virtual_doctypes:

View file

@ -76,6 +76,9 @@ class FormMeta(Meta):
for k in ASSET_KEYS:
d[k] = __dict.get(k)
# add masked fields (per-user, per-meta)
d["masked_fields"] = [df.fieldname for df in self.get_masked_fields()]
return d
def add_code(self):

View file

@ -83,7 +83,7 @@ def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[d
break
if owner_idx:
data = [data.pop(owner_idx)] + data[0:49]
data = [data.pop(owner_idx), *data[0:49]]
else:
data = data[0:50]
else:

View file

@ -1,7 +1,7 @@
<!-- jinja -->
<div class="row download-backups">
<div class="row download-backups py-4 px-4 m-0">
{% for f in files %}
<div class="col-lg-3 col-md-4 col-12">
<div class="col-lg-3 col-md-4 col-12 pr-4 pl-0">
<a href="{{ f[0] }}" target="_blank" rel="noopener noreferrer" class="frappe-card download-backup-card">
<div>
{{ f[1] }}

View file

@ -10,7 +10,7 @@ from frappe.utils.data import convert_utc_to_system_timezone
def get_time(path: Path):
return convert_utc_to_system_timezone(
datetime.datetime.fromtimestamp(path.stat().st_mtime, tz=datetime.UTC)
datetime.datetime.fromtimestamp(path.stat().st_mtime, tz=datetime.timezone.utc)
).strftime("%a %b %d %H:%M %Y")

View file

@ -199,10 +199,11 @@ def run(
is_tree=False,
parent_field=None,
are_default_filters=True,
js_filters=None,
):
if not user:
user = frappe.session.user
validate_filters_permissions(report_name, filters, user)
validate_filters_permissions(report_name, filters, user, js_filters)
report = get_report_doc(report_name)
if not frappe.has_permission(report.ref_doctype, "report"):
frappe.msgprint(
@ -339,9 +340,8 @@ def export_query():
)
frappe.msgprint(
_(
"Your report is being generated in the background. "
f"You will receive an email on {user_email} with a download link once it is ready."
)
"Your report is being generated in the background. You will receive an email on {0} with a download link once it is ready."
).format(user_email)
)
return
@ -416,7 +416,7 @@ def _export_query(form_params, csv_params, populate_response=True):
if not populate_response:
return report_name, file_extension, content
provide_binary_file(report_name, file_extension, content)
provide_binary_file(_(report_name), file_extension, content)
def valid_report_name(report_name, suffix):
@ -674,9 +674,20 @@ def get_filtered_data(ref_doctype, columns, data, user):
shared = frappe.share.get_shared(ref_doctype, user)
columns_dict = get_columns_dict(columns)
role_permissions = get_role_permissions(frappe.get_meta(ref_doctype), user)
ref_doctype_meta = frappe.get_meta(ref_doctype)
role_permissions = get_role_permissions(ref_doctype_meta, user)
if_owner = role_permissions.get("if_owner", {}).get("report")
if ref_doctype_meta.get_masked_fields():
from frappe.model.db_query import mask_field_value
# Apply masking to the fields
for field in ref_doctype_meta.get_masked_fields():
for row in data:
val = row.get(field.fieldname)
row[field.fieldname] = mask_field_value(field, val)
if match_filters_per_doctype:
for row in data:
# Why linked_doctypes.get(ref_doctype)? because if column is empty, linked_doctypes[ref_doctype] is removed
@ -894,25 +905,34 @@ def get_user_match_filters(doctypes, user):
return match_filters
def validate_filters_permissions(report_name, filters=None, user=None):
def validate_filters_permissions(report_name, filters=None, user=None, js_filters=None):
if not filters:
return
if js_filters is None:
js_filters = []
if isinstance(js_filters, str):
js_filters = json.loads(js_filters)
if isinstance(filters, str):
filters = json.loads(filters)
report = frappe.get_doc("Report", report_name)
for field in report.filters:
if field.fieldname in filters and field.fieldtype == "Link":
linked_doctype = field.options
for field in report.filters + js_filters:
if hasattr(field, "as_dict"):
field = field.as_dict()
if field.get("fieldname") in filters and field.get("fieldtype") == "Link":
linked_doctype = field.get("options")
if not has_permission(
doctype=linked_doctype, ptype="read", doc=filters[field.fieldname], user=user
doctype=linked_doctype, ptype="read", doc=filters[field.get("fieldname")], user=user
) and not has_permission(
doctype=linked_doctype, ptype="select", doc=filters[field.fieldname], user=user
doctype=linked_doctype, ptype="select", doc=filters[field.get("fieldname")], user=user
):
frappe.throw(
_("You do not have permission to access {0}: {1}.").format(
linked_doctype, filters[field.fieldname]
linked_doctype, filters[field.get("fieldname")]
)
)

View file

@ -376,6 +376,7 @@ def export_query():
form_params = get_form_params()
form_params["limit_page_length"] = None
form_params["as_list"] = True
csv_params = pop_csv_params(form_params)
export_in_background = int(form_params.pop("export_in_background", 0))
@ -394,9 +395,8 @@ def export_query():
frappe.msgprint(
_(
"Your report is being generated in the background. "
f"You will receive an email on {user_email} with a download link once it is ready."
)
"Your report is being generated in the background. You will receive an email on {0} with a download link once it is ready."
).format(user_email)
)
return
@ -483,7 +483,7 @@ def _export_query(form_params, csv_params, populate_response=True):
if not populate_response:
return title, file_extension, content
provide_binary_file(title, file_extension, content)
provide_binary_file(_(title), file_extension, content)
def append_totals_row(data):
@ -548,7 +548,7 @@ def get_field_info(fields, doctype):
if parenttype != doctype:
# If the column is from a child table, append the child doctype.
# For example, "Item Code (Sales Invoice Item)".
label += f" ({ _(parenttype) })"
label += f" ({_(parenttype)})"
field_info.append(
{"name": name, "label": label, "fieldtype": fieldtype, "translatable": translatable}

View file

@ -117,6 +117,12 @@ def search_widget(
meta = frappe.get_meta(doctype)
include_disabled = False
if filters and "include_disabled" in filters:
if filters["include_disabled"] == 1:
include_disabled = True
filters.pop("include_disabled")
if isinstance(filters, dict):
filters = [make_filter_tuple(doctype, key, value) for key, value in filters.items()]
elif filters is None:
@ -147,10 +153,11 @@ def search_widget(
if not meta.translated_doctype and (f == "name" or (fmeta and fmeta.fieldtype in field_types)):
or_filters.append([doctype, f.strip(), "like", f"%{txt}%"])
if meta.get("fields", {"fieldname": "enabled", "fieldtype": "Check"}):
filters.append([doctype, "enabled", "=", 1])
if meta.get("fields", {"fieldname": "disabled", "fieldtype": "Check"}):
filters.append([doctype, "disabled", "!=", 1])
if not include_disabled:
if meta.get("fields", {"fieldname": "enabled", "fieldtype": "Check"}):
filters.append([doctype, "enabled", "=", 1])
if meta.get("fields", {"fieldname": "disabled", "fieldtype": "Check"}):
filters.append([doctype, "disabled", "!=", 1])
# format a list of fields combining search fields and filter fields
fields = get_std_fields_list(meta, searchfield or "name")

View file

@ -9,8 +9,7 @@ from frappe import _
def get_all_nodes(doctype, label, parent, tree_method, **filters):
"""Recursively gets all data from tree nodes"""
if "cmd" in filters:
del filters["cmd"]
filters.pop("cmd", None)
filters.pop("data", None)
tree_method = frappe.get_attr(tree_method)
@ -20,8 +19,7 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters):
data = tree_method(doctype, parent, **filters)
out = [dict(parent=label, data=data)]
if "is_root" in filters:
del filters["is_root"]
filters.pop("is_root", None)
to_check = [d.get("value") for d in data if d.get("expandable")]
while to_check:

View file

@ -106,9 +106,9 @@ def send_report_email(
message=frappe._(
"The report you requested has been generated.<br><br>"
"Click here to download:<br>"
f"<a href='{file_url}'>{file_url}</a><br><br>"
f"This link will expire in {file_retention_hours} hours."
),
"<a href='{0}'>{0}</a><br><br>"
"This link will expire in {1} hours."
).format(file_url, file_retention_hours),
now=True,
)

View file

@ -54,7 +54,7 @@ class TestEmailQueue(IntegrationTestCase):
Subject: {subject}
From: Test <test@example.com>
To: <!--recipient-->
Date: {frappe.utils.now_datetime().strftime('%a, %d %b %Y %H:%M:%S %z')}
Date: {frappe.utils.now_datetime().strftime("%a, %d %b %Y %H:%M:%S %z")}
Reply-To: test@example.com
X-Frappe-Site: {frappe.local.site}
"""

View file

@ -352,7 +352,9 @@ def get_context(context):
To queue a notification from a server script:
```python
notification = frappe.get_doc("Notification", "My Notification", ignore_permissions=True)
notification = frappe.get_doc(
"Notification", "My Notification", ignore_permissions=True
)
notification.queue_send(customer)
```

View file

@ -237,7 +237,7 @@ class EMail:
"""Append the message with MIME content to the root node (as attachment)"""
from email.mime.text import MIMEText
maintype, subtype = mime_type.split("/")
_maintype, subtype = mime_type.split("/")
part = MIMEText(message, _subtype=subtype, policy=policy.SMTP)
if as_attachment:
@ -445,7 +445,7 @@ def add_attachment(fname, fcontent, content_type=None, parent=None, content_id=N
from email.mime.text import MIMEText
if not content_type:
content_type, encoding = mimetypes.guess_type(fname)
content_type, _encoding = mimetypes.guess_type(fname)
if not parent:
return
@ -597,7 +597,7 @@ def get_header(header=None):
if not title:
title = frappe.get_hooks("app_title")[-1]
email_header, text = get_email_from_template(
email_header, _text = get_email_from_template(
"email_header", {"header_title": title, "indicator": indicator}
)

View file

@ -205,7 +205,7 @@ class EmailServer:
readonly = self.settings.email_sync_rule != "UNSEEN"
self.imap.select(folder, readonly=readonly)
response, message = self.imap.uid("search", None, self.settings.email_sync_rule)
_response, message = self.imap.uid("search", None, self.settings.email_sync_rule)
if message[0]:
email_list = message[0].split()
else:
@ -217,7 +217,7 @@ class EmailServer:
# compare the UIDVALIDITY of email account and imap server
uid_validity = self.settings.uid_validity
response, message = self.imap.status(folder, "(UIDVALIDITY UIDNEXT)")
_response, message = self.imap.status(folder, "(UIDVALIDITY UIDNEXT)")
current_uid_validity = self.parse_imap_response("UIDVALIDITY", message[0]) or 0
uidnext = int(self.parse_imap_response("UIDNEXT", message[0]) or "1")
@ -270,7 +270,7 @@ class EmailServer:
def retrieve_message(self, uid, msg_num, folder):
try:
if cint(self.settings.use_imap):
status, message = self.imap.uid("fetch", uid, "(BODY.PEEK[] BODY.PEEK[HEADER] FLAGS)")
_status, message = self.imap.uid("fetch", uid, "(BODY.PEEK[] BODY.PEEK[HEADER] FLAGS)")
raw = message[0]
self.get_email_seen_status(uid, raw[0])
@ -446,6 +446,15 @@ class Email:
_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"))
if not _from_email:
# happens in some cases when email server is misconfigured
# should not fail the entire syncing process
frappe.log_error(
f"Email missing `From` header. UID: {getattr(self, 'uid', 'unknown')}", str(self.mail)
)
self.from_email = None
return
if _reply_to and not frappe.db.get_value(
"Email Account", {"email_id": _reply_to, "enable_incoming": 1}, "email_id"
):
@ -453,9 +462,7 @@ class Email:
else:
self.from_email = extract_email_id(_from_email)
if self.from_email:
self.from_email = self.from_email.lower()
self.from_email = self.from_email.lower()
self.from_real_name = parse_addr(_from_email)[0] if "@" in _from_email else _from_email
@staticmethod

View file

@ -52,7 +52,8 @@ ml,മലയാളം,0
mn,Монгол,0
mr,मराठी,0
ms,Melayu,0
my,မြန်မာ,0
my,မြန်မာ1
nb,Norsk Bokmål,1
nl,Nederlands,0
no,Norsk,0
pl,Polski,0

Can't render this file because it has a wrong number of fields in line 55.

View file

@ -0,0 +1,73 @@
import json
def extract(fileobj, *args, **kwargs):
"""
Extract messages from Web Form JSON files. To be used to babel extractor
:param fileobj: the file-like object the messages should be extracted from
:rtype: `iterator`
"""
data = json.load(fileobj)
if isinstance(data, list):
return
if data.get("doctype") != "Web Form":
return
web_form_name = data.get("name")
# Extract main web form fields
if title := data.get("title"):
yield None, "_", title, [f"Title of the {web_form_name} Web Form"]
if introduction_text := data.get("introduction_text"):
yield None, "_", introduction_text, [f"Introduction text of the {web_form_name} Web Form"]
if success_message := data.get("success_message"):
yield None, "_", success_message, [f"Success message of the {web_form_name} Web Form"]
if success_title := data.get("success_title"):
yield None, "_", success_title, [f"Success title of the {web_form_name} Web Form"]
if list_title := data.get("list_title"):
yield None, "_", list_title, [f"List title of the {web_form_name} Web Form"]
if button_label := data.get("button_label"):
yield None, "_", button_label, [f"Button label of the {web_form_name} Web Form"]
if meta_title := data.get("meta_title"):
yield None, "_", meta_title, [f"Meta title of the {web_form_name} Web Form"]
if meta_description := data.get("meta_description"):
yield None, "_", meta_description, [f"Meta description of the {web_form_name} Web Form"]
# Extract web form fields
for field in data.get("web_form_fields", []):
if label := field.get("label"):
yield None, "_", label, [f"Label of a field in the {web_form_name} Web Form"]
if description := field.get("description"):
yield None, "_", description, [f"Description of a field in the {web_form_name} Web Form"]
# Extract options for Select fields
if field.get("fieldtype") == "Select" and (options := field.get("options")):
skip_options = (
web_form_name == "edit-profile" and field.get("fieldname") == "time_zone"
) # Dumb workaround for avoiding a flood of strings from this field
if isinstance(options, str) and not skip_options:
# Handle both single values and newline-separated values
option_list = options.split("\n") if "\n" in options else [options]
for option in option_list:
if option.strip():
yield (
None,
"_",
option.strip(),
[f"Option in a Select field in the {web_form_name} Web Form"],
)
# Extract list columns
for column in data.get("list_columns", []):
if isinstance(column, dict) and (label := column.get("label")):
yield None, "_", label, [f"Label of a list column in the {web_form_name} Web Form"]

View file

@ -209,6 +209,7 @@ scheduler_events = {
"frappe.automation.doctype.reminder.reminder.send_reminders",
"frappe.model.utils.link_count.update_link_count",
"frappe.search.sqlite_search.build_index_if_not_exists",
"frappe.pulse.client.send_queued_events",
],
# 10 minutes
"0/10 * * * *": [

View file

@ -359,6 +359,7 @@ def remove_from_installed_apps(app_name):
"DefaultValue", {"defkey": "installed_apps"}, "defvalue", json.dumps(installed_apps)
)
_clear_cache("__global")
frappe.local.doc_events_hooks = None
frappe.get_single("Installed Applications").update_versions()
frappe.db.commit()
if frappe.flags.in_install:

View file

@ -34,7 +34,7 @@ class Here:
"label": address["label"],
"value": json.dumps(
{
"address_line1": f'{address.get("street", "")} {address.get("houseNumber", "")}'.strip(),
"address_line1": f"{address.get('street', '')} {address.get('houseNumber', '')}".strip(),
"city": address.get("city", ""),
"state": address.get("state", ""),
"pincode": address.get("postalCode", ""),

View file

@ -37,7 +37,7 @@ class Nomatim:
"label": result["display_name"],
"value": json.dumps(
{
"address_line1": f'{address.get("road")} {address.get("house_number", "")}'.strip(),
"address_line1": f"{address.get('road')} {address.get('house_number', '')}".strip(),
"city": address.get("city") or address.get("town") or address.get("village"),
"state": address.get("state"),
"pincode": address.get("postcode"),

View file

@ -278,13 +278,14 @@ class LDAPSettings(Document):
elif self.ldap_directory_server.lower() == "openldap":
ldap_object_class = "posixgroup"
ldap_group_members_attribute = "memberuid"
user_search_str = getattr(user, self.ldap_username_field).value
user_search_str = escape_filter_chars(getattr(user, self.ldap_username_field).value)
elif self.ldap_directory_server.lower() == "custom":
ldap_object_class = self.ldap_group_objectclass
ldap_group_members_attribute = self.ldap_group_member_attribute
ldap_custom_group_search = self.ldap_custom_group_search or "{0}"
user_search_str = ldap_custom_group_search.format(getattr(user, self.ldap_username_field).value)
user_value = escape_filter_chars(getattr(user, self.ldap_username_field).value)
user_search_str = ldap_custom_group_search.format(user_value)
else:
# NOTE: depreciate this else path
@ -308,6 +309,7 @@ class LDAPSettings(Document):
if not self.enabled:
frappe.throw(_("LDAP is not enabled."))
username = escape_filter_chars(username)
user_filter = self.ldap_search_string.format(username)
ldap_attributes = self.get_ldap_attributes()
conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False))
@ -335,7 +337,8 @@ class LDAPSettings(Document):
except LDAPInvalidCredentialsResult:
frappe.throw(_("Invalid username or password"))
def reset_password(self, user, password, logout_sessions=False):
def reset_password(self, user: str, password: str, logout_sessions: int = 0):
user = escape_filter_chars(user)
search_filter = f"({self.ldap_email_field}={user})"
conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False), read_only=False)
@ -420,7 +423,7 @@ def login():
@frappe.whitelist()
def reset_password(user, password, logout):
def reset_password(user: str, password: str, logout: int):
ldap: LDAPSettings = frappe.get_doc("LDAP Settings")
if not ldap.enabled:
frappe.throw(_("LDAP is not enabled."))

View file

@ -240,7 +240,7 @@ class LDAP_TestCase:
function_return = self.test_class.connect_to_ldap(
base_dn=self.base_dn, password=self.base_password
)
args, kwargs = ldap3_connection_method.call_args
_args, kwargs = ldap3_connection_method.call_args
for connection_arg in kwargs:
if (
@ -305,7 +305,7 @@ class LDAP_TestCase:
base_dn=self.base_dn, password=self.base_password, read_only=False
)
args, kwargs = ldap3_connection_method.call_args
_args, kwargs = ldap3_connection_method.call_args
self.assertFalse(
kwargs["read_only"],

View file

@ -72,7 +72,7 @@ def approve(*args, **kwargs):
frappe.flags.oauth_credentials,
) = get_oauth_server().validate_authorization_request(r.url, r.method, r.get_data(), r.headers)
headers, body, status = get_oauth_server().create_authorization_response(
headers, _body, _status = get_oauth_server().create_authorization_response(
uri=frappe.flags.oauth_credentials["redirect_uri"],
body=r.get_data(),
headers=r.headers,
@ -144,7 +144,7 @@ def authorize(**kwargs):
def get_token(*args, **kwargs):
try:
r = frappe.request
headers, body, status = get_oauth_server().create_token_response(
_headers, body, _status = get_oauth_server().create_token_response(
r.url, r.method, r.form, r.headers, frappe.flags.oauth_credentials
)
body = frappe._dict(json.loads(body))
@ -165,7 +165,7 @@ def get_token(*args, **kwargs):
def revoke_token(*args, **kwargs):
try:
r = frappe.request
headers, body, status = get_oauth_server().create_revocation_response(
_headers, _body, status = get_oauth_server().create_revocation_response(
r.url,
headers=r.headers,
body=r.form,
@ -184,7 +184,7 @@ def revoke_token(*args, **kwargs):
def openid_profile(*args, **kwargs):
try:
r = frappe.request
headers, body, status = get_oauth_server().create_userinfo_response(
_headers, body, _status = get_oauth_server().create_userinfo_response(
r.url,
headers=r.headers,
body=r.form,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

31819
frappe/locale/my.po Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

31819
frappe/locale/ta.po Normal file

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more