Merge branch 'develop' into computed_child_table
This commit is contained in:
commit
a93530e221
210 changed files with 87632 additions and 18133 deletions
|
|
@ -61,3 +61,6 @@ e9bbe03354079cfcef65a77b0c33f57b047a7c93
|
|||
|
||||
# replace `frappe.flags.in_test` with `frappe.in_test`
|
||||
653c80b8483cc41aef25cd7d66b9b6bb188bf5f8
|
||||
|
||||
# another ruff update
|
||||
6ca4d4d167a1a009d99062747711de7a994aa633
|
||||
|
|
|
|||
2
.github/workflows/run-indinvidual-tests.yml
vendored
2
.github/workflows/run-indinvidual-tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/server-tests.yml
vendored
2
.github/workflows/server-tests.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {})
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ frappe.listview_settings["DocType"] = {
|
|||
fieldtype: "Data",
|
||||
reqd: 1,
|
||||
default: doctype_name,
|
||||
length: 61,
|
||||
},
|
||||
{ fieldtype: "Column Break" },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
7
frappe/core/doctype/file/file_list.js
Normal file
7
frappe/core/doctype/file/file_list.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
frappe.listview_settings["File"] = {
|
||||
formatters: {
|
||||
file_name: function (value) {
|
||||
return frappe.utils.escape_html(value || "");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}')
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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", "");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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] }}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
73
frappe/gettext/extractors/web_form.py
Normal file
73
frappe/gettext/extractors/web_form.py
Normal 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"]
|
||||
|
|
@ -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 * * * *": [
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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", ""),
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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."))
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
1268
frappe/locale/ar.po
1268
frappe/locale/ar.po
File diff suppressed because it is too large
Load diff
1272
frappe/locale/bs.po
1272
frappe/locale/bs.po
File diff suppressed because it is too large
Load diff
1260
frappe/locale/cs.po
1260
frappe/locale/cs.po
File diff suppressed because it is too large
Load diff
1260
frappe/locale/da.po
1260
frappe/locale/da.po
File diff suppressed because it is too large
Load diff
1272
frappe/locale/de.po
1272
frappe/locale/de.po
File diff suppressed because it is too large
Load diff
1272
frappe/locale/eo.po
1272
frappe/locale/eo.po
File diff suppressed because it is too large
Load diff
1274
frappe/locale/es.po
1274
frappe/locale/es.po
File diff suppressed because it is too large
Load diff
1374
frappe/locale/fa.po
1374
frappe/locale/fa.po
File diff suppressed because it is too large
Load diff
1280
frappe/locale/fr.po
1280
frappe/locale/fr.po
File diff suppressed because it is too large
Load diff
1272
frappe/locale/hr.po
1272
frappe/locale/hr.po
File diff suppressed because it is too large
Load diff
1390
frappe/locale/hu.po
1390
frappe/locale/hu.po
File diff suppressed because it is too large
Load diff
1264
frappe/locale/id.po
1264
frappe/locale/id.po
File diff suppressed because it is too large
Load diff
1260
frappe/locale/it.po
1260
frappe/locale/it.po
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
31819
frappe/locale/my.po
Normal file
File diff suppressed because it is too large
Load diff
4647
frappe/locale/nb.po
4647
frappe/locale/nb.po
File diff suppressed because it is too large
Load diff
1260
frappe/locale/nl.po
1260
frappe/locale/nl.po
File diff suppressed because it is too large
Load diff
2076
frappe/locale/pl.po
2076
frappe/locale/pl.po
File diff suppressed because it is too large
Load diff
1306
frappe/locale/pt.po
1306
frappe/locale/pt.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1260
frappe/locale/ru.po
1260
frappe/locale/ru.po
File diff suppressed because it is too large
Load diff
1272
frappe/locale/sr.po
1272
frappe/locale/sr.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1310
frappe/locale/sv.po
1310
frappe/locale/sv.po
File diff suppressed because it is too large
Load diff
31819
frappe/locale/ta.po
Normal file
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
Loading…
Add table
Reference in a new issue