Merge branch 'develop' into fix-note-2
This commit is contained in:
commit
417f2bbd97
127 changed files with 1527 additions and 619 deletions
97
.github/helper/documentation.py
vendored
97
.github/helper/documentation.py
vendored
|
|
@ -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)
|
||||
|
|
|
|||
3
.github/workflows/linters.yml
vendored
3
.github/workflows/linters.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
48
cypress/integration/list_view_drag_select.js
Normal file
48
cypress/integration/list_view_drag_select.js
Normal 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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ user_cache_keys = (
|
|||
"has_role:Page",
|
||||
"has_role:Report",
|
||||
"desk_sidebar_items",
|
||||
"contacts",
|
||||
)
|
||||
|
||||
doctype_cache_keys = (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 "",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
7
frappe/core/doctype/access_log/access_log_list.js
Normal file
7
frappe/core/doctype/access_log/access_log_list.js
Normal 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);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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"]},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ DEFAULT_LOGTYPES_RETENTION = {
|
|||
"Submission Queue": 30,
|
||||
"Prepared Report": 30,
|
||||
"Webhook Request Log": 30,
|
||||
"Integration Request": 90,
|
||||
"Reminder": 30,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
0
frappe/email/doctype/email_queue/patches/__init__.py
Normal file
0
frappe/email/doctype/email_queue/patches/__init__.py
Normal 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}`")
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
</p>
|
||||
</div>
|
||||
<div itemprop="articleBody" class="longform blog-text">
|
||||
{{ doc.get_message() }}
|
||||
{{ doc.get_message(medium="web_page") }}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;">
|
||||
|
|
|
|||
|
|
@ -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"> ({%= __("Primary") %})</span>
|
||||
{% } %}
|
||||
{% if(contact_list[i].designation){ %}
|
||||
<span class="text-muted">– {%= contact_list[i].designation %}</span>
|
||||
{% if(contact.designation){ %}
|
||||
<span class="text-muted">– {%= 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> · <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> · <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> · <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>
|
||||
|
|
|
|||
|
|
@ -773,6 +773,8 @@ class FilterArea {
|
|||
"HTML Editor",
|
||||
"Data",
|
||||
"Code",
|
||||
"Phone",
|
||||
"JSON",
|
||||
"Read Only",
|
||||
].includes(fieldtype)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -508,6 +508,7 @@ frappe.ui.filter_utils = {
|
|||
"HTML Editor",
|
||||
"Tag",
|
||||
"Phone",
|
||||
"JSON",
|
||||
"Comments",
|
||||
"Barcode",
|
||||
"Dynamic Link",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue