Merge branch 'develop' into fix-note-2

This commit is contained in:
Raffael Meyer 2023-04-05 19:14:28 +02:00 committed by GitHub
commit 417f2bbd97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
127 changed files with 1527 additions and 619 deletions

View file

@ -1,50 +1,73 @@
import sys
import requests
from urllib.parse import urlparse
import requests
docs_repos = [
"frappe_docs",
"erpnext_documentation",
WEBSITE_REPOS = [
"erpnext_com",
"frappe_io",
]
DOCUMENTATION_DOMAINS = [
"docs.erpnext.com",
"frappeframework.com",
]
def uri_validator(x):
result = urlparse(x)
return all([result.scheme, result.netloc, result.path])
def docs_link_exists(body):
for line in body.splitlines():
for word in line.split():
if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word)
if parsed_url.netloc == "github.com":
parts = parsed_url.path.split('/')
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
return True
if parsed_url.netloc in ["docs.erpnext.com", "frappeframework.com"]:
return True
def is_valid_url(url: str) -> bool:
parts = urlparse(url)
return all((parts.scheme, parts.netloc, parts.path))
def is_documentation_link(word: str) -> bool:
if not word.startswith("http") or not is_valid_url(word):
return False
parsed_url = urlparse(word)
if parsed_url.netloc in DOCUMENTATION_DOMAINS:
return True
if parsed_url.netloc == "github.com":
parts = parsed_url.path.split("/")
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in WEBSITE_REPOS:
return True
return False
def contains_documentation_link(body: str) -> bool:
return any(
is_documentation_link(word)
for line in body.splitlines()
for word in line.split()
)
def check_pull_request(number: str) -> "tuple[int, str]":
response = requests.get(f"https://api.github.com/repos/frappe/frappe/pulls/{number}")
if not response.ok:
return 1, "Pull Request Not Found! ⚠️"
payload = response.json()
title = (payload.get("title") or "").lower().strip()
head_sha = (payload.get("head") or {}).get("sha")
body = (payload.get("body") or "").lower()
if (
not title.startswith("feat")
or not head_sha
or "no-docs" in body
or "backport" in body
):
return 0, "Skipping documentation checks... 🏃"
if contains_documentation_link(body):
return 0, "Documentation Link Found. You're Awesome! 🎉"
return 1, "Documentation Link Not Found! ⚠️"
if __name__ == "__main__":
pr = sys.argv[1]
response = requests.get(f"https://api.github.com/repos/frappe/frappe/pulls/{pr}")
if response.ok:
payload = response.json()
title = (payload.get("title") or "").lower()
head_sha = (payload.get("head") or {}).get("sha")
body = (payload.get("body") or "").lower()
if title.startswith("feat") and head_sha and "no-docs" not in body:
if docs_link_exists(body):
print("Documentation Link Found. You're Awesome! 🎉")
else:
print("Documentation Link Not Found! ⚠️")
sys.exit(1)
else:
print("Skipping documentation checks... 🏃")
exit_code, message = check_pull_request(sys.argv[1])
print(message)
sys.exit(exit_code)

View file

@ -92,7 +92,8 @@ jobs:
${{ runner.os }}-pip-
${{ runner.os }}-
- run: |
- name: Install and run pip-audit
run: |
pip install pip-audit
cd ${GITHUB_WORKSPACE}
sed -i '/dropbox/d' pyproject.toml # Remove dropbox temporarily https://github.com/dropbox/dropbox-sdk-python/pull/456

View file

@ -98,15 +98,17 @@ context("Kanban Board", () => {
});
it("Checks if Kanban Board edits are blocked for non-System Manager and non-owner of the Board", () => {
// create admin kanban board
cy.call("frappe.tests.ui_test_helpers.create_todo", { description: "Frappe User ToDo" });
cy.switch_to_user("Administrator");
cy.call("frappe.tests.ui_test_helpers.create_admin_kanban");
// remove sys manager
cy.remove_role("frappe@example.com", "System Manager");
cy.switch_to_user("frappe@example.com");
const noSystemManager = "nosysmanager@example.com";
cy.call("frappe.tests.ui_test_helpers.create_test_user", {
username: noSystemManager,
});
cy.remove_role(noSystemManager, "System Manager");
cy.call("frappe.tests.ui_test_helpers.create_todo", { description: "Frappe User ToDo" });
cy.call("frappe.tests.ui_test_helpers.create_admin_kanban");
cy.switch_to_user(noSystemManager);
cy.visit("/app/todo/view/kanban/Admin Kanban");
@ -122,7 +124,8 @@ context("Kanban Board", () => {
// Column actions should be hidden (dropdown for 'Archive' and indicators)
cy.get(".kanban .column-options").should("have.length", 0);
cy.add_role("frappe@example.com", "System Manager");
cy.switch_to_user("Administrator");
cy.call("frappe.client.delete", { doctype: "User", name: noSystemManager });
});
after(() => {

View file

@ -54,12 +54,8 @@ context("List View", () => {
method: "POST",
url: "api/method/frappe.model.workflow.bulk_workflow_approval",
}).as("bulk-approval");
cy.intercept({
method: "POST",
url: "api/method/frappe.desk.reportview.get",
}).as("real-time-update");
cy.wrap(elements).contains("Approve").click();
cy.wait(["@bulk-approval", "@real-time-update"]);
cy.wait("@bulk-approval");
cy.wait(300);
cy.get_open_dialog().find(".btn-modal-close").click();
cy.reload();

View file

@ -0,0 +1,48 @@
context("List View", () => {
before(() => {
cy.login();
cy.go_to_list("DocType");
});
it("List view check rows on drag", () => {
cy.get(".list-row-checkbox").then(($checkbox) => {
cy.wrap($checkbox).first().trigger("mousedown");
cy.get(".level.list-row").each(($ele) => {
cy.wrap($ele).trigger("mousemove");
});
cy.document().trigger("mouseup");
});
cy.get(".level.list-row .list-row-checkbox").each(($checkbox) => {
cy.wrap($checkbox).should("be.checked");
});
});
it("Check all rows are checked", () => {
cy.get(".level.list-row .list-row-checkbox")
.its("length")
.then((len) => {
cy.get(".level-item.list-header-meta")
.should("be.visible")
.should("contain.text", `${len} items selected`);
});
});
it("List view uncheck rows on drag", () => {
cy.get(".list-row-checkbox").then(($checkbox) => {
cy.wrap($checkbox).first().trigger("mousedown");
cy.get(".level.list-row").each(($ele) => {
cy.wrap($ele).trigger("mousemove");
});
cy.document().trigger("mouseup");
});
cy.get(".level.list-row .list-row-checkbox").each(($checkbox) => {
cy.wrap($checkbox).should("not.be.checked");
});
});
it("Check all rows are unchecked", () => {
cy.get(".level-item.list-header-meta").should("not.be.visible");
});
});

View file

@ -4,11 +4,11 @@ context("Rounding behaviour", () => {
cy.visit("/app/");
});
it("Rounds floats accurately", () => {
it("Commercial Rounding", () => {
cy.window()
.its("flt")
.then((flt) => {
let rounding_method = "Rounding Half Away From Zero";
let rounding_method = "Commercial Rounding";
expect(flt("0.5", 0, null, rounding_method)).eq(1);
expect(flt("0.3", null, null, rounding_method)).eq(0.3);
@ -41,4 +41,64 @@ context("Rounding behaviour", () => {
expect(flt(-0.15, 1, null, rounding_method)).eq(-0.2);
});
});
it("Banker's Rounding", () => {
cy.window()
.its("flt")
.then((flt) => {
let rounding_method = "Banker's Rounding";
expect(flt("0.5", 0, null, rounding_method)).eq(0);
expect(flt("0.3", null, rounding_method)).eq(0.3);
expect(flt("1.5", 0, null, rounding_method)).eq(2);
// positive rounding to integers
expect(flt(0.4, 0, null, rounding_method)).eq(0);
expect(flt(0.5, 0, null, rounding_method)).eq(0);
expect(flt(1.455, 0, null, rounding_method)).eq(1);
expect(flt(1.5, 0, null, rounding_method)).eq(2);
// negative rounding to integers
expect(flt(-0.5, 0, null, rounding_method)).eq(0);
expect(flt(-1.5, 0, null, rounding_method)).eq(-2);
// negative precision i.e. round to nearest 10th
expect(flt(123, -1, null, rounding_method)).eq(120);
expect(flt(125, -1, null, rounding_method)).eq(120);
expect(flt(134.45, -1, null, rounding_method)).eq(130);
expect(flt(135, -1, null, rounding_method)).eq(140);
// positive multiple digit rounding
expect(flt(1.25, 1, null, rounding_method)).eq(1.2);
expect(flt(0.15, 1, null, rounding_method)).eq(0.2);
expect(flt(2.675, 2, null, rounding_method)).eq(2.68);
expect(flt(-2.675, 2, null, rounding_method)).eq(-2.68);
// negative multiple digit rounding
expect(flt(-1.25, 1, null, rounding_method)).eq(-1.2);
expect(flt(-0.15, 1, null, rounding_method)).eq(-0.2);
// Nearest number and not even (the default behaviour)
expect(flt(0.5, 0, null, rounding_method)).eq(0);
expect(flt(1.5, 0, null, rounding_method)).eq(2);
expect(flt(2.5, 0, null, rounding_method)).eq(2);
expect(flt(3.5, 0, null, rounding_method)).eq(4);
expect(flt(0.05, 1, null, rounding_method)).eq(0.0);
expect(flt(1.15, 1, null, rounding_method)).eq(1.2);
expect(flt(2.25, 1, null, rounding_method)).eq(2.2);
expect(flt(3.35, 1, null, rounding_method)).eq(3.4);
expect(flt(-0.5, 0, null, rounding_method)).eq(0);
expect(flt(-1.5, 0, null, rounding_method)).eq(-2);
expect(flt(-2.5, 0, null, rounding_method)).eq(-2);
expect(flt(-3.5, 0, null, rounding_method)).eq(-4);
expect(flt(-0.05, 1, null, rounding_method)).eq(0.0);
expect(flt(-1.15, 1, null, rounding_method)).eq(-1.2);
expect(flt(-2.25, 1, null, rounding_method)).eq(-2.2);
expect(flt(-3.35, 1, null, rounding_method)).eq(-3.4);
});
});
});

View file

@ -1274,7 +1274,7 @@ def reload_doc(
return frappe.modules.reload_doc(module, dt, dn, force=force, reset_permissions=reset_permissions)
@whitelist()
@whitelist(methods=["POST", "PUT"])
def rename_doc(
doctype: str,
old: str,

View file

@ -63,6 +63,7 @@ user_cache_keys = (
"has_role:Page",
"has_role:Report",
"desk_sidebar_items",
"contacts",
)
doctype_cache_keys = (

View file

@ -24,7 +24,11 @@ EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True}
@click.option("--app", help="Build assets for app")
@click.option("--apps", help="Build assets for specific apps")
@click.option(
"--hard-link", is_flag=True, default=False, help="Copy the files instead of symlinking"
"--hard-link",
is_flag=True,
default=False,
help="Copy the files instead of symlinking",
envvar="FRAPPE_HARD_LINK_ASSETS",
)
@click.option(
"--make-copy",
@ -908,7 +912,7 @@ def run_ui_tests(
os.chdir(app_base_path)
node_bin = subprocess.getoutput("yarn bin")
node_bin = subprocess.getoutput("(cd ../frappe && yarn bin)")
cypress_path = f"{node_bin}/cypress"
drag_drop_plugin_path = f"{node_bin}/../@4tw/cypress-drag-drop"
real_events_plugin_path = f"{node_bin}/../cypress-real-events"
@ -935,7 +939,7 @@ def run_ui_tests(
"@cypress/code-coverage@^3",
]
)
frappe.commands.popen(f"yarn add {packages} --no-lockfile")
frappe.commands.popen(f"(cd ../frappe && yarn add {packages} --no-lockfile)")
# run for headless mode
run_or_open = "run --browser chrome --record" if headless else "open"

View file

@ -254,19 +254,23 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
"""select
`tabAddress`.name, `tabAddress`.city, `tabAddress`.country
from
`tabAddress`, `tabDynamic Link`
`tabAddress`
join `tabDynamic Link`
on (`tabDynamic Link`.parent = `tabAddress`.name and `tabDynamic Link`.parenttype = 'Address')
where
`tabDynamic Link`.parent = `tabAddress`.name and
`tabDynamic Link`.parenttype = 'Address' and
`tabDynamic Link`.link_doctype = %(link_doctype)s and
`tabDynamic Link`.link_name = %(link_name)s and
ifnull(`tabAddress`.disabled, 0) = 0 and
({search_condition})
{mcond} {condition}
order by
if(locate(%(_txt)s, `tabAddress`.name), locate(%(_txt)s, `tabAddress`.name), 99999),
case
when locate(%(_txt)s, `tabAddress`.name) != 0
then locate(%(_txt)s, `tabAddress`.name)
else 99999
end,
`tabAddress`.idx desc, `tabAddress`.name
limit %(start)s, %(page_len)s """.format(
limit %(page_len)s offset %(start)s""".format(
mcond=get_match_cond(doctype),
search_condition=search_condition,
condition=condition or "",

View file

@ -1,7 +1,9 @@
# Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE
from functools import partial
import frappe
from frappe.contacts.doctype.address.address import get_address_display
from frappe.contacts.doctype.address.address import address_query, get_address_display
from frappe.tests.utils import FrappeTestCase
@ -28,3 +30,29 @@ class TestAddress(FrappeTestCase):
address = frappe.get_list("Address")[0].name
display = get_address_display(frappe.get_doc("Address", address).as_dict())
self.assertTrue(display)
def test_address_query(self):
def query(doctype="Address", txt="", searchfield="name", start=0, page_len=20, filters=None):
if filters is None:
filters = {"link_doctype": "User", "link_name": "Administrator"}
return address_query(doctype, txt, searchfield, start, page_len, filters)
frappe.get_doc(
{
"address_type": "Billing",
"address_line1": "1",
"city": "Mumbai",
"state": "Maharashtra",
"country": "India",
"doctype": "Address",
"links": [
{
"link_doctype": "User",
"link_name": "Administrator",
}
],
}
).insert()
self.assertGreaterEqual(len(query(txt="Admin")), 1)
self.assertEqual(len(query(txt="what_zyx")), 0)

View file

@ -0,0 +1,7 @@
frappe.listview_settings["Access Log"] = {
onload: function (list_view) {
frappe.require("logtypes.bundle.js", () => {
frappe.utils.logtypes.show_log_retention_message(list_view.doctype);
});
},
};

View file

@ -318,7 +318,7 @@
},
{
"fieldname": "message_id",
"fieldtype": "Data",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Message ID",
"length": 995,
@ -395,7 +395,7 @@
"icon": "fa fa-comment",
"idx": 1,
"links": [],
"modified": "2022-05-09 00:13:45.310564",
"modified": "2023-03-16 12:04:18.113817",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication",

View file

@ -397,6 +397,7 @@ def on_doctype_update():
"""Add indexes in `tabCommunication`"""
frappe.db.add_index("Communication", ["reference_doctype", "reference_name"])
frappe.db.add_index("Communication", ["status", "communication_type"])
frappe.db.add_index("Communication", ["message_id(140)"])
def has_permission(doc, ptype, user):

View file

@ -165,7 +165,7 @@ def _make(
if not comm.get_outgoing_email_account():
frappe.throw(
_(
"Unable to send mail because of a missing email account. Please setup default Email Account from Setup > Email > Email Account"
"Unable to send mail because of a missing email account. Please setup default Email Account from Settings > Email Account"
),
exc=frappe.OutgoingEmailError,
)

View file

@ -70,7 +70,7 @@ class CommunicationEmailMixin:
if include_sender:
cc.append(self.sender_mailid)
if is_inbound_mail_communcation:
if (doc_owner := self.get_owner()) not in frappe.STANDARD_USERS:
if (doc_owner := self.get_owner()) and (doc_owner not in frappe.STANDARD_USERS):
cc.append(doc_owner)
cc = set(cc) - {self.sender_mailid}
cc.update(self.get_assignees())
@ -216,7 +216,11 @@ class CommunicationEmailMixin:
"reference_name": self.reference_name,
"reference_type": self.reference_doctype,
}
return ToDo.get_owners(filters)
if self.reference_doctype and self.reference_name:
return ToDo.get_owners(filters)
else:
return []
@staticmethod
def filter_thread_notification_disbled_users(emails):

View file

@ -7,6 +7,7 @@ import frappe
from frappe import _
from frappe.desk.doctype.bulk_update.bulk_update import show_progress
from frappe.model.document import Document
from frappe.model.workflow import get_workflow_name
class DeletedDocument(Document):
@ -27,6 +28,11 @@ def restore(name, alert=True):
except frappe.DocstatusTransitionError:
frappe.msgprint(_("Cancelled Document restored as Draft"))
doc.docstatus = 0
active_workflow = get_workflow_name(doc.doctype)
if active_workflow:
workflow_state_fieldname = frappe.get_value("Workflow", active_workflow, "workflow_state_field")
if doc.get(workflow_state_fieldname):
doc.set(workflow_state_fieldname, None)
doc.insert()
doc.add_comment("Edit", _("restored {0} as {1}").format(deleted.deleted_name, doc.name))

View file

@ -4,7 +4,7 @@
import frappe
import frappe.share
from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
test_dependencies = ["User"]
@ -139,3 +139,61 @@ class TestDocShare(FrappeTestCase):
test_doc.reload()
self.assertTrue(test_doc.has_permission("read"))
@change_settings("System Settings", {"disable_document_sharing": 1})
def test_share_disabled_add(self):
"Test if user loses share access on disabling share globally."
frappe.share.add("Event", self.event.name, self.user, share=1) # Share as admin
frappe.set_user(self.user)
# User does not have share access although given to them
self.assertFalse(self.event.has_permission("share"))
self.assertRaises(
frappe.PermissionError, frappe.share.add, "Event", self.event.name, "test1@example.com"
)
@change_settings("System Settings", {"disable_document_sharing": 1})
def test_share_disabled_add_with_ignore_permissions(self):
frappe.share.add("Event", self.event.name, self.user, share=1)
frappe.set_user(self.user)
# User does not have share access although given to them
self.assertFalse(self.event.has_permission("share"))
# Test if behaviour is consistent for developer overrides
frappe.share.add_docshare(
"Event", self.event.name, "test1@example.com", flags={"ignore_share_permission": True}
)
@change_settings("System Settings", {"disable_document_sharing": 1})
def test_share_disabled_set_permission(self):
frappe.share.add("Event", self.event.name, self.user, share=1)
frappe.set_user(self.user)
# User does not have share access although given to them
self.assertFalse(self.event.has_permission("share"))
self.assertRaises(
frappe.PermissionError,
frappe.share.set_permission,
"Event",
self.event.name,
"test1@example.com",
"read",
)
@change_settings("System Settings", {"disable_document_sharing": 1})
def test_share_disabled_assign_to(self):
"""
Assigning a document to a user without access must not share the document,
if sharing disabled.
"""
from frappe.desk.form.assign_to import add
frappe.share.add("Event", self.event.name, self.user, share=1)
frappe.set_user(self.user)
self.assertRaises(
frappe.ValidationError,
add,
{"doctype": "Event", "name": self.event.name, "assign_to": ["test1@example.com"]},
)

View file

@ -42,12 +42,14 @@ frappe.ui.form.on("DocType", {
if (!frappe.boot.developer_mode && !frm.doc.custom) {
// make the document read-only
frm.set_read_only();
frm.dashboard.clear_comment();
frm.dashboard.add_comment(
__("DocTypes can not be modified, please use {0} instead", [customize_form_link]),
"blue",
true
);
} else if (frappe.boot.developer_mode) {
frm.dashboard.clear_comment();
let msg = __(
"This site is running in developer mode. Any change made here will be updated in code."
);

View file

@ -189,6 +189,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.istable",
"fieldname": "beta",
"fieldtype": "Check",
"label": "Beta"
@ -463,6 +464,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.istable",
"description": "Tree structures are implemented using Nested Set",
"fieldname": "is_tree",
"fieldtype": "Check",
@ -539,6 +541,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.istable",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Is Virtual"
@ -622,6 +625,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.istable",
"description": "Enables Calendar and Gantt views.",
"fieldname": "is_calendar_and_gantt",
"fieldtype": "Check",
@ -708,7 +712,7 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2023-01-04 17:23:09.206018",
"modified": "2023-03-23 16:15:51.067267",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -20,6 +20,7 @@ DEFAULT_LOGTYPES_RETENTION = {
"Submission Queue": 30,
"Prepared Report": 30,
"Webhook Request Log": 30,
"Integration Request": 90,
"Reminder": 30,
}

View file

@ -0,0 +1,7 @@
frappe.listview_settings["Prepared Report"] = {
onload: function (list_view) {
frappe.require("logtypes.bundle.js", () => {
frappe.utils.logtypes.show_log_retention_message(list_view.doctype);
});
},
};

View file

@ -4,8 +4,6 @@ from ..role import desk_properties
def execute():
frappe.reload_doctype("user")
frappe.reload_doctype("role")
for role in frappe.get_all("Role", ["name", "desk_access"]):
role_doc = frappe.get_doc("Role", role.name)
for key in desk_properties:

View file

@ -64,13 +64,14 @@ class Role(Document):
user.save()
def get_info_based_on_role(role, field="email"):
def get_info_based_on_role(role, field="email", ignore_permissions=False):
"""Get information of all users that have been assigned this role"""
users = frappe.get_list(
"Has Role",
filters={"role": role, "parenttype": "User"},
parent_doctype="User",
fields=["parent as user_name"],
ignore_permissions=ignore_permissions,
)
return get_user_info(users, field)

View file

@ -4,11 +4,15 @@ frappe.listview_settings["RQ Job"] = {
onload(listview) {
if (!has_common(frappe.user_roles, ["Administrator", "System Manager"])) return;
listview.page.add_inner_button(__("Remove Failed Jobs"), () => {
frappe.confirm(__("Are you sure you want to remove all failed jobs?"), () => {
frappe.xcall("frappe.core.doctype.rq_job.rq_job.remove_failed_jobs");
});
});
listview.page.add_inner_button(
__("Remove Failed Jobs"),
() => {
frappe.confirm(__("Are you sure you want to remove all failed jobs?"), () => {
frappe.xcall("frappe.core.doctype.rq_job.rq_job.remove_failed_jobs");
});
},
__("Actions")
);
if (listview.list_view_settings) {
listview.list_view_settings.disable_count = 1;
@ -20,6 +24,25 @@ frappe.listview_settings["RQ Job"] = {
listview.page.set_indicator(__("Scheduler: Active"), "green");
} else {
listview.page.set_indicator(__("Scheduler: Inactive"), "red");
listview.page.add_inner_button(
__("Enable Scheduler"),
() => {
frappe.confirm(__("Are you sure you want to re-enable scheduler?"), () => {
frappe
.xcall("frappe.utils.scheduler.activate_scheduler")
.then(() => {
frappe.show_alert(__("Enabled Scheduler"));
})
.catch((e) => {
frappe.show_alert({
message: __("Failed to enable scheduler: {0}", e),
indicator: "error",
});
});
});
},
__("Actions")
);
}
});

View file

@ -44,6 +44,12 @@ class TestRQJob(FrappeTestCase):
)
self.check_status(job, "finished")
def test_configurable_ttl(self):
frappe.conf.rq_job_failure_ttl = 600
job = frappe.enqueue(method=self.BG_JOB, queue="short")
self.assertEqual(job.failure_ttl, 600)
def test_func_obj_serialization(self):
job = frappe.enqueue(method=test_func, queue="short")
rq_job = frappe.get_doc("RQ Job", job.id)

View file

@ -169,7 +169,6 @@ class ServerScript(Document):
return items
@frappe.whitelist()
def setup_scheduler_events(script_name, frequency):
"""Creates or Updates Scheduled Job Type documents based on the specified script name and frequency

View file

@ -16,6 +16,8 @@ frappe.ui.form.on("System Settings", {
}
},
});
frm.trigger("set_rounding_method_options");
},
enable_password_policy: function (frm) {
if (frm.doc.enable_password_policy == 0) {
@ -56,4 +58,17 @@ frappe.ui.form.on("System Settings", {
}
);
},
set_rounding_method_options: function (frm) {
if (frm.doc.rounding_method != "Banker's Rounding (legacy)") {
let field = frm.fields_dict.rounding_method;
field.df.options = field.df.options
.split("\n")
.filter((o) => o != "Banker's Rounding (legacy)")
.join("\n");
field.refresh();
}
},
});

View file

@ -13,6 +13,7 @@
"time_zone",
"enable_onboarding",
"setup_complete",
"disable_document_sharing",
"date_and_number_format",
"date_format",
"time_format",
@ -523,17 +524,23 @@
"label": "Login with email link expiry (in minutes)"
},
{
"default": "Round Half Even",
"default": "Banker's Rounding (legacy)",
"fieldname": "rounding_method",
"fieldtype": "Select",
"label": "Rounding Method",
"options": "Round Half Even\nRounding Half Away From Zero"
"options": "Banker's Rounding (legacy)\nBanker's Rounding\nCommercial Rounding"
},
{
"default": "0",
"fieldname": "disable_document_sharing",
"fieldtype": "Check",
"label": "Disable Document Sharing"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2023-03-06 11:31:19.144956",
"modified": "2023-03-14 11:30:56.465653",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
@ -552,4 +559,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View file

@ -114,12 +114,16 @@ frappe.ui.form.on("User", {
return;
}
function hasChanged(doc_attr, boot_attr) {
return (doc_attr || boot_attr) && doc_attr !== boot_attr;
}
if (
doc.name === frappe.session.user &&
!doc.__unsaved &&
frappe.all_timezones &&
(doc.language || frappe.boot.user.language) &&
doc.language !== frappe.boot.user.language
(hasChanged(doc.language, frappe.boot.user.language) ||
hasChanged(doc.time_zone, frappe.boot.time_zone.user))
) {
frappe.msgprint(__("Refreshing..."));
window.location.reload();
@ -215,7 +219,10 @@ frappe.ui.form.on("User", {
});
}
if (frappe.session.user == doc.name || frappe.user.has_role("System Manager")) {
if (
cint(frappe.boot.sysdefaults.enable_two_factor_auth) &&
(frappe.session.user == doc.name || frappe.user.has_role("System Manager"))
) {
frm.add_custom_button(
__("Reset OTP Secret"),
function () {

View file

@ -25,6 +25,27 @@ frappe.ui.form.on("Custom Field", {
frm.toggle_enable("dt", frm.doc.__islocal);
frm.trigger("dt");
frm.toggle_reqd("label", !frm.doc.fieldname);
if (frm.doc.is_system_generated) {
frm.dashboard.add_comment(
__(
"<strong>Warning:</strong> This field is system generated and may be overwritten by a future update. Modify it using {0} instead.",
[
frappe.utils.get_form_link(
"Customize Form",
"Customize Form",
true,
__("Customize Form"),
{
doc_type: frm.doc.dt,
}
),
]
),
"yellow",
true
);
}
},
dt: function (frm) {
if (!frm.doc.dt) {

View file

@ -89,7 +89,7 @@ frappe.ui.form.on("Customize Form", {
setup_sortable: function (frm) {
frm.doc.fields.forEach(function (f) {
if (!f.is_custom_field) {
if (!f.is_custom_field || f.is_system_generated) {
f._sortable = false;
}
@ -251,10 +251,23 @@ frappe.ui.form.on("Customize Form", {
// can't delete standard fields
frappe.ui.form.on("Customize Form Field", {
before_fields_remove: function (frm, doctype, name) {
var row = frappe.get_doc(doctype, name);
const row = frappe.get_doc(doctype, name);
if (row.is_system_generated) {
frappe.throw(
__(
"Cannot delete system generated field <strong>{0}</strong>. You can hide it instead.",
[__(row.label) || row.fieldname]
)
);
}
if (!(row.is_custom_field || row.__islocal)) {
frappe.msgprint(__("Cannot delete standard field. You can hide it if you want"));
throw "cannot delete standard field";
frappe.throw(
__("Cannot delete standard field <strong>{0}</strong>. You can hide it instead.", [
__(row.label) || row.fieldname,
])
);
}
},
fields_add: function (frm, cdt, cdn) {

View file

@ -193,8 +193,9 @@ class CustomizeForm(Document):
# docfield
for df in self.get("fields"):
meta_df = meta.get("fields", {"fieldname": df.fieldname})
if not meta_df or meta_df[0].get("is_custom_field"):
if not meta_df or not is_standard_or_system_generated_field(meta_df[0]):
continue
self.set_property_setters_for_docfield(meta, df, meta_df)
# action and links
@ -350,12 +351,14 @@ class CustomizeForm(Document):
def update_custom_fields(self):
for i, df in enumerate(self.get("fields")):
if df.get("is_custom_field"):
if not frappe.db.exists("Custom Field", {"dt": self.doc_type, "fieldname": df.fieldname}):
self.add_custom_field(df, i)
self.flags.update_db = True
else:
self.update_in_custom_field(df, i)
if is_standard_or_system_generated_field(df):
continue
if not frappe.db.exists("Custom Field", {"dt": self.doc_type, "fieldname": df.fieldname}):
self.add_custom_field(df, i)
self.flags.update_db = True
else:
self.update_in_custom_field(df, i)
self.delete_custom_fields()
@ -380,7 +383,7 @@ class CustomizeForm(Document):
def update_in_custom_field(self, df, i):
meta = frappe.get_meta(self.doc_type)
meta_df = meta.get("fields", {"fieldname": df.fieldname})
if not (meta_df and meta_df[0].get("is_custom_field")):
if not meta_df or is_standard_or_system_generated_field(meta_df[0]):
# not a custom field
return
@ -416,7 +419,7 @@ class CustomizeForm(Document):
}
for fieldname in fields_to_remove:
df = meta.get("fields", {"fieldname": fieldname})[0]
if df.get("is_custom_field"):
if not is_standard_or_system_generated_field(df):
frappe.delete_doc("Custom Field", df.name)
def make_property_setter(
@ -561,6 +564,10 @@ def reset_customization(doctype):
frappe.clear_cache(doctype=doctype)
def is_standard_or_system_generated_field(df):
return not df.get("is_custom_field") or df.get("is_system_generated")
doctype_properties = {
"search_fields": "Data",
"title_field": "Data",

View file

@ -403,3 +403,25 @@ class TestCustomizeForm(FrappeTestCase):
with self.assertRaises(frappe.ValidationError):
d.run_method("save_customization")
def test_system_generated_fields(self):
doctype = "Event"
custom_field_name = "test_custom_field"
custom_field = frappe.get_doc("Custom Field", {"dt": doctype, "fieldname": custom_field_name})
custom_field.is_system_generated = 1
custom_field.save()
d = self.get_customize_form(doctype)
custom_field = d.getone("fields", {"fieldname": custom_field_name})
custom_field.description = "Test Description"
d.run_method("save_customization")
property_setter_filters = {
"doc_type": doctype,
"field_name": custom_field_name,
"property": "description",
}
self.assertEqual(
frappe.db.get_value("Property Setter", property_setter_filters, "value"), "Test Description"
)

View file

@ -1300,8 +1300,8 @@ def enqueue_jobs_after_commit():
execute_job,
timeout=job.get("timeout"),
kwargs=job.get("queue_args"),
failure_ttl=RQ_JOB_FAILURE_TTL,
result_ttl=RQ_RESULTS_TTL,
failure_ttl=frappe.conf.get("rq_job_failure_ttl") or RQ_JOB_FAILURE_TTL,
result_ttl=frappe.conf.get("rq_results_ttl") or RQ_RESULTS_TTL,
)
frappe.flags.enqueue_after_commit = []

View file

@ -200,7 +200,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
return db_size[0].get("database_size")
def log_query(self, query, values, debug, explain):
self.last_query = query = self._cursor._last_executed
self.last_query = query = self._cursor._executed
self._log_query(query, debug, explain)
return self.last_query

View file

@ -16,38 +16,27 @@ frappe.ui.form.on("Bulk Update", {
if (!frm.doc.update_value) {
frappe.throw(__('Field "value" is mandatory. Please specify value to be updated'));
} else {
frappe
.call({
method: "frappe.desk.doctype.bulk_update.bulk_update.update",
args: {
doctype: frm.doc.document_type,
field: frm.doc.field,
value: frm.doc.update_value,
condition: frm.doc.condition,
limit: frm.doc.limit,
},
})
.then((r) => {
let failed = r.message;
if (!failed) failed = [];
frm.call("bulk_update").then((r) => {
let failed = r.message;
if (!failed) failed = [];
if (failed.length && !r._server_messages) {
frappe.throw(
__("Cannot update {0}", [
failed.map((f) => (f.bold ? f.bold() : f)).join(", "),
])
);
} else {
frappe.msgprint({
title: __("Success"),
message: __("Updated Successfully"),
indicator: "green",
});
}
if (failed.length && !r._server_messages) {
frappe.throw(
__("Cannot update {0}", [
failed.map((f) => (f.bold ? f.bold() : f)).join(", "),
])
);
} else {
frappe.msgprint({
title: __("Success"),
message: __("Updated Successfully"),
indicator: "green",
});
}
frappe.hide_progress();
frm.save();
});
frappe.hide_progress();
frm.save();
});
}
});
},

View file

@ -10,26 +10,24 @@ from frappe.utils.scheduler import is_scheduler_inactive
class BulkUpdate(Document):
pass
@frappe.whitelist()
def bulk_update(self):
self.check_permission("write")
limit = self.limit if self.limit and cint(self.limit) < 500 else 500
condition = ""
if self.condition:
if ";" in self.condition:
frappe.throw(_("; not allowed in condition"))
@frappe.whitelist()
def update(doctype, field, value, condition="", limit=500):
if not limit or cint(limit) > 500:
limit = 500
condition = f" where {self.condition}"
if condition:
condition = " where " + condition
if ";" in condition:
frappe.throw(_("; not allowed in condition"))
docnames = frappe.db.sql_list(
f"""select name from `tab{doctype}`{condition} limit {limit} offset 0"""
)
data = {}
data[field] = value
return submit_cancel_or_update_docs(doctype, docnames, "update", data)
docnames = frappe.db.sql_list(
f"""select name from `tab{self.document_type}`{condition} limit {limit} offset 0"""
)
return submit_cancel_or_update_docs(
self.document_type, docnames, "update", {self.field: self.update_value}
)
@frappe.whitelist()

View file

@ -93,10 +93,17 @@ def add(args=None):
doc = frappe.get_doc(args["doctype"], args["name"])
# if assignee does not have permissions, share
# if assignee does not have permissions, share or inform
if not frappe.has_permission(doc=doc, user=assign_to):
frappe.share.add(doc.doctype, doc.name, assign_to)
shared_with_users.append(assign_to)
if frappe.get_system_settings("disable_document_sharing"):
msg = _("User {0} is not permitted to access this document.").format(frappe.bold(assign_to))
msg += "<br>" + _(
"As document sharing is disabled, please give them the required permissions before assigning."
)
frappe.throw(msg, title=_("Missing Permission"))
else:
frappe.share.add(doc.doctype, doc.name, assign_to)
shared_with_users.append(assign_to)
# make this document followed by assigned user
if frappe.get_cached_value("User", assign_to, "follow_assigned_documents"):

View file

@ -57,7 +57,14 @@ def update_comment(name, content):
if frappe.session.user not in ["Administrator", doc.owner]:
frappe.throw(_("Comment can only be edited by the owner"), frappe.PermissionError)
doc.content = content
if doc.reference_doctype and doc.reference_name:
reference_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name)
reference_doc.check_permission()
doc.content = extract_images_from_html(reference_doc, content, is_private=True)
else:
doc.content = content
doc.save(ignore_permissions=True)

View file

@ -165,6 +165,7 @@ def update_system_settings(args):
"language": get_language_code(args.get("language")) or "en",
"time_zone": args.get("timezone"),
"float_precision": 3,
"rounding_method": "Banker's Rounding",
"date_format": frappe.db.get_value("Country", args.get("country"), "date_format"),
"time_format": frappe.db.get_value("Country", args.get("country"), "time_format"),
"number_format": number_format,

View file

@ -11,7 +11,7 @@ def sendmail_to_system_managers(subject, content):
@frappe.whitelist()
def get_contact_list(txt, page_length=20) -> list[dict]:
"""Returns contacts (from autosuggest)"""
"""Return email ids for a multiselect field."""
if cached_contacts := get_cached_contacts(txt):
return cached_contacts[:page_length]
@ -19,11 +19,14 @@ def get_contact_list(txt, page_length=20) -> list[dict]:
reportview_conditions = build_match_conditions("Contact")
match_conditions = f"and {reportview_conditions}" if reportview_conditions else ""
# The multiselect field will store the `label` as the selected value.
# The `value` is just used as a unique key to distinguish between the options.
# https://github.com/frappe/frappe/blob/6c6a89bcdd9454060a1333e23b855d0505c9ebc2/frappe/public/js/frappe/form/controls/autocomplete.js#L29-L35
out = frappe.db.sql(
f"""select email_id as value,
f"""select name as value, email_id as label,
concat(first_name, ifnull(concat(' ',last_name), '' )) as description
from tabContact
where name like %(txt)s or email_id like %(txt)s
where (name like %(txt)s or email_id like %(txt)s) and email_id != ''
{match_conditions}
limit %(page_length)s""",
{"txt": f"%{txt}%", "page_length": page_length},

View file

@ -19,7 +19,6 @@ from frappe.email.utils import get_port
from frappe.model.document import Document
from frappe.utils import cint, comma_or, cstr, parse_addr, validate_email_address
from frappe.utils.background_jobs import enqueue, get_jobs
from frappe.utils.error import raise_error_on_no_output
from frappe.utils.jinja import render_template
from frappe.utils.user import get_system_managers
@ -301,11 +300,6 @@ class EmailAccount(Document):
return cls.from_record({"sender": "notifications@example.com"})
@classmethod
@raise_error_on_no_output(
keep_quiet=lambda: not cint(frappe.get_system_settings("setup_complete")),
error_message=_("Please setup default Email Account from Setup > Email > Email Account"),
error_type=frappe.OutgoingEmailError,
) # noqa
@cache_email_account("outgoing_email_account")
def find_outgoing(cls, match_by_email=None, match_by_doctype=None, _raise_error=False):
"""Find the outgoing Email account to use.
@ -329,6 +323,12 @@ class EmailAccount(Document):
if doc:
return {"default": doc}
if _raise_error:
frappe.throw(
_("Please setup default Email Account from Settings > Email Account"),
frappe.OutgoingEmailError,
)
@classmethod
def find_default_outgoing(cls):
"""Find default outgoing account."""

View file

@ -67,10 +67,9 @@
},
{
"fieldname": "message_id",
"fieldtype": "Data",
"fieldtype": "Small Text",
"label": "Message ID",
"read_only": 1,
"search_index": 1
"read_only": 1
},
{
"fieldname": "reference_doctype",
@ -153,7 +152,7 @@
"idx": 1,
"in_create": 1,
"links": [],
"modified": "2022-07-12 15:17:37.934316",
"modified": "2023-03-16 12:15:17.850292",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Queue",

View file

@ -5,6 +5,7 @@ import json
import quopri
import smtplib
import traceback
from contextlib import suppress
from email.parser import Parser
from email.policy import SMTPUTF8
@ -407,6 +408,8 @@ def on_doctype_update():
"Email Queue", ("status", "send_after", "priority", "creation"), "index_bulk_flush"
)
frappe.db.add_index("Email Queue", ["message_id(140)"])
def get_email_retry_limit():
return cint(frappe.db.get_system_setting("email_retry_limit")) or 3
@ -704,7 +707,10 @@ class QueueBuilder:
if not smtp_server_instance:
email_account = q.get_email_account()
smtp_server_instance = email_account.get_smtp_server()
q.send(smtp_server_instance=smtp_server_instance)
with suppress(Exception):
q.send(smtp_server_instance=smtp_server_instance)
smtp_server_instance.quit()
def as_dict(self, include_recipients=True):

View file

@ -0,0 +1,11 @@
import frappe
def execute():
"""Drop search index on message_id"""
if frappe.db.get_column_type("Email Queue", "message_id") == "text":
return
if index := frappe.db.get_column_index("tabEmail Queue", "message_id", unique=False):
frappe.db.sql(f"ALTER TABLE `tabEmail Queue` DROP INDEX `{index.Key_name}`")

View file

@ -29,6 +29,7 @@
"message",
"message_md",
"message_html",
"campaign",
"attachments",
"send_unsubscribe_link",
"send_webview_link",
@ -237,6 +238,13 @@
"label": "Total Views",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "campaign",
"fieldtype": "Link",
"label": "Campaign",
"options": "Marketing Campaign",
"reqd": 0
}
],
"has_web_view": 1,
@ -245,7 +253,7 @@
"index_web_pages_for_search": 1,
"is_published_field": "published",
"links": [],
"modified": "2023-02-23 12:53:18.478018",
"modified": "2023-03-20 22:45:59.129630",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
@ -270,4 +278,4 @@
"states": [],
"title_field": "subject",
"track_changes": 1
}
}

View file

@ -167,7 +167,7 @@ class Newsletter(WebsiteGenerator):
attachments = self.get_newsletter_attachments()
sender = self.send_from or frappe.utils.get_formatted_email(self.owner)
args = self.as_dict()
args["message"] = self.get_message()
args["message"] = self.get_message(medium="email")
is_auto_commit_set = bool(frappe.db.auto_commit_on_many_writes)
frappe.db.auto_commit_on_many_writes = not frappe.flags.in_test
@ -193,7 +193,7 @@ class Newsletter(WebsiteGenerator):
frappe.db.auto_commit_on_many_writes = is_auto_commit_set
def get_message(self) -> str:
def get_message(self, medium=None) -> str:
message = self.message
if self.content_type == "Markdown":
message = frappe.utils.md_to_html(self.message_md)
@ -202,9 +202,9 @@ class Newsletter(WebsiteGenerator):
html = frappe.render_template(message, {"doc": self.as_dict()})
return self.add_source(html)
return self.add_source(html, medium=medium)
def add_source(self, html: str) -> str:
def add_source(self, html: str, medium="None") -> str:
"""Add source to the site links in the newsletter content."""
from bs4 import BeautifulSoup
@ -216,8 +216,8 @@ class Newsletter(WebsiteGenerator):
if href and not href.startswith("#"):
if not frappe.utils.is_site_link(href):
continue
new_href = frappe.utils.add_source_to_url(
href, reference_doctype=self.doctype, reference_docname=self.name
new_href = frappe.utils.add_trackers_to_url(
href, source="Newsletter", campaign=self.campaign, medium=medium
)
link["href"] = new_href
@ -377,14 +377,17 @@ def send_scheduled_email():
def newsletter_email_read(recipient_email, reference_doctype, reference_name):
verify_request()
try:
doc = frappe.get_doc(reference_doctype, reference_name)
doc = frappe.get_cached_doc("Newsletter", reference_name)
if doc.add_viewed(recipient_email, force=True, unique_views=True):
doc.db_set("total_views", frappe.utils.cint(doc.total_views) + 1, update_modified=False)
newsletter = frappe.qb.DocType("Newsletter")
(
frappe.qb.update(newsletter)
.set(newsletter.total_views, newsletter.total_views + 1)
.where(newsletter.name == doc.name)
).run()
except Exception:
frappe.log_error(
f"Unable to mark as viewed for {recipient_email}", None, reference_doctype, reference_name
)
doc.log_error(f"Unable to mark as viewed for {recipient_email}")
finally:
frappe.response.update(frappe.utils.get_imaginary_pixel_response())

View file

@ -36,7 +36,7 @@
</p>
</div>
<div itemprop="articleBody" class="longform blog-text">
{{ doc.get_message() }}
{{ doc.get_message(medium="web_page") }}
</div>
</article>

View file

@ -298,7 +298,7 @@ def get_context(context):
# For sending emails to specified role
if recipient.receiver_by_role:
emails = get_info_based_on_role(recipient.receiver_by_role, "email")
emails = get_info_based_on_role(recipient.receiver_by_role, "email", ignore_permissions=True)
for email in emails:
recipients = recipients + email.split("\n")

View file

@ -782,7 +782,7 @@ class InboundMail(Email):
Here are the cases to handle:
1. If mail is a reply to already sent mail, then we can get parent communicaion from
Email Queue record.
Email Queue record or message_id on communication.
2. Sometimes we send communication name in message-ID directly, use that to get parent communication.
3. Sender sent a reply but reply is on top of what (s)he sent before,
then parent record exists directly in communication.
@ -795,17 +795,15 @@ class InboundMail(Email):
if not self.is_reply():
return ""
if not self.is_reply_to_system_sent_mail():
communication = Communication.find_one_by_filters(
message_id=self.in_reply_to, creation=[">=", self.get_relative_dt(-30)]
)
elif self.parent_email_queue() and self.parent_email_queue().communication:
communication = Communication.find(self.parent_email_queue().communication, ignore_error=True)
else:
reference = self.in_reply_to
if "@" in self.in_reply_to:
reference, _ = self.in_reply_to.split("@", 1)
communication = Communication.find(reference, ignore_error=True)
communication = Communication.find_one_by_filters(message_id=self.in_reply_to)
if not communication:
if self.parent_email_queue() and self.parent_email_queue().communication:
communication = Communication.find(self.parent_email_queue().communication, ignore_error=True)
else:
reference = self.in_reply_to
if "@" in self.in_reply_to:
reference, _ = self.in_reply_to.split("@", 1)
communication = Communication.find(reference, ignore_error=True)
self._parent_communication = communication or ""
return self._parent_communication

View file

@ -69,9 +69,7 @@ class SMTPServer:
if not self.server:
frappe.msgprint(
_(
"Email Account not setup. Please create a new Email Account from Setup > Email > Email Account"
),
_("Email Account not setup. Please create a new Email Account from Settings > Email Account"),
raise_exception=frappe.OutgoingEmailError,
)

View file

@ -198,7 +198,7 @@ def upload_file():
filename = file.filename
content_type = guess_type(filename)[0]
if optimize and content_type.startswith("image/"):
if optimize and content_type and content_type.startswith("image/"):
args = {"content": content, "content_type": content_type}
if frappe.form_dict.max_width:
args["max_width"] = int(frappe.form_dict.max_width)

View file

@ -5,50 +5,43 @@ frappe.ui.form.on("Dropbox Settings", {
refresh: function (frm) {
frm.toggle_display(
["app_access_key", "app_secret_key"],
!(frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config)
!frm.doc.__onload?.dropbox_setup_via_site_config
);
frm.clear_custom_buttons();
frm.events.take_backup(frm);
},
are_keys_present: function (frm) {
return (
(frm.doc.app_access_key && frm.doc.app_secret_key) ||
frm.doc.__onload?.dropbox_setup_via_site_config
);
},
allow_dropbox_access: function (frm) {
if (frm.doc.app_access_key && frm.doc.app_secret_key) {
frappe.call({
method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_dropbox_authorize_url",
freeze: true,
callback: function (r) {
if (!r.exc) {
window.open(r.message.auth_url);
}
},
});
} else if (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config) {
frappe.call({
method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_redirect_url",
freeze: true,
callback: function (r) {
if (!r.exc) {
window.open(r.message.auth_url);
}
},
});
} else {
frappe.msgprint(__("Please enter values for App Access Key and App Secret Key"));
if (!frm.events.are_keys_present(frm)) {
frappe.msgprint(__("App Access Key and/or Secret Key are not present."));
return;
}
frappe.call({
method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_dropbox_authorize_url",
freeze: true,
callback: function (r) {
if (!r.exc) {
window.open(r.message.auth_url);
}
},
});
},
take_backup: function (frm) {
if (
frm.doc.enabled &&
((frm.doc.app_access_key && frm.doc.app_secret_key) ||
(frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config))
) {
frm.add_custom_button(__("Take Backup Now"), function (frm) {
if (frm.doc.enabled && (frm.doc.dropbox_refresh_token || frm.doc.dropbox_access_token)) {
frm.add_custom_button(__("Take Backup Now"), function () {
frappe.call({
method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backup",
freeze: true,
});
}).addClass("btn-primary");
});
}
},
});

View file

@ -1,8 +1,10 @@
{
"actions": [],
"creation": "2016-09-21 10:12:57.399174",
"doctype": "DocType",
"document_type": "System",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"send_notifications_to",
@ -14,8 +16,7 @@
"app_access_key",
"app_secret_key",
"allow_dropbox_access",
"dropbox_access_key",
"dropbox_access_secret",
"dropbox_refresh_token",
"dropbox_access_token"
],
"fields": [
@ -82,17 +83,11 @@
"label": "Allow Dropbox Access"
},
{
"fieldname": "dropbox_access_key",
"fieldname": "dropbox_refresh_token",
"fieldtype": "Password",
"hidden": 1,
"label": "Dropbox Access Key",
"read_only": 1
},
{
"fieldname": "dropbox_access_secret",
"fieldtype": "Password",
"hidden": 1,
"label": "Dropbox Access Secret",
"label": "Dropbox Refresh Token",
"no_copy": 1,
"read_only": 1
},
{
@ -104,7 +99,8 @@
],
"in_create": 1,
"issingle": 1,
"modified": "2019-08-22 16:26:44.468391",
"links": [],
"modified": "2023-03-20 14:20:19.180611",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Dropbox Settings",
@ -125,5 +121,6 @@
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# License: MIT. See LICENSE
import json
import os
from urllib.parse import parse_qs, urlparse
@ -16,16 +15,8 @@ from frappe.integrations.offsite_backup_utils import (
send_email,
validate_file_size,
)
from frappe.integrations.utils import make_post_request
from frappe.model.document import Document
from frappe.utils import (
cint,
encode,
get_backups_path,
get_files_path,
get_request_site_address,
get_url,
)
from frappe.utils import cint, encode, get_backups_path, get_files_path, get_request_site_address
from frappe.utils.background_jobs import enqueue
from frappe.utils.backups import new_backup
@ -101,27 +92,9 @@ def take_backup_to_dropbox(retry_count=0, upload_db_backup=True):
def backup_to_dropbox(upload_db_backup=True):
if not frappe.db:
frappe.connect()
# upload database
dropbox_settings = get_dropbox_settings()
if not dropbox_settings["access_token"]:
access_token = generate_oauth2_access_token_from_oauth1_token(dropbox_settings)
if not access_token.get("oauth2_token"):
return (
"Failed backup upload",
"No Access Token exists! Please generate the access token for Dropbox.",
)
dropbox_settings["access_token"] = access_token["oauth2_token"]
set_dropbox_access_token(access_token["oauth2_token"])
dropbox_client = dropbox.Dropbox(
oauth2_access_token=dropbox_settings["access_token"], timeout=None
)
dropbox_client = get_dropbox_client(dropbox_settings)
if upload_db_backup:
if frappe.flags.create_new_backup:
@ -267,24 +240,36 @@ def get_uploaded_files_meta(dropbox_folder, dropbox_client):
# folder not found
if isinstance(e.error, dropbox.files.ListFolderError):
return frappe._dict({"entries": []})
else:
raise
raise
def get_dropbox_client(dropbox_settings):
dropbox_client = dropbox.Dropbox(
oauth2_access_token=dropbox_settings["access_token"],
oauth2_refresh_token=dropbox_settings["refresh_token"],
app_key=dropbox_settings["app_key"],
app_secret=dropbox_settings["app_secret"],
timeout=None,
)
# checking if the access token has expired
dropbox_client.files_list_folder("")
if dropbox_settings["access_token"] != dropbox_client._oauth2_access_token:
set_dropbox_token(dropbox_client._oauth2_access_token)
return dropbox_client
def get_dropbox_settings(redirect_uri=False):
if not frappe.conf.dropbox_broker_site:
frappe.conf.dropbox_broker_site = "https://dropbox.erpnext.com"
# NOTE: access token is kept for legacy dropbox apps
settings = frappe.get_doc("Dropbox Settings")
app_details = {
"app_key": settings.app_access_key or frappe.conf.dropbox_access_key,
"app_secret": settings.get_password(fieldname="app_secret_key", raise_exception=False)
if settings.app_secret_key
else frappe.conf.dropbox_secret_key,
"access_token": settings.get_password("dropbox_access_token", raise_exception=False)
if settings.dropbox_access_token
else "",
"access_key": settings.get_password("dropbox_access_key", raise_exception=False),
"access_secret": settings.get_password("dropbox_access_secret", raise_exception=False),
"refresh_token": settings.get_password("dropbox_refresh_token", raise_exception=False),
"access_token": settings.get_password("dropbox_access_token", raise_exception=False),
"file_backup": settings.file_backup,
"no_of_backups": settings.no_of_backups if settings.limit_no_of_backups else None,
}
@ -294,14 +279,11 @@ def get_dropbox_settings(redirect_uri=False):
{
"redirect_uri": get_request_site_address(True)
+ "/api/method/frappe.integrations.doctype.dropbox_settings.dropbox_settings.dropbox_auth_finish"
if settings.app_secret_key
else frappe.conf.dropbox_broker_site
+ "/api/method/dropbox_erpnext_broker.www.setup_dropbox.generate_dropbox_access_token",
}
)
if not app_details["app_key"] or not app_details["app_secret"]:
raise Exception(_("Please set Dropbox access keys in your site config"))
if not (app_details["app_key"] and app_details["app_secret"]):
raise Exception(_("Please set Dropbox access keys in site config or doctype"))
return app_details
@ -321,28 +303,6 @@ def delete_older_backups(dropbox_client, folder_path, to_keep):
dropbox_client.files_delete(os.path.join(folder_path, f.name))
@frappe.whitelist()
def get_redirect_url():
if not frappe.conf.dropbox_broker_site:
frappe.conf.dropbox_broker_site = "https://dropbox.erpnext.com"
url = "{}/api/method/dropbox_erpnext_broker.www.setup_dropbox.get_authotize_url".format(
frappe.conf.dropbox_broker_site
)
try:
response = make_post_request(url, data={"site": get_url()})
if response.get("message"):
return response["message"]
except Exception:
frappe.log_error()
frappe.throw(
_(
"Something went wrong while generating dropbox access token. Please check error log for more details."
)
)
@frappe.whitelist()
def get_dropbox_authorize_url():
app_details = get_dropbox_settings(redirect_uri=True)
@ -352,6 +312,7 @@ def get_dropbox_authorize_url():
session={},
csrf_token_session_key="dropbox-auth-csrf-token",
consumer_secret=app_details["app_secret"],
token_access_type="offline",
)
auth_url = dropbox_oauth_flow.start()
@ -360,11 +321,20 @@ def get_dropbox_authorize_url():
@frappe.whitelist()
def dropbox_auth_finish(return_access_token=False):
def dropbox_auth_finish():
app_details = get_dropbox_settings(redirect_uri=True)
callback = frappe.form_dict
close = '<p class="text-muted">' + _("Please close this window") + "</p>"
if not callback.state or not callback.code:
frappe.respond_as_web_page(
_("Dropbox Setup"),
_("Illegal Access Token. Please try again") + close,
indicator_color="red",
http_status_code=frappe.AuthenticationError.http_status_code,
)
return
dropbox_oauth_flow = dropbox.DropboxOAuth2Flow(
consumer_key=app_details["app_key"],
redirect_uri=app_details["redirect_uri"],
@ -373,40 +343,20 @@ def dropbox_auth_finish(return_access_token=False):
consumer_secret=app_details["app_secret"],
)
if callback.state or callback.code:
token = dropbox_oauth_flow.finish({"state": callback.state, "code": callback.code})
if return_access_token and token.access_token:
return token.access_token, callback.state
token = dropbox_oauth_flow.finish({"state": callback.state, "code": callback.code})
set_dropbox_token(token.access_token, token.refresh_token)
set_dropbox_access_token(token.access_token)
else:
frappe.respond_as_web_page(
_("Dropbox Setup"),
_("Illegal Access Token. Please try again") + close,
indicator_color="red",
http_status_code=frappe.AuthenticationError.http_status_code,
)
frappe.respond_as_web_page(
_("Dropbox Setup"), _("Dropbox access is approved!") + close, indicator_color="green"
)
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = "/app/dropbox-settings"
def set_dropbox_access_token(access_token):
frappe.db.set_single_value("Dropbox Settings", "dropbox_access_token", access_token)
def set_dropbox_token(access_token, refresh_token=None):
# NOTE: used doc object instead of db.set_value so that password field is set properly
dropbox_settings = frappe.get_single("Dropbox Settings")
dropbox_settings.dropbox_access_token = access_token
if refresh_token:
dropbox_settings.dropbox_refresh_token = refresh_token
dropbox_settings.save()
frappe.db.commit()
def generate_oauth2_access_token_from_oauth1_token(dropbox_settings=None):
if not dropbox_settings.get("access_key") or not dropbox_settings.get("access_secret"):
return {}
url = "https://api.dropboxapi.com/2/auth/token/from_oauth1"
headers = {"Content-Type": "application/json"}
auth = (dropbox_settings["app_key"], dropbox_settings["app_secret"])
data = {
"oauth1_token": dropbox_settings["access_key"],
"oauth1_token_secret": dropbox_settings["access_secret"],
}
return make_post_request(url, auth=auth, headers=headers, data=json.dumps(data))

View file

@ -403,7 +403,7 @@ def insert_event_in_google_calendar(doc, method=None):
event = {"summary": doc.subject, "description": doc.description, "google_calendar_event": 1}
event.update(
format_date_according_to_google_calendar(
doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on)
doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on) if doc.ends_on else None
)
)
@ -484,7 +484,7 @@ def update_event_in_google_calendar(doc, method=None):
)
event.update(
format_date_according_to_google_calendar(
doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on)
doc.all_day, get_datetime(doc.starts_on), get_datetime(doc.ends_on) if doc.ends_on else None
)
)

View file

@ -13,6 +13,13 @@ class IntegrationRequest(Document):
if self.flags._name:
self.name = self.flags._name
def clear_old_logs(days=30):
from frappe.query_builder import Interval
from frappe.query_builder.functions import Now
table = frappe.qb.DocType("Integration Request")
frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))
def update_status(self, params, status):
data = json.loads(self.data)
data.update(params)

View file

@ -0,0 +1,7 @@
frappe.listview_settings["Integration Request"] = {
onload: function (list_view) {
frappe.require("logtypes.bundle.js", () => {
frappe.utils.logtypes.show_log_retention_message(list_view.doctype);
});
},
};

View file

@ -0,0 +1,7 @@
frappe.listview_settings["Webhook Request Log"] = {
onload: function (list_view) {
frappe.require("logtypes.bundle.js", () => {
frappe.utils.logtypes.show_log_retention_message(list_view.doctype);
});
},
};

View file

@ -200,6 +200,10 @@ def get_permitted_fields(
if doctype in core_doctypes_list:
return valid_columns
# DocType has only fields of type Table (Table, Table MultiSelect)
if set(valid_columns).issubset(default_fields):
return valid_columns
if permitted_fields := meta.get_permitted_fieldnames(parenttype=parenttype, user=user):
meta_fields = meta.default_fields.copy()
optional_meta_fields = [x for x in optional_fields if x in valid_columns]

View file

@ -488,6 +488,9 @@ def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None:
if frappe.conf.developer_mode:
for name in frappe.get_all("DocField", filters={"options": old}, pluck="parent"):
if name in (old, new):
continue
doctype = frappe.get_doc("DocType", name)
save = False
for f in doctype.fields:
@ -496,11 +499,11 @@ def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None:
save = True
if save:
doctype.save()
else:
DocField = frappe.qb.DocType("DocField")
frappe.qb.update(DocField).set(DocField.options, new).where(
(DocField.fieldtype == fieldtype) & (DocField.options == old)
).run()
DocField = frappe.qb.DocType("DocField")
frappe.qb.update(DocField).set(DocField.options, new).where(
(DocField.fieldtype == fieldtype) & (DocField.options == old)
).run()
frappe.qb.update(CustomField).set(CustomField.options, new).where(
(CustomField.fieldtype == fieldtype) & (CustomField.options == old)

View file

@ -162,7 +162,6 @@ execute:frappe.delete_doc("DocType", "Footer Item")
execute:frappe.reload_doctype('user')
execute:frappe.reload_doctype('docperm')
frappe.patches.v13_0.replace_field_target_with_open_in_new_tab
frappe.core.doctype.role.patches.v13_set_default_desk_properties
frappe.patches.v13_0.add_switch_theme_to_navbar_settings
frappe.patches.v13_0.update_icons_in_customized_desk_pages
execute:frappe.db.set_default('desktop:home_page', 'space')
@ -184,6 +183,7 @@ frappe.patches.v13_0.encrypt_2fa_secrets
frappe.patches.v13_0.reset_corrupt_defaults
frappe.patches.v13_0.remove_share_for_std_users
execute:frappe.reload_doc('custom', 'doctype', 'custom_field')
frappe.email.doctype.email_queue.patches.drop_search_index_on_message_id
frappe.patches.v14_0.update_workspace2 # 20.09.2021
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021
frappe.patches.v14_0.transform_todo_schema
@ -199,6 +199,7 @@ frappe.patches.v15_0.remove_event_streaming
frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report
[post_model_sync]
frappe.core.doctype.role.patches.v13_set_default_desk_properties
frappe.patches.v14_0.drop_data_import_legacy
frappe.patches.v14_0.copy_mail_data #08.03.21
frappe.patches.v14_0.update_github_endpoints #08-11-2021

View file

@ -77,6 +77,9 @@ def has_permission(
if user == "Administrator":
return True
if ptype == "share" and frappe.get_system_settings("disable_document_sharing"):
return False
if not doc and hasattr(doctype, "doctype"):
# first argument can be doc or doctype
doc = doctype

View file

@ -347,7 +347,7 @@ frappe.ui.form.PrintView = class {
set_default_print_language() {
let print_format = this.get_print_format();
this.lang_code =
print_format.default_print_language || this.frm.doc.language || frappe.boot.lang;
this.frm.doc.language || print_format.default_print_language || frappe.boot.lang;
this.language_selector.val(this.lang_code);
}
@ -657,7 +657,7 @@ frappe.ui.form.PrintView = class {
}
get_letterhead() {
return this.letterhead_selector.val();
return this.letterhead_selector.val() || __("No Letterhead");
}
get_no_preview_html() {

View file

@ -38,7 +38,12 @@ export default class Column {
resize_all_columns() {
// distribute all columns equally
let colspan = cint(12 / this.section.wrapper.find(".form-column").length);
let columns = this.section.wrapper.find(".form-column").length;
let colspan = cint(12 / columns);
if (columns == 5) {
colspan = 20;
}
this.section.wrapper
.find(".form-column")

View file

@ -27,6 +27,7 @@ frappe.ui.form.ControlBarcode = class ControlBarcode extends frappe.ui.form.Cont
let svg = value;
let barcode_value = "";
this.set_empty_description();
if (value && value.startsWith("<svg")) {
barcode_value = $(svg).attr("data-barcode-value");
}
@ -44,10 +45,14 @@ frappe.ui.form.ControlBarcode = class ControlBarcode extends frappe.ui.form.Cont
if (value) {
// Get svg
const svg = this.barcode_area.find("svg")[0];
JsBarcode(svg, value, this.get_options(value));
$(svg).attr("data-barcode-value", value);
$(svg).attr("width", "100%");
return this.barcode_area.html();
try {
JsBarcode(svg, value, this.get_options(value));
$(svg).attr("data-barcode-value", value);
$(svg).attr("width", "100%");
return this.barcode_area.html();
} catch (e) {
this.set_description(`Invalid Barcode: ${String(e)}`);
}
}
}

View file

@ -69,6 +69,12 @@ frappe.ui.form.Dashboard = class FormDashboard {
this.progress_area.body.empty();
this.progress_area.hide();
// clear heatmap
this.heatmap_area.hide();
// clear chart
this.chart_area.hide();
// clear links
this.links_area.body.find(".count, .open-notification").addClass("hidden");
this.links_area.hide();
@ -478,27 +484,25 @@ frappe.ui.form.Dashboard = class FormDashboard {
// heatmap
render_heatmap() {
if (!this.heatmap) {
this.heatmap = new frappe.Chart("#heatmap-" + frappe.model.scrub(this.frm.doctype), {
type: "heatmap",
start: new Date(moment().subtract(1, "year").toDate()),
count_label: "interactions",
discreteDomains: 1,
radius: 3,
data: {},
});
this.heatmap = new frappe.Chart("#heatmap-" + frappe.model.scrub(this.frm.doctype), {
type: "heatmap",
start: new Date(moment().subtract(1, "year").toDate()),
count_label: "interactions",
discreteDomains: 1,
radius: 3,
data: {},
});
// center the heatmap
this.heatmap_area.show();
this.heatmap_area.body.find("svg").css({ margin: "auto" });
// center the heatmap
this.heatmap_area.show();
this.heatmap_area.body.find("svg").css({ margin: "auto" });
// message
let heatmap_message = this.heatmap_area.body.find(".heatmap-message");
if (this.data.heatmap_message) {
heatmap_message.removeClass("hidden").html(this.data.heatmap_message);
} else {
heatmap_message.addClass("hidden");
}
// message
let heatmap_message = this.heatmap_area.body.find(".heatmap-message");
if (this.data.heatmap_message) {
heatmap_message.removeClass("hidden").html(this.data.heatmap_message);
} else {
heatmap_message.addClass("hidden");
}
}
@ -567,7 +571,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
this.chart_area.show();
this.chart_area.body.empty();
$.extend(args, {
type: "line",
type: args.type || "line",
colors: args.colors || ["green"],
truncateLegends: 1,
axisOptions: {

View file

@ -186,7 +186,7 @@ frappe.form.formatters = {
return value;
}
if (value) {
value = frappe.datetime.str_to_user(value);
value = frappe.datetime.str_to_user(value, false, true);
// handle invalid date
if (value === "Invalid date") {
value = null;

View file

@ -40,7 +40,7 @@ export default class GridRow {
render_row = this.render_row();
}
if (!this.render_row) return;
if (!render_row) return;
this.set_data();
this.wrapper.appendTo(this.parent);
@ -762,7 +762,8 @@ export default class GridRow {
show_search_row() {
// show or remove search columns based on grid rows
this.show_search = this.show_search && this.grid?.data?.length >= 20;
this.show_search =
this.show_search && (this.grid?.data?.length >= 20 || this.grid.filter_applied);
!this.show_search && this.wrapper.remove();
return this.show_search;
}

View file

@ -42,8 +42,8 @@ export default class Tab {
hide = true;
}
if (!hide && !this.df.show_dashboard) {
// show only if there is at least one visibe section or control
if (!hide) {
// show only if there is at least one visible section or control
hide = true;
if (
this.wrapper.find(
@ -54,11 +54,6 @@ export default class Tab {
}
}
// hide if dashboard and not saved
if (!hide && this.df.show_dashboard && this.frm.is_new()) {
hide = true;
}
this.toggle(!hide);
}

View file

@ -8,6 +8,8 @@
<span class="text-muted">({%= __("Primary") %})</span>{% } %}
{% if(addr_list[i].is_shipping_address) { %}
<span class="text-muted">({%= __("Shipping") %})</span>{% } %}
{% if(addr_list[i].disabled) { %}
<span class="text-muted">({%= __("Disabled") %})</span>{% } %}
<a href="/app/Form/Address/{%= encodeURIComponent(addr_list[i].name) %}" class="btn btn-default btn-xs pull-right"
style="margin-top:-3px; margin-right: -5px;">

View file

@ -1,49 +1,51 @@
<div class="clearfix"></div>
{% for(var i=0, l=contact_list.length; i<l; i++) { %}
{% for(const contact of contact_list) { %}
<div class="address-box">
<p class="h6 flex align-center">
{%= contact_list[i].first_name %} {%= contact_list[i].last_name %}
{% if(contact_list[i].is_primary_contact) { %}
{%= contact.first_name %} {%= contact.last_name %}
{% if(contact.is_primary_contact) { %}
<span class="text-muted">&nbsp;({%= __("Primary") %})</span>
{% } %}
{% if(contact_list[i].designation){ %}
<span class="text-muted">&ndash; {%= contact_list[i].designation %}</span>
{% if(contact.designation){ %}
<span class="text-muted">&ndash; {%= contact.designation %}</span>
{% } %}
<a href="/app/Form/Contact/{%= encodeURIComponent(contact_list[i].name) %}"
<a href="/app/Form/Contact/{%= encodeURIComponent(contact.name) %}"
class="btn btn-xs btn-default ml-auto">
{%= __("Edit") %}
</a>
</p>
{% if (contact_list[i].phones || contact_list[i].email_ids) { %}
{% if (contact.phone || contact.mobile_no || contact.phone_nos.length > 0) { %}
<p>
{% if(contact_list[i].phone) { %}
{%= __("Phone") %}: {%= contact_list[i].phone %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
{% if(contact.phone) { %}
<a href="tel:{%= frappe.utils.escape_html(contact.phone) %}">{%= frappe.utils.escape_html(contact.phone) %}</a> &#183; <span class="text-muted">{%= __("Primary Phone") %}</span><br>
{% endif %}
{% if(contact_list[i].mobile_no) { %}
{%= __("Mobile No") %}: {%= contact_list[i].mobile_no %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
{% if(contact.mobile_no) { %}
<a href="tel:{%= frappe.utils.escape_html(contact.mobile_no) %}">{%= frappe.utils.escape_html(contact.mobile_no) %}</a> &#183; <span class="text-muted">{%= __("Primary Mobile") %}</span><br>
{% endif %}
{% if(contact_list[i].phone_nos) { %}
{% for(var j=0, k=contact_list[i].phone_nos.length; j<k; j++) { %}
{%= __("Phone") %}: {%= contact_list[i].phone_nos[j].phone %}<br>
{% } %}
{% endif %}
</p>
<p>
{% if(contact_list[i].email_id) { %}
{%= __("Email") %}: {%= contact_list[i].email_id %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
{% endif %}
{% if(contact_list[i].email_ids) { %}
{% for(var j=0, k=contact_list[i].email_ids.length; j<k; j++) { %}
{%= __("Email") %}: {%= contact_list[i].email_ids[j].email_id %}<br>
{% if(contact.phone_nos) { %}
{% for(const phone_no of contact.phone_nos) { %}
<a href="tel:{%= frappe.utils.escape_html(phone_no.phone) %}">{%= frappe.utils.escape_html(phone_no.phone) %}</a><br>
{% } %}
{% endif %}
</p>
{% endif %}
{% if (contact.email_id || contact.email_ids.length > 0) { %}
<p>
{% if (contact_list[i].address) { %}
{%= __("Address") %}: {%= contact_list[i].address %}<br>
{% endif %}
{% if(contact.email_id) { %}
<a href="mailto:{%= frappe.utils.escape_html(contact.email_id) %}">{%= frappe.utils.escape_html(contact.email_id) %}</a> &#183; <span class="text-muted">{%= __("Primary Email") %}</span><br>
{% endif %}
{% if(contact.email_ids) { %}
{% for(const email_id of contact.email_ids) { %}
<a href="mailto:{%= frappe.utils.escape_html(email_id.email_id) %}">{%= frappe.utils.escape_html(email_id.email_id) %}</a><br>
{% } %}
{% endif %}
</p>
{% endif %}
{% if (contact.address) { %}
<p>
{%= contact.address %}
</p>
{% endif %}
</div>
{% } %}
{% if(!contact_list.length) { %}
@ -51,4 +53,4 @@
{% } %}
<p><button class="btn btn-xs btn-default btn-contact">
{{ __("New Contact") }}</button>
</p>
</p>

View file

@ -773,6 +773,8 @@ class FilterArea {
"HTML Editor",
"Data",
"Code",
"Phone",
"JSON",
"Read Only",
].includes(fieldtype)
) {

View file

@ -38,6 +38,8 @@ frappe.views.ListSidebar = class ListSidebar {
this.reload_stats();
});
}
this.add_insights_banner();
}
setup_views() {
@ -239,4 +241,43 @@ frappe.views.ListSidebar = class ListSidebar {
this.sidebar.find(".stat-no-records").remove();
this.get_stats();
}
add_insights_banner() {
try {
if (this.list_view.view != "Report") {
return;
}
if (localStorage.getItem("show_insights_banner") == "false") {
return;
}
if (this.insights_banner) {
this.insights_banner.remove();
}
const message = "Get more insights from your data with Frappe Insights.";
const link = "https://frappe.io/s/insights";
const cta = "Get Frappe Insights";
this.insights_banner = $(`
<div style="position: relative;">
<div class="">
${message}
</div>
<div class="mt-2">
<a href="${link}" target="_blank" style="color: var(--primary-color)">${cta} -> </a>
</div>
<div style="position: absolute; top: 0px; right: 0px; cursor: pointer;" title="Dismiss"
onclick="localStorage.setItem('show_insights_banner', 'false') || this.parentElement.remove()">
<svg class="icon icon-sm" style="">
<use class="" href="#icon-close"></use>
</svg>
</div>
</div>
`).appendTo(this.sidebar);
} catch (error) {
console.error(error);
}
}
};

View file

@ -308,6 +308,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
this.render_header(refresh_header);
this.update_checkbox();
this.update_url_with_filters();
this.setup_realtime_updates();
});
}
@ -1051,6 +1052,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
setup_events() {
this.setup_filterable();
this.setup_list_click();
this.setup_drag_click();
this.setup_tag_event();
this.setup_new_doc_event();
this.setup_check_events();
@ -1227,6 +1229,36 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
});
}
setup_drag_click() {
/*
Click on the check box in the list view and
drag through the rows to select.
Do it again to unselect.
If the first click is on checked checkbox, then it will unselect rows on drag,
else if it is unchecked checkbox, it will select rows on drag.
*/
this.dragClick = false;
this.$result.on("mousedown", ".list-row-checkbox", (e) => {
this.dragClick = true;
this.check = !e.target.checked;
});
$(document).on("mouseup", () => {
this.dragClick = false;
});
this.$result.on("mousemove", ".level.list-row", (e) => {
if (this.dragClick) {
this.check_row_on_drag(e, this.check);
}
});
}
check_row_on_drag(event, check = true) {
$(event.target).find(".list-row-checkbox").prop("checked", check);
this.on_row_checked();
}
setup_action_handler() {
this.$result.on("click", ".btn-action", (e) => {
const $button = $(e.currentTarget);
@ -1329,17 +1361,19 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
setup_realtime_updates() {
this.pending_document_refreshes = [];
if (this.list_view_settings && this.list_view_settings.disable_auto_refresh) {
if (this.list_view_settings?.disable_auto_refresh || this.realtime_events_setup) {
return;
}
frappe.socketio.doctype_subscribe(this.doctype);
frappe.realtime.off("list_update");
frappe.realtime.on("list_update", (data) => {
if (data?.doctype !== this.doctype) {
return;
}
if (!frappe.get_doc(data?.doctype, data?.name)?.__unsaved) {
frappe.model.remove_from_locals(data.doctype, data.name);
// if some bulk operation is happening by selecting list items, don't refresh
if (this.$checks && this.$checks.length) {
return;
}
if (this.avoid_realtime_update()) {
@ -1349,11 +1383,25 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
this.pending_document_refreshes.push(data);
frappe.utils.debounce(this.process_document_refreshes.bind(this), 1000)();
});
this.realtime_events_setup = true;
}
disable_realtime_updates() {
frappe.socketio.doctype_unsubscribe(this.doctype);
this.realtime_events_setup = false;
}
process_document_refreshes() {
if (!this.pending_document_refreshes.length) return;
const route = frappe.get_route() || [];
if (!cur_list || route[0] != "List" || cur_list.doctype != route[1]) {
// wait till user is back on list view before refreshing
this.pending_document_refreshes = [];
this.disable_realtime_updates();
return;
}
const names = this.pending_document_refreshes.map((d) => d.name);
this.pending_document_refreshes = this.pending_document_refreshes.filter(
(d) => names.indexOf(d.name) === -1

View file

@ -48,6 +48,8 @@ $.extend(frappe.model, {
// set title field / name as name
if (meta.autoname && meta.autoname.indexOf("field:") !== -1) {
doc[meta.autoname.substr(6)] = frappe.route_options.name_field;
} else if (meta.autoname && meta.autoname === "prompt") {
doc.__newname = frappe.route_options.name_field;
} else if (meta.title_field) {
doc[meta.title_field] = frappe.route_options.name_field;
}

View file

@ -447,6 +447,12 @@ $.extend(frappe.model, {
},
can_share: function (doctype, frm) {
let disable_sharing = cint(frappe.sys_defaults.disable_document_sharing);
if (disable_sharing && frappe.session.user !== "Administrator") {
return false;
}
if (frm) {
return frm.perm[0].share === 1;
}

View file

@ -529,7 +529,7 @@ frappe.request.report_error = function (xhr, request_opts) {
code_block(JSON.stringify(frappe.boot.versions, null, "\t")),
"### Route",
code_block(frappe.get_route_str()),
"### Trackeback",
"### Traceback",
code_block(exc),
"### Request Data",
code_block(JSON.stringify(request_opts, null, "\t")),

View file

@ -132,6 +132,9 @@ frappe.socketio = {
doctype_subscribe: function (doctype) {
frappe.socketio.socket.emit("doctype_subscribe", doctype);
},
doctype_unsubscribe: function (doctype) {
frappe.socketio.socket.emit("doctype_unsubscribe", doctype);
},
doc_subscribe: function (doctype, docname) {
if (frappe.flags.doc_subscribe) {
console.log("throttled");

View file

@ -508,6 +508,7 @@ frappe.ui.filter_utils = {
"HTML Editor",
"Tag",
"Phone",
"JSON",
"Comments",
"Barcode",
"Dynamic Link",

View file

@ -18,9 +18,9 @@ frappe.ui.misc.about = function () {
<p><i class='fa fa-facebook fa-fw'></i>
Facebook: <a href='https://facebook.com/erpnext' target='_blank'>https://facebook.com/erpnext</a></p>
<p><i class='fa fa-twitter fa-fw'></i>
Twitter: <a href='https://twitter.com/erpnext' target='_blank'>https://twitter.com/erpnext</a></p>
Twitter: <a href='https://twitter.com/frappetech' target='_blank'>https://twitter.com/frappetech</a></p>
<p><i class='fa fa-youtube fa-fw'></i>
YouTube: <a href='https://www.youtube.com/@erpnextofficial' target='_blank'>https://www.youtube.com/@erpnextofficial</a></p>
YouTube: <a href='https://www.youtube.com/@frappetech' target='_blank'>https://www.youtube.com/@frappetech</a></p>
<hr>
<h4>${__("Installed Apps")}</h4>
<div id='about-app-versions'>${__("Loading versions...")}</div>

View file

@ -129,10 +129,10 @@ frappe.ui.toolbar.Toolbar = class {
let awesome_bar = new frappe.search.AwesomeBar();
awesome_bar.setup("#navbar-search");
// TODO: Remove this in v14
frappe.search.utils.make_function_searchable(function () {
frappe.set_route("List", "Client Script");
}, __("Custom Script List"));
frappe.search.utils.make_function_searchable(
frappe.utils.generate_tracking_url,
__("Generate Tracking URL")
);
}
}

View file

@ -175,11 +175,11 @@ function get_number_format_info(format) {
function _round(num, precision, rounding_method) {
rounding_method =
rounding_method || frappe.boot.sysdefaults.rounding_method || "Round Half Even";
rounding_method || frappe.boot.sysdefaults.rounding_method || "Banker's Rounding (legacy)";
let is_negative = num < 0 ? true : false;
if (rounding_method == "Round Half Even") {
if (rounding_method == "Banker's Rounding (legacy)") {
var d = cint(precision);
var m = Math.pow(10, d);
var n = +(d ? Math.abs(num) * m : Math.abs(num)).toFixed(8); // Avoid rounding errors
@ -188,7 +188,27 @@ function _round(num, precision, rounding_method) {
var r = !precision && f == 0.5 ? (i % 2 == 0 ? i : i + 1) : Math.round(n);
r = d ? r / m : r;
return is_negative ? -r : r;
} else if (rounding_method == "Rounding Half Away From Zero") {
} else if (rounding_method == "Banker's Rounding") {
if (num == 0) return 0.0;
precision = cint(precision);
let multiplier = Math.pow(10, precision);
num = Math.abs(num) * multiplier;
let floor_num = Math.floor(num);
let decimal_part = num - floor_num;
// For explanation of this method read python flt implementation notes.
let epsilon = 2.0 ** (Math.log2(Math.abs(num)) - 52.0);
if (Math.abs(decimal_part - 0.5) < epsilon) {
num = floor_num % 2 == 0 ? floor_num : floor_num + 1;
} else {
num = Math.round(num);
}
num = num / multiplier;
return is_negative ? -num : num;
} else if (rounding_method == "Commercial Rounding") {
if (num == 0) return 0.0;
let digits = cint(precision);

View file

@ -1610,4 +1610,65 @@ Object.assign(frappe.utils, {
});
},
},
generate_tracking_url() {
frappe.prompt(
[
{
fieldname: "url",
label: __("Web Page URL"),
fieldtype: "Data",
options: "URL",
reqd: 1,
default: localStorage.getItem("tracker_url:url"),
},
{
fieldname: "source",
label: __("Source"),
fieldtype: "Data",
default: localStorage.getItem("tracker_url:source"),
},
{
fieldname: "campaign",
label: __("Campaign"),
fieldtype: "Link",
ignore_link_validation: 1,
options: "Marketing Campaign",
default: localStorage.getItem("tracker_url:campaign"),
},
{
fieldname: "medium",
label: __("Medium"),
fieldtype: "Data",
default: localStorage.getItem("tracker_url:medium"),
},
],
function (data) {
let url = data.url;
localStorage.setItem("tracker_url:url", data.url);
if (data.source) {
url += "?source=" + data.source;
localStorage.setItem("tracker_url:source", data.source);
}
if (data.campaign) {
url += "&campaign=" + data.campaign;
localStorage.setItem("tracker_url:campaign", data.campaign);
}
if (data.medium) {
url += "&medium=" + data.medium.toLowerCase();
localStorage.setItem("tracker_url:medium", data.medium);
}
frappe.utils.copy_to_clipboard(url);
frappe.msgprint(
__("Tracking URL generated and copied to clipboard") +
": <br>" +
`<a href="${url}">${url.bold()}</a>`,
__("Here's your tracking URL")
);
},
__("Generate Tracking URL")
);
},
});

View file

@ -130,11 +130,6 @@ frappe.views.CommunicationComposer = class {
fieldtype: "Select",
fieldname: "select_print_format",
},
{
label: __("Select Languages"),
fieldtype: "Select",
fieldname: "language_sel",
},
{ fieldtype: "Column Break" },
{
label: __("Select Attachments"),
@ -183,7 +178,6 @@ frappe.views.CommunicationComposer = class {
prepare() {
this.setup_multiselect_queries();
this.setup_subject_and_recipients();
this.setup_print_language();
this.setup_print();
this.setup_attach();
this.setup_email();
@ -295,7 +289,6 @@ frappe.views.CommunicationComposer = class {
args: {
template_name: email_template,
doc: me.doc,
_lang: me.dialog.get_value("language_sel"),
},
callback(r) {
prepend_reply(r.message);
@ -403,29 +396,6 @@ frappe.views.CommunicationComposer = class {
}
}
setup_print_language() {
const fields = this.dialog.fields_dict;
//Load default print language from doctype
this.lang_code =
this.doc.language ||
this.get_print_format().default_print_language ||
frappe.boot.lang;
//On selection of language retrieve language code
const me = this;
$(fields.language_sel.input).change(function () {
me.lang_code = this.value;
});
// Load all languages in the select field language_sel
$(fields.language_sel.input).empty().add_options(frappe.get_languages());
if (this.lang_code) {
$(fields.language_sel.input).val(this.lang_code);
}
}
setup_print() {
// print formats
const fields = this.dialog.fields_dict;
@ -676,7 +646,6 @@ frappe.views.CommunicationComposer = class {
sender_full_name: form_values.sender ? frappe.user.full_name() : undefined,
email_template: form_values.email_template,
attachments: selected_attachments,
_lang: me.lang_code,
read_receipt: form_values.send_read_receipt,
print_letterhead: me.is_print_letterhead_checked(),
},

View file

@ -157,7 +157,10 @@ frappe.views.Workspace = class Workspace {
sidebar_section.addClass("hidden");
}
if (sidebar_section.find("> [item-is-hidden='0']").length == 0) {
if (
sidebar_section.find("sidebar-item-container").length &&
sidebar_section.find("> [item-is-hidden='0']").length == 0
) {
sidebar_section.addClass("hidden show-in-edit-mode");
}
}
@ -1445,15 +1448,15 @@ frappe.views.Workspace = class Workspace {
}
create_sidebar_skeleton() {
if (this.sidebar.find(".workspace-sidebar-skeleton").length) return;
if ($(".workspace-sidebar-skeleton").length) return;
this.sidebar.prepend(frappe.render_template("workspace_sidebar_loading_skeleton"));
this.sidebar.find(".standard-sidebar-section").addClass("hidden");
$(frappe.render_template("workspace_sidebar_loading_skeleton")).insertBefore(this.sidebar);
this.sidebar.addClass("hidden");
}
remove_sidebar_skeleton() {
this.sidebar.find(".standard-sidebar-section").removeClass("hidden");
this.sidebar.find(".workspace-sidebar-skeleton").remove();
this.sidebar.removeClass("hidden");
$(".workspace-sidebar-skeleton").remove();
}
register_awesomebar_shortcut() {

View file

@ -382,8 +382,8 @@ class ShortcutDialog extends WidgetDialog {
reqd: 1,
options: "type",
onchange: () => {
if (this.dialog.get_value("type") == "DocType") {
let doctype = this.dialog.get_value("link_to");
const doctype = this.dialog.get_value("link_to");
if (doctype && this.dialog.get_value("type") == "DocType") {
frappe.model.with_doctype(doctype, () => {
let meta = frappe.get_meta(doctype);

View file

@ -208,7 +208,6 @@ body {
// Overrides for each widgets
&.dashboard-widget-box {
min-height: 240px;
padding: var(--padding-md) var(--padding-lg);
.filter-chart {
background-color: var(--control-bg);
@ -238,13 +237,16 @@ body {
}
.widget-head {
padding: var(--padding-sm);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 6px;
}
.widget-body {
padding-top: 7px;
}
.widget-control {
display: flex;
align-items: center;
@ -560,7 +562,7 @@ body {
}
&.links-widget-box {
padding: 18px 12px;
padding: 12px 7px;
.link-item {
display: flex;

View file

@ -409,6 +409,21 @@
}
}
// handle 5 columns in form
.form-column.col-sm-20 {
position: relative;
width: 100%;
padding-right: 15px;
padding-left: 15px;
}
@media (min-width: map-get($grid-breakpoints, "sm")) {
.form-column.col-sm-20 {
flex: 0 0 20%;
max-width: 20%;
}
}
// above mobile
@media (min-width: map-get($grid-breakpoints, "md")) {
.layout-main .form-column.col-sm-12 > form > .input-max-width {

View file

@ -12,6 +12,7 @@ from frappe.utils import get_table_name
class Base:
terms = terms
desc = Order.desc
asc = Order.asc
Schema = Schema
Table = Table

View file

@ -105,10 +105,8 @@ def get_redis_server():
@frappe.whitelist(allow_guest=True)
def can_subscribe_doc(doctype: str, docname: str) -> bool:
from frappe.exceptions import PermissionError
from frappe.sessions import Session
session = Session(None, resume=True).get_session_data()
if not frappe.has_permission(user=session.user, doctype=doctype, doc=docname, ptype="read"):
if not frappe.has_permission(doctype=doctype, doc=docname, ptype="read"):
raise PermissionError()
return True
@ -118,7 +116,7 @@ def can_subscribe_doc(doctype: str, docname: str) -> bool:
def can_subscribe_doctype(doctype: str) -> bool:
from frappe.exceptions import PermissionError
if not frappe.has_permission(user=frappe.session.user, doctype=doctype, ptype="read"):
if not frappe.has_permission(doctype=doctype, ptype="read"):
raise PermissionError()
return True
@ -126,13 +124,9 @@ def can_subscribe_doctype(doctype: str) -> bool:
@frappe.whitelist(allow_guest=True)
def get_user_info():
from frappe.sessions import Session
session = Session(None, resume=True).get_session_data()
return {
"user": session.user,
"user_type": session.user_type,
"user": frappe.session.user,
"user_type": frappe.session.user_type,
}

View file

@ -23,23 +23,22 @@ frappe.ready(function() {
}
$("#contact-alert").toggle(false);
frappe.send_message({
subject: $('[name="subject"]').val(),
sender: email,
message: message,
frappe.call({
type: "POST",
method: "frappe.www.contact.send_message",
args: {
subject: $('[name="subject"]').val(),
sender: email,
message: message,
},
callback: function(r) {
if(r.message==="okay") {
if (!r.exc) {
frappe.msgprint('{{ _("Thank you for your message") }}');
} else {
frappe.msgprint('{{ _("There were errors") }}');
console.log(r.exc);
}
$(':input').val('');
}
}, this);
return false;
},
});
});
});
var msgprint = function(txt) {

View file

@ -56,9 +56,9 @@
<td class="{{ get_align_class(tdf) }}" {{ fieldmeta(df) }}>
{% if doc.child_print_templates %}
{%- set child_templates = doc.child_print_templates.get(df.fieldname) -%}
<div class="value">{{ print_value(tdf, d, doc, visible_columns, child_templates) }}</div></td>
<div class="value">{{ _(print_value(tdf, d, doc, visible_columns, child_templates)) }}</div></td>
{% else %}
<div class="value">{{ print_value(tdf, d, doc, visible_columns) }}</div></td>
<div class="value">{{ _(print_value(tdf, d, doc, visible_columns)) }}</div></td>
{% endif %}
{% endfor %}
</tr>

View file

@ -8,6 +8,7 @@ import unittest
from io import StringIO
from unittest.mock import patch
import git
import yaml
import frappe
@ -134,6 +135,9 @@ class TestBoilerPlate(unittest.TestCase):
self.check_parsable_python_files(new_app_dir)
app_repo = git.Repo(new_app_dir)
self.assertEqual(app_repo.active_branch.name, "develop")
def test_create_app_without_git_init(self):
app_name = "test_app_no_git"

View file

@ -4,7 +4,7 @@ from unittest.mock import MagicMock
import frappe
from frappe.tests.test_api import FrappeAPITestCase
from frappe.tests.utils import FrappeTestCase
from frappe.utils.caching import request_cache, site_cache
from frappe.utils.caching import redis_cache, request_cache, site_cache
CACHE_TTL = 4
external_service = MagicMock(return_value=30)
@ -82,13 +82,84 @@ class TestSiteCache(FrappeAPITestCase):
api_with_ttl = f"{module}.ping_with_ttl"
api_without_ttl = f"{module}.ping"
start = time.monotonic()
for _ in range(5):
self.get(f"/api/method/{api_with_ttl}")
self.get(f"/api/method/{api_without_ttl}")
end = time.monotonic()
self.assertEqual(register_with_external_service.call_count, 2)
time.sleep(CACHE_TTL - (end - start))
time.sleep(CACHE_TTL)
self.get(f"/api/method/{api_with_ttl}")
self.assertEqual(register_with_external_service.call_count, 3)
class TestRedisCache(FrappeAPITestCase):
def test_redis_cache(self):
function_call_count = 0
@redis_cache(ttl=CACHE_TTL)
def calculate_area(radius: float) -> float:
nonlocal function_call_count
function_call_count += 1
return 3.14 * radius**2
self.assertEqual(calculate_area(10), 314)
self.assertEqual(function_call_count, 1)
self.assertEqual(calculate_area(10), 314)
self.assertEqual(function_call_count, 1)
time.sleep(CACHE_TTL)
self.assertEqual(calculate_area(10), 314)
self.assertEqual(function_call_count, 2)
calculate_area.clear_cache()
self.assertEqual(calculate_area(10), 314)
self.assertEqual(function_call_count, 3)
calculate_area.clear_cache()
def test_redis_cache_without_params(self):
function_call_count = 0
@redis_cache
def calculate_area(radius: float) -> float:
nonlocal function_call_count
function_call_count += 1
return 3.14 * radius**2
calculate_area.clear_cache()
self.assertEqual(calculate_area(10), 314)
self.assertEqual(function_call_count, 1)
calculate_area.clear_cache()
self.assertEqual(calculate_area(10), 314)
self.assertEqual(function_call_count, 2)
calculate_area.clear_cache()
def test_redis_cache_diff_args(self):
function_call_count = 0
@redis_cache(ttl=CACHE_TTL)
def calculate_area(radius: float) -> float:
nonlocal function_call_count
function_call_count += 1
return 3.14 * radius**2
self.assertEqual(calculate_area(10), 314)
self.assertEqual(function_call_count, 1)
self.assertEqual(calculate_area(100), 31400)
self.assertEqual(function_call_count, 2)
self.assertEqual(calculate_area(5), 25 * 3.14)
self.assertEqual(function_call_count, 3)
calculate_area(10)
# from cache now
self.assertEqual(function_call_count, 3)
calculate_area(radius=10)
# args, kwargs are treated differently
self.assertEqual(function_call_count, 4)
calculate_area(radius=10)
# kwargs should hit cache too
self.assertEqual(function_call_count, 4)

View file

@ -8,6 +8,7 @@ from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.query_builder import Field
from frappe.query_builder.functions import Max
from frappe.tests.utils import FrappeTestCase
from frappe.utils import random_string
from frappe.utils.nestedset import (
NestedSetChildExistsError,
NestedSetInvalidMergeError,
@ -213,6 +214,12 @@ class TestNestedSet(FrappeTestCase):
remove_subtree("Test Tree DocType", "Parent 2")
self.test_basic_tree()
def test_rename_nestedset(self):
doctype = new_doctype(is_tree=True).insert()
# Rename doctype
frappe.rename_doc("DocType", doctype.name, "Test " + random_string(10), force=True)
def test_merge_groups(self):
global records
el = {"some_fieldname": "Parent 2", "parent_test_tree_doctype": "Root Node", "is_group": 1}

View file

@ -9,14 +9,9 @@ from unittest.mock import patch
import frappe
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.exceptions import DoesNotExistError, ValidationError
from frappe.exceptions import DoesNotExistError
from frappe.model.base_document import get_controller
from frappe.model.rename_doc import (
bulk_rename,
get_fetch_fields,
update_document_title,
update_linked_doctypes,
)
from frappe.model.rename_doc import bulk_rename, update_document_title
from frappe.modules.utils import get_doc_path
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_to_date, now
@ -255,14 +250,16 @@ class TestRenameDoc(FrappeTestCase):
)
def test_deprecated_utils(self):
from frappe.model.rename_doc import get_fetch_fields, update_linked_doctypes
stdout = StringIO()
with redirect_stdout(stdout), patch_db(["set_value"]):
get_fetch_fields("User", "ToDo", ["Activity Log"])
self.assertTrue("Function frappe.model.rename_doc.get_fetch_fields" in stdout.getvalue())
self.assertIn("Function frappe.model.rename_doc.get_fetch_fields", stdout.getvalue())
update_linked_doctypes("User", "ToDo", "str", "str")
self.assertTrue("Function frappe.model.rename_doc.update_linked_doctypes" in stdout.getvalue())
self.assertIn("Function frappe.model.rename_doc.update_linked_doctypes", stdout.getvalue())
def test_doc_rename_method(self):
name = choice(self.available_documents)

View file

@ -63,6 +63,7 @@ from frappe.utils.data import (
now_datetime,
nowtime,
pretty_date,
rounded,
to_timedelta,
validate_python_code,
)
@ -1007,7 +1008,7 @@ class TestTBSanitization(FrappeTestCase):
class TestRounding(FrappeTestCase):
@change_settings("System Settings", {"rounding_method": "Rounding Half Away From Zero"})
@change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
def test_normal_rounding(self):
self.assertEqual(flt("what"), 0)
@ -1041,7 +1042,7 @@ class TestRounding(FrappeTestCase):
self.assertEqual(flt(-0.15, 1), -0.2)
def test_normal_rounding_as_argument(self):
rounding_method = "Rounding Half Away From Zero"
rounding_method = "Commercial Rounding"
self.assertEqual(flt("0.5", 0, rounding_method=rounding_method), 1)
self.assertEqual(flt("0.3", rounding_method=rounding_method), 0.3)
@ -1073,9 +1074,84 @@ class TestRounding(FrappeTestCase):
self.assertEqual(flt(-1.25, 1, rounding_method=rounding_method), -1.3)
self.assertEqual(flt(-0.15, 1, rounding_method=rounding_method), -0.2)
@change_settings("System Settings", {"rounding_method": "Rounding Half Away From Zero"})
# Nearest number and not even (the default behaviour)
self.assertEqual(flt(0.5, 0, rounding_method=rounding_method), 1)
self.assertEqual(flt(1.5, 0, rounding_method=rounding_method), 2)
self.assertEqual(flt(2.5, 0, rounding_method=rounding_method), 3)
self.assertEqual(flt(3.5, 0, rounding_method=rounding_method), 4)
self.assertEqual(flt(0.05, 1, rounding_method=rounding_method), 0.1)
self.assertEqual(flt(1.15, 1, rounding_method=rounding_method), 1.2)
self.assertEqual(flt(2.25, 1, rounding_method=rounding_method), 2.3)
self.assertEqual(flt(3.35, 1, rounding_method=rounding_method), 3.4)
@change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
@given(st.decimals(min_value=-1e8, max_value=1e8), st.integers(min_value=-2, max_value=4))
def test_normal_rounding_property(self, number, precision):
with localcontext() as ctx:
ctx.rounding = ROUND_HALF_UP
self.assertEqual(Decimal(str(flt(float(number), precision))), round(number, precision))
def test_bankers_rounding(self):
rounding_method = "Banker's Rounding"
self.assertEqual(rounded(0, 0, rounding_method=rounding_method), 0)
self.assertEqual(flt("0.5", 0, rounding_method=rounding_method), 0)
self.assertEqual(flt("0.3", rounding_method=rounding_method), 0.3)
self.assertEqual(flt("1.5", 0, rounding_method=rounding_method), 2)
# positive rounding to integers
self.assertEqual(flt(0.4, 0, rounding_method=rounding_method), 0)
self.assertEqual(flt(0.5, 0, rounding_method=rounding_method), 0)
self.assertEqual(flt(1.455, 0, rounding_method=rounding_method), 1)
self.assertEqual(flt(1.5, 0, rounding_method=rounding_method), 2)
# negative rounding to integers
self.assertEqual(flt(-0.5, 0, rounding_method=rounding_method), 0)
self.assertEqual(flt(-1.5, 0, rounding_method=rounding_method), -2)
# negative precision i.e. round to nearest 10th
self.assertEqual(flt(123, -1, rounding_method=rounding_method), 120)
self.assertEqual(flt(125, -1, rounding_method=rounding_method), 120)
self.assertEqual(flt(134.45, -1, rounding_method=rounding_method), 130)
self.assertEqual(flt(135, -1, rounding_method=rounding_method), 140)
# positive multiple digit rounding
self.assertEqual(flt(1.25, 1, rounding_method=rounding_method), 1.2)
self.assertEqual(flt(0.15, 1, rounding_method=rounding_method), 0.2)
self.assertEqual(flt(2.675, 2, rounding_method=rounding_method), 2.68)
self.assertEqual(flt(-2.675, 2, rounding_method=rounding_method), -2.68)
# negative multiple digit rounding
self.assertEqual(flt(-1.25, 1, rounding_method=rounding_method), -1.2)
self.assertEqual(flt(-0.15, 1, rounding_method=rounding_method), -0.2)
# Nearest number and not even (the default behaviour)
self.assertEqual(flt(0.5, 0, rounding_method=rounding_method), 0)
self.assertEqual(flt(1.5, 0, rounding_method=rounding_method), 2)
self.assertEqual(flt(2.5, 0, rounding_method=rounding_method), 2)
self.assertEqual(flt(3.5, 0, rounding_method=rounding_method), 4)
self.assertEqual(flt(0.05, 1, rounding_method=rounding_method), 0.0)
self.assertEqual(flt(1.15, 1, rounding_method=rounding_method), 1.2)
self.assertEqual(flt(2.25, 1, rounding_method=rounding_method), 2.2)
self.assertEqual(flt(3.35, 1, rounding_method=rounding_method), 3.4)
self.assertEqual(flt(-0.5, 0, rounding_method=rounding_method), 0)
self.assertEqual(flt(-1.5, 0, rounding_method=rounding_method), -2)
self.assertEqual(flt(-2.5, 0, rounding_method=rounding_method), -2)
self.assertEqual(flt(-3.5, 0, rounding_method=rounding_method), -4)
self.assertEqual(flt(-0.05, 1, rounding_method=rounding_method), 0.0)
self.assertEqual(flt(-1.15, 1, rounding_method=rounding_method), -1.2)
self.assertEqual(flt(-2.25, 1, rounding_method=rounding_method), -2.2)
self.assertEqual(flt(-3.35, 1, rounding_method=rounding_method), -3.4)
@change_settings("System Settings", {"rounding_method": "Banker's Rounding"})
@given(st.decimals(min_value=-1e8, max_value=1e8), st.integers(min_value=-2, max_value=4))
def test_bankers_rounding_property(self, number, precision):
self.assertEqual(Decimal(str(flt(float(number), precision))), round(number, precision))
def test_default_rounding(self):
self.assertEqual(frappe.get_system_settings("rounding_method"), "Banker's Rounding")

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