')
+ email_body = quill_parser.parse(self.content)
+
+ if not email_body:
+ return
+
+ email_body = email_body[0]
+
+ user_email_signature = frappe.db.get_value(
+ "User",
+ self.sender,
+ "email_signature",
+ ) if self.sender else None
+
+ signature = user_email_signature or frappe.db.get_value(
+ "Email Account",
+ {"default_outgoing": 1, "add_signature": 1},
+ "signature",
+ )
+
+ if not signature:
+ return
+
+ _signature = quill_parser.parse(signature)[0] if "ql-editor" in signature else None
+
+ if (_signature or signature) not in self.content:
+ self.content = f'{self.content}
{signature}'
+
+ def before_save(self):
+ if not self.flags.skip_add_signature:
+ self.set_signature_in_email_content()
+
def on_update(self):
# add to _comment property of the doctype, so it shows up in
# comments count for the list view
diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py
index 46ef7bf5d2..b51749ccb7 100755
--- a/frappe/core/doctype/communication/email.py
+++ b/frappe/core/doctype/communication/email.py
@@ -22,12 +22,30 @@ OUTGOING_EMAIL_ACCOUNT_MISSING = _("""
@frappe.whitelist()
-def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
- sender=None, sender_full_name=None, recipients=None, communication_medium="Email", send_email=False,
- print_html=None, print_format=None, attachments='[]', send_me_a_copy=False, cc=None, bcc=None,
- flags=None, read_receipt=None, print_letterhead=True, email_template=None, communication_type=None,
- ignore_permissions=False) -> Dict[str, str]:
- """Make a new communication.
+def make(
+ doctype=None,
+ name=None,
+ content=None,
+ subject=None,
+ sent_or_received="Sent",
+ sender=None,
+ sender_full_name=None,
+ recipients=None,
+ communication_medium="Email",
+ send_email=False,
+ print_html=None,
+ print_format=None,
+ attachments="[]",
+ send_me_a_copy=False,
+ cc=None,
+ bcc=None,
+ read_receipt=None,
+ print_letterhead=True,
+ email_template=None,
+ communication_type=None,
+ **kwargs,
+) -> Dict[str, str]:
+ """Make a new communication. Checks for email permissions for specified Document.
:param doctype: Reference DocType.
:param name: Reference Document name.
@@ -44,17 +62,71 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
:param send_me_a_copy: Send a copy to the sender (default **False**).
:param email_template: Template which is used to compose mail .
"""
- is_error_report = (doctype=="User" and name==frappe.session.user and subject=="Error Report")
- send_me_a_copy = cint(send_me_a_copy)
+ if kwargs:
+ from frappe.utils.commands import warn
+ warn(
+ f"Options {kwargs} used in frappe.core.doctype.communication.email.make "
+ "are deprecated or unsupported",
+ category=DeprecationWarning
+ )
- if not ignore_permissions:
- if doctype and name and not is_error_report and not frappe.has_permission(doctype, "email", name) and not (flags or {}).get('ignore_doctype_permissions'):
- raise frappe.PermissionError("You are not allowed to send emails related to: {doctype} {name}".format(
- doctype=doctype, name=name))
+ if doctype and name and not frappe.has_permission(doctype=doctype, ptype="email", doc=name):
+ raise frappe.PermissionError(
+ f"You are not allowed to send emails related to: {doctype} {name}"
+ )
- if not sender:
- sender = get_formatted_email(frappe.session.user)
+ return _make(
+ doctype=doctype,
+ name=name,
+ content=content,
+ subject=subject,
+ sent_or_received=sent_or_received,
+ sender=sender,
+ sender_full_name=sender_full_name,
+ recipients=recipients,
+ communication_medium=communication_medium,
+ send_email=send_email,
+ print_html=print_html,
+ print_format=print_format,
+ attachments=attachments,
+ send_me_a_copy=cint(send_me_a_copy),
+ cc=cc,
+ bcc=bcc,
+ read_receipt=read_receipt,
+ print_letterhead=print_letterhead,
+ email_template=email_template,
+ communication_type=communication_type,
+ add_signature=False,
+ )
+
+def _make(
+ doctype=None,
+ name=None,
+ content=None,
+ subject=None,
+ sent_or_received="Sent",
+ sender=None,
+ sender_full_name=None,
+ recipients=None,
+ communication_medium="Email",
+ send_email=False,
+ print_html=None,
+ print_format=None,
+ attachments="[]",
+ send_me_a_copy=False,
+ cc=None,
+ bcc=None,
+ read_receipt=None,
+ print_letterhead=True,
+ email_template=None,
+ communication_type=None,
+ add_signature=True,
+) -> Dict[str, str]:
+ """Internal method to make a new communication that ignores Permission checks.
+ """
+
+ sender = sender or get_formatted_email(frappe.session.user)
recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients
cc = list_to_str(cc) if isinstance(cc, list) else cc
bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc
@@ -77,7 +149,9 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
"read_receipt":read_receipt,
"has_attachment": 1 if attachments else 0,
"communication_type": communication_type,
- }).insert(ignore_permissions=True)
+ })
+ comm.flags.skip_add_signature = not add_signature
+ comm.insert(ignore_permissions=True)
# if not committed, delayed task doesn't find the communication
if attachments:
@@ -87,17 +161,21 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
if cint(send_email):
if not comm.get_outgoing_email_account():
- frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError)
+ frappe.throw(
+ msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError
+ )
- comm.send_email(print_html=print_html, print_format=print_format,
- send_me_a_copy=send_me_a_copy, print_letterhead=print_letterhead)
+ comm.send_email(
+ print_html=print_html,
+ print_format=print_format,
+ send_me_a_copy=send_me_a_copy,
+ print_letterhead=print_letterhead,
+ )
emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy)
- return {
- "name": comm.name,
- "emails_not_sent_to": ", ".join(emails_not_sent_to)
- }
+ return {"name": comm.name, "emails_not_sent_to": ", ".join(emails_not_sent_to)}
+
def validate_email(doc: "Communication") -> None:
"""Validate Email Addresses of Recipients and CC"""
diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py
index d933c2f494..8012d8facf 100644
--- a/frappe/core/doctype/communication/test_communication.py
+++ b/frappe/core/doctype/communication/test_communication.py
@@ -4,8 +4,8 @@ import unittest
from urllib.parse import quote
import frappe
-from frappe.email.doctype.email_queue.email_queue import EmailQueue
from frappe.core.doctype.communication.communication import get_emails
+from frappe.email.doctype.email_queue.email_queue import EmailQueue
test_records = frappe.get_test_records('Communication')
@@ -202,7 +202,7 @@ class TestCommunication(unittest.TestCase):
self.assertIn(("Note", note.name), doc_links)
- def parse_emails(self):
+ def test_parse_emails(self):
emails = get_emails(
[
'comm_recipient+DocType+DocName@example.com',
diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py
index 79570d5048..9f1492af19 100644
--- a/frappe/core/doctype/data_export/exporter.py
+++ b/frappe/core/doctype/data_export/exporter.py
@@ -324,7 +324,7 @@ class DataExporter:
d = doc.copy()
meta = frappe.get_meta(dt)
if self.all_doctypes:
- d.name = '"'+ d.name+'"'
+ d.name = f'"{d.name}"'
if len(rows) < rowidx + 1:
rows.append([""] * (len(self.columns) + 1))
diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js
index f250a6a109..88cc5577a6 100644
--- a/frappe/core/doctype/doctype/doctype.js
+++ b/frappe/core/doctype/doctype/doctype.js
@@ -61,6 +61,13 @@ frappe.ui.form.on('DocType', {
frm.events.set_naming_rule_description(frm);
},
+ istable: (frm) => {
+ if (frm.doc.istable && frm.is_new()) {
+ frm.set_value('autoname', 'autoincrement');
+ frm.set_value('allow_rename', 0);
+ }
+ },
+
naming_rule: function(frm) {
// set the "autoname" property based on naming_rule
if (frm.doc.naming_rule && !frm.__from_autoname) {
@@ -70,6 +77,10 @@ frappe.ui.form.on('DocType', {
if (frm.doc.naming_rule=='Set by user') {
frm.set_value('autoname', 'Prompt');
+ } else if (frm.doc.naming_rule === 'Autoincrement') {
+ frm.set_value('autoname', 'autoincrement');
+ // set allow rename to be false when using autoincrement
+ frm.set_value('allow_rename', 0);
} else if (frm.doc.naming_rule=='By fieldname') {
frm.set_value('autoname', 'field:');
} else if (frm.doc.naming_rule=='By "Naming Series" field') {
@@ -91,6 +102,7 @@ frappe.ui.form.on('DocType', {
set_naming_rule_description(frm) {
let naming_rule_description = {
'Set by user': '',
+ 'Autoincrement': 'Uses Auto Increment feature of database. WARNING: After using this option, any other naming option will not be accessible.',
'By fieldname': 'Format: field:[fieldname]. Valid fieldname must exist',
'By "Naming Series" field': 'Format: naming_series:[fieldname]. Fieldname called naming_series must exist',
'Expression': 'Format: format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.',
@@ -111,6 +123,8 @@ frappe.ui.form.on('DocType', {
frm.__from_autoname = true;
if (frm.doc.autoname.toLowerCase() === 'prompt') {
frm.set_value('naming_rule', 'Set by user');
+ } else if (frm.doc.autoname.toLowerCase() === 'autoincrement') {
+ frm.set_value('naming_rule', 'Autoincrement');
} else if (frm.doc.autoname.startsWith('field:')) {
frm.set_value('naming_rule', 'By fieldname');
} else if (frm.doc.autoname.startsWith('naming_series:')) {
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 2bba4127bb..8169a59566 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -208,7 +208,7 @@
"label": "Naming"
},
{
- "description": "Naming Options:\n
field:[fieldname] - By Field
naming_series: - By Naming Series (field called naming_series must be present
Prompt - Prompt user for a name
[series] - Series by prefix (separated by a dot); for example PRE.#####
\n
format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
",
+ "description": "Naming Options:\n
field:[fieldname] - By Field
autoincrement - Uses Databases' Auto Increment feature
naming_series: - By Naming Series (field called naming_series must be present
Prompt - Prompt user for a name
[series] - Series by prefix (separated by a dot); for example PRE.#####
\n
format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
",
"fieldname": "autoname",
"fieldtype": "Data",
"label": "Auto Name",
@@ -216,6 +216,7 @@
"oldfieldtype": "Data"
},
{
+ "depends_on": "eval:doc.naming_rule !== \"Autoincrement\"",
"fieldname": "name_case",
"fieldtype": "Select",
"label": "Name Case",
@@ -282,6 +283,7 @@
},
{
"default": "1",
+ "depends_on": "eval:doc.naming_rule !== \"Autoincrement\"",
"fieldname": "allow_rename",
"fieldtype": "Check",
"label": "Allow Rename",
@@ -565,7 +567,7 @@
"fieldtype": "Select",
"label": "Naming Rule",
"length": 40,
- "options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
+ "options": "\nSet by user\nAutoincrement\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
},
{
"fieldname": "migration_hash",
@@ -593,6 +595,7 @@
],
"icon": "fa fa-bolt",
"idx": 6,
+ "index_web_pages_for_search": 1,
"links": [
{
"group": "Views",
@@ -670,10 +673,11 @@
"link_fieldname": "reference_doctype"
}
],
- "modified": "2022-01-07 16:07:06.196534",
+ "modified": "2022-02-15 21:47:16.467217",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@@ -703,5 +707,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index dca0a05281..29b56fbff6 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -60,6 +60,7 @@ class DocType(Document):
self.check_developer_mode()
+ self.validate_autoname()
self.validate_name()
self.set_defaults_for_single_and_table()
@@ -714,6 +715,18 @@ class DocType(Document):
self.name)
return max_idx and max_idx[0][0] or 0
+ def validate_autoname(self):
+ if not self.is_new():
+ doc_before_save = self.get_doc_before_save()
+ if doc_before_save:
+ if (self.autoname == "autoincrement" and doc_before_save.autoname != "autoincrement") \
+ or (self.autoname != "autoincrement" and doc_before_save.autoname == "autoincrement"):
+ frappe.throw(_("Cannot change to/from Autoincrement naming rule"))
+
+ else:
+ if self.autoname == "autoincrement":
+ self.allow_rename = 0
+
def validate_name(self, name=None):
if not name:
name = self.name
@@ -732,9 +745,12 @@ class DocType(Document):
frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError)
# a DocType's name should not start with a number or underscore
- # and should only contain letters, numbers and underscore
- if not re.match(r"^(?![\W])[^\d_\s][\w ]+$", name, **flags):
- frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError)
+ # and should only contain letters, numbers, underscore, and hyphen
+ if not re.match(r"^(?![\W])[^\d_\s][\w -]+$", name, **flags):
+ frappe.throw(_(
+ "A DocType's name should start with a letter and can only "
+ "consist of letters, numbers, spaces, underscores and hyphens"
+ ), frappe.NameError, title="Invalid Name")
validate_route_conflict(self.doctype, self.name)
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index 9b4f733e7d..dc6d14b451 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -24,7 +24,7 @@ class TestDocType(unittest.TestCase):
self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert)
self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert)
self.assertRaises(frappe.NameError, new_doctype("Some Doctype with a name whose length is more than 61 characters").insert)
- for name in ("Some DocType", "Some_DocType"):
+ for name in ("Some DocType", "Some_DocType", "Some-DocType"):
if frappe.db.exists("DocType", name):
frappe.delete_doc("DocType", name)
@@ -505,7 +505,23 @@ class TestDocType(unittest.TestCase):
dt.delete()
-def new_doctype(name, unique=0, depends_on='', fields=None):
+ def test_autoincremented_doctype_transition(self):
+ frappe.delete_doc("testy_autoinc_dt")
+ dt = new_doctype("testy_autoinc_dt", autoincremented=True).insert(ignore_permissions=True)
+ dt.autoname = "hash"
+
+ try:
+ dt.save(ignore_permissions=True)
+ except frappe.ValidationError as e:
+ self.assertEqual(e.args[0], "Cannot change to/from Autoincrement naming rule")
+ else:
+ self.fail("Shouldnt be possible to transition autoincremented doctype to any other naming rule")
+ finally:
+ # cleanup
+ dt.delete(ignore_permissions=True)
+
+
+def new_doctype(name, unique=0, depends_on='', fields=None, autoincremented=False):
doc = frappe.get_doc({
"doctype": "DocType",
"module": "Core",
@@ -521,7 +537,8 @@ def new_doctype(name, unique=0, depends_on='', fields=None):
"role": "System Manager",
"read": 1,
}],
- "name": name
+ "name": name,
+ "autoname": "autoincrement" if autoincremented else ""
})
if fields:
diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index d8e748a518..fb98a18d6e 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -382,7 +382,7 @@ class TestFile(unittest.TestCase):
}).insert(ignore_permissions=True)
test_file.make_thumbnail()
- self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg')
+ self.assertEqual(test_file.thumbnail_url, '/files/image_small.jpg')
# test web image without extension
test_file = frappe.get_doc({
@@ -399,7 +399,7 @@ class TestFile(unittest.TestCase):
test_file.reload()
test_file.file_url = "/files/image_small.jpg"
test_file.make_thumbnail(suffix="xs", crop=True)
- self.assertEquals(test_file.thumbnail_url, '/files/image_small_xs.jpg')
+ self.assertEqual(test_file.thumbnail_url, '/files/image_small_xs.jpg')
frappe.clear_messages()
test_file.db_set('thumbnail_url', None)
@@ -407,7 +407,7 @@ class TestFile(unittest.TestCase):
test_file.file_url = frappe.utils.get_url('unknown.jpg')
test_file.make_thumbnail(suffix="xs")
self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found")
- self.assertEquals(test_file.thumbnail_url, None)
+ self.assertEqual(test_file.thumbnail_url, None)
def test_file_unzip(self):
file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip')
diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py
index 389e18dd4c..f955c29462 100644
--- a/frappe/core/doctype/role/role.py
+++ b/frappe/core/doctype/role/role.py
@@ -61,7 +61,7 @@ class Role(Document):
def get_info_based_on_role(role, field='email'):
''' Get information of all users that have been assigned this role '''
- users = frappe.get_list("Has Role", filters={"role": role, "parenttype": "User"},
+ users = frappe.get_list("Has Role", filters={"role": role}, parent_doctype="User",
fields=["parent as user_name"])
return get_user_info(users, field)
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index d9381bcd16..aa4507b858 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -112,7 +112,10 @@ class TestServerScript(unittest.TestCase):
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello')
def test_permission_query(self):
- self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', run=False))
+ if frappe.conf.db_type == "mariadb":
+ self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', run=False))
+ else:
+ self.assertTrue('where (1 = \'1\')' in frappe.db.get_list('ToDo', run=False))
self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list))
def test_attribute_error(self):
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index a47f539466..9e9529cd5e 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -668,8 +668,7 @@
"link_fieldname": "user"
}
],
- "max_attachments": 5,
- "modified": "2022-01-03 11:53:25.250822",
+ "modified": "2022-03-09 01:47:56.745069",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index d08755f9a8..1ad977547c 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -253,8 +253,8 @@ class User(Document):
self.email_new_password(new_password)
except frappe.OutgoingEmailError:
- print(frappe.get_traceback())
- pass # email server not set, don't send email
+ # email server not set, don't send email
+ frappe.log_error(frappe.get_traceback())
@Document.hook
def validate_reset_password(self):
diff --git a/frappe/core/doctype/user_permission/user_permission.js b/frappe/core/doctype/user_permission/user_permission.js
index 8d5c5c1a23..f6989db5d8 100644
--- a/frappe/core/doctype/user_permission/user_permission.js
+++ b/frappe/core/doctype/user_permission/user_permission.js
@@ -44,8 +44,9 @@ frappe.ui.form.on('User Permission', {
set_applicable_for_constraint: frm => {
frm.toggle_reqd('applicable_for', !frm.doc.apply_to_all_doctypes);
+
if (frm.doc.apply_to_all_doctypes && frm.doc.applicable_for) {
- frm.set_value('applicable_for', null);
+ frm.set_value('applicable_for', null, null, true);
}
},
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 4862185b99..9cfe315e44 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -14,7 +14,6 @@ frappe.ui.form.on("Customize Form", {
},
onload: function(frm) {
- frm.disable_save();
frm.set_query("doc_type", function() {
return {
translate_values: false,
@@ -110,7 +109,7 @@ frappe.ui.form.on("Customize Form", {
},
refresh: function(frm) {
- frm.disable_save();
+ frm.disable_save(true);
frm.page.clear_icons();
if (frm.doc.doc_type) {
@@ -169,7 +168,7 @@ frappe.ui.form.on("Customize Form", {
doc_type = localStorage.getItem("customize_doctype");
}
if (doc_type) {
- setTimeout(() => frm.set_value("doc_type", doc_type), 1000);
+ setTimeout(() => frm.set_value("doc_type", doc_type, false, true), 1000);
}
},
@@ -341,11 +340,11 @@ frappe.customize_form.confirm = function(msg, frm) {
}
frappe.customize_form.clear_locals_and_refresh = function(frm) {
+ delete frm.doc.__unsaved;
// clear doctype from locals
frappe.model.clear_doc("DocType", frm.doc.doc_type);
delete frappe.meta.docfield_copy[frm.doc.doc_type];
-
frm.refresh();
-}
+};
extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm}));
diff --git a/frappe/database/__init__.py b/frappe/database/__init__.py
index 7b26ac31b3..5db0537ed7 100644
--- a/frappe/database/__init__.py
+++ b/frappe/database/__init__.py
@@ -18,7 +18,8 @@ def setup_database(force, source_sql=None, verbose=None, no_mariadb_socket=False
def drop_user_and_database(db_name, root_login=None, root_password=None):
import frappe
if frappe.conf.db_type == 'postgres':
- pass
+ import frappe.database.postgres.setup_db
+ return frappe.database.postgres.setup_db.drop_user_and_database(db_name, root_login, root_password)
else:
import frappe.database.mariadb.setup_db
return frappe.database.mariadb.setup_db.drop_user_and_database(db_name, root_login, root_password)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index dc9f20d8c2..82a7e6f919 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -119,6 +119,9 @@ class Database(object):
if not run:
return query
+ # remove \n \t from start and end of query
+ query = re.sub(r'^\s*|\s*$', '', query)
+
if re.search(r'ifnull\(', query, flags=re.IGNORECASE):
# replaces ifnull in query with coalesce
query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE)
@@ -142,8 +145,6 @@ class Database(object):
self.log_query(query, values, debug, explain)
if values!=():
- if isinstance(values, dict):
- values = dict(values)
# MySQL-python==1.2.5 hack!
if not isinstance(values, (dict, tuple, list)):
@@ -181,7 +182,7 @@ class Database(object):
print(e)
raise
- if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):
+ if ignore_ddl and (self.is_missing_column(e) or self.is_table_missing(e) or self.cant_drop_field_or_key(e)):
pass
else:
raise
@@ -386,7 +387,7 @@ class Database(object):
"""
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug,
- order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct)
+ order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct, limit=1)
if not run:
return ret
@@ -395,7 +396,7 @@ class Database(object):
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False,
- run=True, pluck=False, distinct=False):
+ run=True, pluck=False, distinct=False, limit=None):
"""Returns multiple document properties.
:param doctype: DocType name.
@@ -425,14 +426,15 @@ class Database(object):
if isinstance(filters, list):
out = self._get_value_for_many_names(
- doctype,
- filters,
- fieldname,
- order_by,
+ doctype=doctype,
+ names=filters,
+ field=fieldname,
+ order_by=order_by,
debug=debug,
run=run,
pluck=pluck,
distinct=distinct,
+ limit=limit,
)
else:
@@ -446,17 +448,18 @@ class Database(object):
if order_by:
order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by
out = self._get_values_from_table(
- fields,
- filters,
- doctype,
- as_dict,
- debug,
- order_by,
- update,
+ fields=fields,
+ filters=filters,
+ doctype=doctype,
+ as_dict=as_dict,
+ debug=debug,
+ order_by=order_by,
+ update=update,
for_update=for_update,
run=run,
pluck=pluck,
- distinct=distinct
+ distinct=distinct,
+ limit=limit,
)
except Exception as e:
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
@@ -625,6 +628,7 @@ class Database(object):
run=True,
pluck=False,
distinct=False,
+ limit=None,
):
field_objects = []
@@ -643,6 +647,7 @@ class Database(object):
field_objects=field_objects,
fields=fields,
distinct=distinct,
+ limit=limit,
)
if (
fields == "*"
@@ -656,7 +661,7 @@ class Database(object):
)
return r
- def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False):
+ def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False, limit=None):
names = list(filter(None, names))
if names:
return self.get_all(
@@ -669,6 +674,7 @@ class Database(object):
as_list=1,
run=run,
distinct=distinct,
+ limit_page_length=limit
)
else:
return {}
@@ -884,27 +890,39 @@ class Database(object):
return self.sql("select name from `tab{doctype}` limit 1".format(doctype=doctype))
def exists(self, dt, dn=None, cache=False):
- """Returns true if document exists.
+ """Return the document name of a matching document, or None.
- :param dt: DocType name.
- :param dn: Document name or filter dict."""
- if isinstance(dt, str):
- if dt!="DocType" and dt==dn:
- return True # single always exists (!)
- try:
- return self.get_value(dt, dn, "name", cache=cache)
- except Exception:
- return None
+ Note: `cache` only works if `dt` and `dn` are of type `str`.
- elif isinstance(dt, dict) and dt.get('doctype'):
- try:
- conditions = []
- for d in dt:
- if d == 'doctype': continue
- conditions.append([d, '=', dt[d]])
- return self.get_all(dt['doctype'], filters=conditions, as_list=1)
- except Exception:
- return None
+ ## Examples
+
+ Pass doctype and docname (only in this case we can cache the result)
+
+ ```
+ exists("User", "jane@example.org", cache=True)
+ ```
+
+ Pass a dict of filters including the `"doctype"` key:
+
+ ```
+ exists({"doctype": "User", "full_name": "Jane Doe"})
+ ```
+
+ Pass the doctype and a dict of filters:
+
+ ```
+ exists("User", {"full_name": "Jane Doe"})
+ ```
+ """
+ if dt != "DocType" and dt == dn:
+ # single always exists (!)
+ return dn
+
+ if isinstance(dt, dict):
+ dt = dt.copy() # don't modify the original dict
+ dt, dn = dt.pop("doctype"), dt
+
+ return self.get_value(dt, dn, ignore=True, cache=cache)
def count(self, dt, filters=None, debug=False, cache=False):
"""Returns `COUNT(*)` for given DocType and filters."""
@@ -1028,7 +1046,7 @@ class Database(object):
return []
def is_missing_table_or_column(self, e):
- return self.is_missing_column(e) or self.is_missing_table(e)
+ return self.is_missing_column(e) or self.is_table_missing(e)
def multisql(self, sql_dict, values=(), **kwargs):
current_dialect = frappe.db.db_type or 'mariadb'
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index b5971e236e..a6d5e7b3f2 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -154,6 +154,10 @@ class MariaDBDatabase(Database):
def is_table_missing(e):
return e.args[0] == ER.NO_SUCH_TABLE
+ @staticmethod
+ def is_missing_table(e):
+ return MariaDBDatabase.is_table_missing(e)
+
@staticmethod
def is_missing_column(e):
return e.args[0] == ER.BAD_FIELD_ERROR
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index fd4bfc6dd0..3b7aa443f2 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -1,12 +1,16 @@
import frappe
from frappe import _
from frappe.database.schema import DBTable
+from frappe.database.sequence import create_sequence
+from frappe.model import log_types
+
class MariaDBTable(DBTable):
def create(self):
additional_definitions = ""
engine = self.meta.get("engine") or "InnoDB"
varchar_len = frappe.db.VARCHAR_LEN
+ name_column = f"name varchar({varchar_len}) primary key"
# columns
column_defs = self.get_column_definitions()
@@ -29,9 +33,27 @@ class MariaDBTable(DBTable):
)
) + ',\n'
+ # creating sequence(s)
+ if (not self.meta.issingle and self.meta.autoname == "autoincrement")\
+ or self.doctype in log_types:
+
+ # NOTE: using a very small cache - as during backup, if the sequence was used in anyform,
+ # it drops the cache and uses the next non cached value in setval func and
+ # puts that in the backup file, which will start the counter
+ # from that value when inserting any new record in the doctype.
+ # By default the cache is 1000 which will mess up the sequence when
+ # using the system after a restore.
+ # issue link: https://jira.mariadb.org/browse/MDEV-21786
+ create_sequence(self.doctype, check_not_exists=True, cache=50)
+
+ # NOTE: not used nextval func as default as the ability to restore
+ # database with sequences has bugs in mariadb and gives a scary error.
+ # issue link: https://jira.mariadb.org/browse/MDEV-21786
+ name_column = "name bigint primary key"
+
# create table
query = f"""create table `{self.table_name}` (
- name varchar({varchar_len}) not null primary key,
+ {name_column},
creation datetime(6),
modified datetime(6),
modified_by varchar({varchar_len}),
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index b0793fcbf0..eb3e33d39c 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -99,16 +99,13 @@ class PostgresDatabase(Database):
return db_size[0].get('database_size')
# pylint: disable=W0221
- def sql(self, *args, **kwargs):
- if args:
- # since tuple is immutable
- args = list(args)
- args[0] = modify_query(args[0])
- args = tuple(args)
- elif kwargs.get('query'):
- kwargs['query'] = modify_query(kwargs.get('query'))
-
- return super(PostgresDatabase, self).sql(*args, **kwargs)
+ def sql(self, query, values=(), *args, **kwargs):
+ return super(PostgresDatabase, self).sql(
+ modify_query(query),
+ modify_values(values),
+ *args,
+ **kwargs
+ )
def get_tables(self, cached=True):
return [d[0] for d in self.sql("""select table_name
@@ -153,6 +150,10 @@ class PostgresDatabase(Database):
def is_table_missing(e):
return getattr(e, 'pgcode', None) == '42P01'
+ @staticmethod
+ def is_missing_table(e):
+ return PostgresDatabase.is_table_missing(e)
+
@staticmethod
def is_missing_column(e):
return getattr(e, 'pgcode', None) == '42703'
@@ -335,12 +336,47 @@ def modify_query(query):
query = replace_locate_with_strpos(query)
# select from requires ""
if re.search('from tab', query, flags=re.IGNORECASE):
- query = re.sub('from tab([a-zA-Z]*)', r'from "tab\1"', query, flags=re.IGNORECASE)
+ query = re.sub(r'from tab([\w-]*)', r'from "tab\1"', query, flags=re.IGNORECASE)
+ # only find int (with/without signs), ignore decimals (with/without signs), ignore hashes (which start with numbers),
+ # drop .0 from decimals and add quotes around them
+ #
+ # >>> query = "c='abcd' , a >= 45, b = -45.0, c = 40, d=4500.0, e=3500.53, f=40psdfsd, g=9092094312, h=12.00023"
+ # >>> re.sub(r"([=><]+)\s*(?!\d+[a-zA-Z])(?![+-]?\d+\.\d\d+)([+-]?\d+)(\.0)?", r"\1 '\2'", query)
+ # "c='abcd' , a >= '45', b = '-45', c = '40', d= '4500', e=3500.53, f=40psdfsd, g= '9092094312', h=12.00023
+
+ query = re.sub(r"([=><]+)\s*(?!\d+[a-zA-Z])(?![+-]?\d+\.\d\d+)([+-]?\d+)(\.0)?", r"\1 '\2'", query)
return query
+def modify_values(values):
+ def stringify_value(value):
+ if isinstance(value, int):
+ value = str(value)
+ elif isinstance(value, float):
+ truncated_float = int(value)
+ if value == truncated_float:
+ value = str(truncated_float)
+
+ return value
+
+ if not values:
+ return values
+
+ if isinstance(values, dict):
+ for k, v in values.items():
+ values[k] = stringify_value(v)
+ elif isinstance(values, (tuple, list)):
+ new_values = []
+ for val in values:
+ new_values.append(stringify_value(val))
+ values = new_values
+ else:
+ values = stringify_value(values)
+
+ return values
+
def replace_locate_with_strpos(query):
# strpos is the locate equivalent in postgres
if re.search(r'locate\(', query, flags=re.IGNORECASE):
- query = re.sub(r'locate\(([^,]+),([^)]+)\)', r'strpos(\2, \1)', query, flags=re.IGNORECASE)
+ query = re.sub(r'locate\(([^,]+),([^)]+)(\)?)\)', r'strpos(\2\3, \1)', query, flags=re.IGNORECASE)
return query
diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py
index bb7ff20a26..b09f73300e 100644
--- a/frappe/database/postgres/schema.py
+++ b/frappe/database/postgres/schema.py
@@ -2,10 +2,14 @@ import frappe
from frappe import _
from frappe.utils import cint, flt
from frappe.database.schema import DBTable, get_definition
+from frappe.database.sequence import create_sequence
+from frappe.model import log_types
+
class PostgresTable(DBTable):
def create(self):
varchar_len = frappe.db.VARCHAR_LEN
+ name_column = f"name varchar({varchar_len}) primary key"
additional_definitions = ""
# columns
@@ -26,9 +30,21 @@ class PostgresTable(DBTable):
)
)
+ # creating sequence(s)
+ if (not self.meta.issingle and self.meta.autoname == "autoincrement")\
+ or self.doctype in log_types:
+
+ # The sequence cache is per connection.
+ # Since we're opening and closing connections for every transaction this results in skipping the cache
+ # to the next non-cached value hence not using cache in postgres.
+ # ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers
+ create_sequence(self.doctype, check_not_exists=True)
+ name_column = "name bigint primary key"
+
+ # TODO: set docstatus length
# create table
frappe.db.sql(f"""create table `{self.table_name}` (
- name varchar({varchar_len}) not null primary key,
+ {name_column},
creation timestamp(6),
modified timestamp(6),
modified_by varchar({varchar_len}),
diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py
index b3b2e0fd41..4b265e7660 100644
--- a/frappe/database/postgres/setup_db.py
+++ b/frappe/database/postgres/setup_db.py
@@ -95,3 +95,11 @@ def get_root_connection(root_login=None, root_password=None):
frappe.local.flags.root_connection = frappe.database.get_db(user=root_login, password=root_password)
return frappe.local.flags.root_connection
+
+
+def drop_user_and_database(db_name, root_login, root_password):
+ root_conn = get_root_connection(frappe.flags.root_login or root_login, frappe.flags.root_password or root_password)
+ root_conn.commit()
+ root_conn.sql(f"SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = %s", (db_name, ))
+ root_conn.sql(f"DROP DATABASE IF EXISTS {db_name}")
+ root_conn.sql(f"DROP USER IF EXISTS {db_name}")
diff --git a/frappe/database/sequence.py b/frappe/database/sequence.py
new file mode 100644
index 0000000000..334fd3d71e
--- /dev/null
+++ b/frappe/database/sequence.py
@@ -0,0 +1,80 @@
+from frappe import db, scrub
+
+
+def create_sequence(
+ doctype_name: str,
+ *,
+ slug: str = "_id_seq",
+ check_not_exists: bool = False,
+ cycle: bool = False,
+ cache: int = 0,
+ start_value: int = 0,
+ increment_by: int = 0,
+ min_value: int = 0,
+ max_value: int = 0
+) -> str:
+
+ query = "create sequence"
+ sequence_name = scrub(doctype_name + slug)
+
+ if check_not_exists:
+ query += " if not exists"
+
+ query += f" {sequence_name}"
+
+ if cache:
+ query += f" cache {cache}"
+ else:
+ # in postgres, the default is cache 1
+ if db.db_type == "mariadb":
+ query += " nocache"
+
+ if start_value:
+ # default is 1
+ query += f" start with {start_value}"
+
+ if increment_by:
+ # default is 1
+ query += f" increment by {increment_by}"
+
+ if min_value:
+ # default is 1
+ query += f" min value {min_value}"
+
+ if max_value:
+ query += f" max value {max_value}"
+
+ if not cycle:
+ if db.db_type == "mariadb":
+ query += " nocycle"
+ else:
+ query += " cycle"
+
+ db.sql(query)
+
+ return sequence_name
+
+
+def get_next_val(doctype_name: str, slug: str = "_id_seq") -> int:
+ if db.db_type == "postgres":
+ return db.sql(f"select nextval(\'\"{scrub(doctype_name + slug)}\"\')")[0][0]
+ return db.sql(f"select nextval(`{scrub(doctype_name + slug)}`)")[0][0]
+
+
+def set_next_val(
+ doctype_name: str,
+ next_val: int,
+ *,
+ slug: str = "_id_seq",
+ is_val_used :bool = False
+) -> None:
+
+ if not is_val_used:
+ is_val_used = 0 if db.db_type == "mariadb" else "f"
+ else:
+ is_val_used = 1 if db.db_type == "mariadb" else "t"
+
+ if db.db_type == "postgres":
+ db.sql(f"SELECT SETVAL('\"{scrub(doctype_name + slug)}\"', {next_val}, '{is_val_used}')")
+ else:
+ db.sql(f"SELECT SETVAL(`{scrub(doctype_name + slug)}`, {next_val}, {is_val_used})")
diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js
index 6d1454a2cb..f548388a99 100644
--- a/frappe/desk/doctype/number_card/number_card.js
+++ b/frappe/desk/doctype/number_card/number_card.js
@@ -28,6 +28,7 @@ frappe.ui.form.on('Number Card', {
frm.trigger('render_filters_table');
}
frm.trigger('create_add_to_dashboard_button');
+ frm.trigger('set_parent_document_type');
},
create_add_to_dashboard_button: function(frm) {
@@ -141,7 +142,9 @@ frappe.ui.form.on('Number Card', {
frm.set_value('filters_json', '[]');
frm.set_value('dynamic_filters_json', '[]');
frm.set_value('aggregate_function_based_on', '');
+ frm.set_value('parent_document_type', '');
frm.trigger('set_options');
+ frm.trigger('set_parent_document_type');
},
set_options: function(frm) {
@@ -317,6 +320,7 @@ frappe.ui.form.on('Number Card', {
frm.filter_group = new frappe.ui.FilterGroup({
parent: dialog.get_field('filter_area').$wrapper,
doctype: frm.doc.document_type,
+ parent_doctype: frm.doc.parent_document_type,
on_change: () => {},
});
filters && frm.filter_group.add_filters_to_filter_group(filters);
@@ -436,6 +440,36 @@ frappe.ui.form.on('Number Card', {
frm.dynamic_filter_table.find('tbody').html(filter_rows);
}
+ },
+
+ set_parent_document_type: async function(frm) {
+ let document_type = frm.doc.document_type;
+ let doc_is_table = document_type &&
+ (await frappe.db.get_value('DocType', document_type, 'istable')).message.istable;
+
+ frm.set_df_property('parent_document_type', 'hidden', !doc_is_table);
+
+ if (document_type && doc_is_table) {
+ let parent = await frappe.db.get_list('DocField', {
+ filters: {
+ 'fieldtype': 'Table',
+ 'options': document_type
+ },
+ fields: ['parent']
+ });
+
+ parent && frm.set_query('parent_document_type', function() {
+ return {
+ filters: {
+ "name": ['in', parent.map(({ parent }) => parent)]
+ }
+ };
+ });
+
+ if (parent.length === 1) {
+ frm.set_value('parent_document_type', parent[0].parent);
+ }
+ }
}
});
diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json
index d3e9598eb7..7975d878ba 100644
--- a/frappe/desk/doctype/number_card/number_card.json
+++ b/frappe/desk/doctype/number_card/number_card.json
@@ -16,6 +16,7 @@
"aggregate_function_based_on",
"column_break_2",
"document_type",
+ "parent_document_type",
"report_field",
"report_function",
"is_public",
@@ -188,10 +189,17 @@
"label": "Function",
"mandatory_depends_on": "eval: doc.type == 'Report'",
"options": "Sum\nAverage\nMinimum\nMaximum"
+ },
+ {
+ "description": "The document type selected is a child table, so the parent document type is required.",
+ "fieldname": "parent_document_type",
+ "fieldtype": "Link",
+ "label": "Parent Document Type",
+ "options": "DocType"
}
],
"links": [],
- "modified": "2020-07-23 11:11:03.391719",
+ "modified": "2022-03-10 15:34:38.210910",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",
@@ -234,6 +242,7 @@
"search_fields": "label, document_type",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "label",
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index 5662523a9d..784f46bb19 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -3,6 +3,7 @@
# License: MIT. See LICENSE
import frappe
+from frappe import _
from frappe.model.document import Document
from frappe.utils import cint
from frappe.model.naming import append_number_if_name_exists
@@ -17,6 +18,13 @@ class NumberCard(Document):
if frappe.db.exists("Number Card", self.name):
self.name = append_number_if_name_exists('Number Card', self.name)
+ def validate(self):
+ if not self.document_type:
+ frappe.throw(_("Document type is required to create a number card"))
+
+ if self.document_type and frappe.get_meta(self.document_type).istable and not self.parent_document_type:
+ frappe.throw(_("Parent document type is required to create a number card"))
+
def on_update(self):
if frappe.conf.developer_mode and self.is_standard:
export_to_files(record_list=[['Number Card', self.name]], record_module=self.module)
diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js
index fc83069fd2..7751ffe860 100644
--- a/frappe/desk/doctype/system_console/system_console.js
+++ b/frappe/desk/doctype/system_console/system_console.js
@@ -88,15 +88,16 @@ frappe.ui.form.on('System Console', {
${row.Progress}
`
}
+
frm.get_field('processlist').html(`
Requested on: ${timestamp}
-
Id
+
Id
Time
State
Info
-
Progress
+
Progress / Wait Event
${rows}`);
});
diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py
index 107ab2f932..bf0925e2d7 100644
--- a/frappe/desk/doctype/system_console/system_console.py
+++ b/frappe/desk/doctype/system_console/system_console.py
@@ -41,4 +41,14 @@ def execute_code(doc):
@frappe.whitelist()
def show_processlist():
frappe.only_for('System Manager')
- return frappe.db.sql('show full processlist', as_dict=1)
+
+ return frappe.db.multisql({
+ "postgres": """
+ SELECT pid AS "Id",
+ query_start AS "Time",
+ state AS "State",
+ query AS "Info",
+ wait_event AS "Progress"
+ FROM pg_stat_activity""",
+ "mariadb": "show full processlist"
+ }, as_dict=True)
diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index f0a3531ae4..ba3319b591 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -277,6 +277,7 @@ def sort_page(workspace_pages, pages):
doc = frappe.get_doc('Workspace', page.name)
doc.sequence_id = seq + 1
doc.parent_page = d.get('parent_page') or ""
+ doc.flags.ignore_links = True
doc.save(ignore_permissions=True)
break
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index 572d3f2a94..010d65c95b 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -1,9 +1,10 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
+
import json
from collections import defaultdict
import itertools
-from typing import List
+from typing import Dict, List, Optional
import frappe
import frappe.desk.form.load
@@ -367,7 +368,7 @@ def get_exempted_doctypes():
@frappe.whitelist()
-def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
+def get_linked_docs(doctype: str, name: str, linkinfo: Optional[Dict] = None) -> Dict[str, List]:
if isinstance(linkinfo, str):
# additional fields are added in linkinfo
linkinfo = json.loads(linkinfo)
@@ -377,23 +378,21 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
if not linkinfo:
return results
- if for_doctype:
- links = frappe.get_doc(doctype, name).get_link_filters(for_doctype)
-
- if links:
- linkinfo = links
-
- if for_doctype in linkinfo:
- # only get linked with for this particular doctype
- linkinfo = { for_doctype: linkinfo.get(for_doctype) }
- else:
- return results
-
for dt, link in linkinfo.items():
filters = []
link["doctype"] = dt
- link_meta_bundle = frappe.desk.form.load.get_meta_bundle(dt)
+ try:
+ link_meta_bundle = frappe.desk.form.load.get_meta_bundle(dt)
+ except Exception as e:
+ if isinstance(e, frappe.DoesNotExistError):
+ if frappe.local.message_log:
+ frappe.local.message_log.pop()
+ continue
linkmeta = link_meta_bundle[0]
+
+ if not linkmeta.has_permission():
+ continue
+
if not linkmeta.get("issingle"):
fields = [d.fieldname for d in linkmeta.get("fields", {
"in_list_view": 1,
@@ -456,6 +455,13 @@ def get_linked_docs(doctype, name, linkinfo=None, for_doctype=None):
return results
+
+@frappe.whitelist()
+def get(doctype, docname):
+ linked_doctypes = get_linked_doctypes(doctype=doctype)
+ return get_linked_docs(doctype=doctype, name=docname, linkinfo=linked_doctypes)
+
+
@frappe.whitelist()
def get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False):
"""add list of doctypes this doctype is 'linked' with.
@@ -470,6 +476,7 @@ def get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False):
else:
return frappe.cache().hget("linked_doctypes", doctype, lambda: _get_linked_doctypes(doctype))
+
def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False):
ret = {}
# find fields where this doctype is linked
@@ -499,6 +506,7 @@ def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False)
return ret
+
def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False):
filters = [['fieldtype','=', 'Link'], ['options', '=', doctype]]
@@ -529,6 +537,7 @@ def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False):
return ret
+
def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=False):
ret = {}
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index b5dfacb1d6..0140157c9d 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -10,6 +10,7 @@ import frappe.desk.form.meta
from frappe.model.utils.user_settings import get_user_settings
from frappe.permissions import get_doc_permissions
from frappe.desk.form.document_follow import is_document_followed
+from frappe.utils.data import cstr
from frappe import _
from frappe import _dict
from urllib.parse import quote
@@ -124,7 +125,6 @@ def get_docinfo(doc=None, doctype=None, name=None):
update_user_info(docinfo)
frappe.response["docinfo"] = docinfo
- return docinfo
def add_comments(doc, docinfo):
# divide comments into separate lists
@@ -356,7 +356,7 @@ def get_document_email(doctype, name):
return None
email = email.split("@")
- return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(name), email[1])
+ return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(cstr(name)), email[1])
def get_automatic_email_link():
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index b344763916..f5f50b14fe 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -352,14 +352,10 @@ def export_query():
)
return
- columns = get_columns_dict(data.columns)
-
from frappe.utils.xlsxutils import make_xlsx
- data["result"] = handle_duration_fieldtype_values(
- data.get("result"), data.get("columns")
- )
- xlsx_data, column_widths = build_xlsx_data(columns, data, visible_idx, include_indentation)
+ format_duration_fields(data)
+ xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation)
xlsx_file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths)
frappe.response["filename"] = report_name + ".xlsx"
@@ -367,39 +363,18 @@ def export_query():
frappe.response["type"] = "binary"
-def handle_duration_fieldtype_values(result, columns):
- for i, col in enumerate(columns):
- fieldtype = None
- if isinstance(col, str):
- col = col.split(":")
- if len(col) > 1:
- if col[1]:
- fieldtype = col[1]
- if "/" in fieldtype:
- fieldtype, options = fieldtype.split("/")
- else:
- fieldtype = "Data"
- else:
- fieldtype = col.get("fieldtype")
+def format_duration_fields(data: frappe._dict) -> None:
+ for i, col in enumerate(data.columns):
+ if col.get("fieldtype") != "Duration":
+ continue
- if fieldtype == "Duration":
- for entry in range(0, len(result)):
- row = result[entry]
- if isinstance(row, dict):
- val_in_seconds = row[col.fieldname]
- if val_in_seconds:
- duration_val = format_duration(val_in_seconds)
- row[col.fieldname] = duration_val
- else:
- val_in_seconds = row[i]
- if val_in_seconds:
- duration_val = format_duration(val_in_seconds)
- row[i] = duration_val
-
- return result
+ for row in data.result:
+ index = col.fieldname if isinstance(row, dict) else i
+ if row[index]:
+ row[index] = format_duration(row[index])
-def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visible_idx=False):
+def build_xlsx_data(data, visible_idx, include_indentation, ignore_visible_idx=False):
result = [[]]
column_widths = []
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index 3b76953ed1..b54ea46268 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -257,7 +257,7 @@ def scrub_custom_query(query, key, txt):
def relevance_sorter(key, query, as_dict):
value = _(key.name if as_dict else key[0])
return (
- value.lower().startswith(query.lower()) is not True,
+ cstr(value).lower().startswith(query.lower()) is not True,
value
)
diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py
index 682f0df7cf..abeb681a25 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -15,8 +15,6 @@ from frappe.utils.csvutils import to_csv
from frappe.utils.xlsxutils import make_xlsx
from frappe.desk.query_report import build_xlsx_data
-max_reports_per_user = frappe.local.conf.max_reports_per_user or 3
-
class AutoEmailReport(Document):
def autoname(self):
@@ -46,6 +44,8 @@ class AutoEmailReport(Document):
def validate_report_count(self):
'''check that there are only 3 enabled reports per user'''
count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0]
+ max_reports_per_user = frappe.local.conf.max_reports_per_user or 3
+
if count > max_reports_per_user + (-1 if self.flags.in_insert else 0):
frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user))
@@ -104,7 +104,7 @@ class AutoEmailReport(Document):
report_data['columns'] = columns
report_data['result'] = data
- xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
+ xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True)
xlsx_file = make_xlsx(xlsx_data, "Auto Email Report", column_widths=column_widths)
return xlsx_file.getvalue()
@@ -113,7 +113,7 @@ class AutoEmailReport(Document):
report_data['columns'] = columns
report_data['result'] = data
- xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
+ xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True)
return to_csv(xlsx_data)
else:
diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json
index baabd4991e..b42f4755cb 100644
--- a/frappe/email/doctype/newsletter/newsletter.json
+++ b/frappe/email/doctype/newsletter/newsletter.json
@@ -236,8 +236,7 @@
"index_web_pages_for_search": 1,
"is_published_field": "published",
"links": [],
- "max_attachments": 3,
- "modified": "2021-12-06 20:09:37.963141",
+ "modified": "2022-03-09 01:48:16.741603",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py
index 8c1f803a46..b091c31c74 100644
--- a/frappe/email/doctype/newsletter/test_newsletter.py
+++ b/frappe/email/doctype/newsletter/test_newsletter.py
@@ -51,7 +51,7 @@ class TestNewsletterMixin:
"reference_name": newsletter,
})
frappe.delete_doc("Newsletter", newsletter)
- frappe.db.delete("Newsletter Email Group", newsletter)
+ frappe.db.delete("Newsletter Email Group", {"parent": newsletter})
newsletters.remove(newsletter)
def setup_email_group(self):
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index 2b62530847..bad32fb68f 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -186,7 +186,7 @@ def get_context(context):
def send_an_email(self, doc, context):
from email.utils import formataddr
- from frappe.core.doctype.communication.email import make as make_communication
+ from frappe.core.doctype.communication.email import _make as make_communication
subject = self.subject
if "{" in subject:
subject = frappe.render_template(self.subject, context)
@@ -216,7 +216,8 @@ def get_context(context):
# Add mail notification to communication list
# No need to add if it is already a communication.
if doc.doctype != 'Communication':
- make_communication(doctype=doc.doctype,
+ make_communication(
+ doctype=doc.doctype,
name=doc.name,
content=message,
subject=subject,
@@ -228,7 +229,7 @@ def get_context(context):
cc=cc,
bcc=bcc,
communication_type='Automated Message',
- ignore_permissions=True)
+ )
def send_a_slack_msg(self, doc, context):
send_slack_message(
diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py
index f05d35be3e..f6f216ada2 100644
--- a/frappe/email/doctype/notification/test_notification.py
+++ b/frappe/email/doctype/notification/test_notification.py
@@ -240,7 +240,7 @@ class TestNotification(unittest.TestCase):
self.assertTrue(email_queue)
# check if description is changed after alert since set_property_after_alert is set
- self.assertEquals(todo.description, 'Changed by Notification')
+ self.assertEqual(todo.description, 'Changed by Notification')
recipients = [d.recipient for d in email_queue.recipients]
self.assertTrue('test2@example.com' in recipients)
diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py
index c25e996bd3..0f45e42aac 100755
--- a/frappe/email/email_body.py
+++ b/frappe/email/email_body.py
@@ -259,17 +259,12 @@ def get_formatted_html(subject, message, footer=None, print_html=None,
email_account = email_account or EmailAccount.find_outgoing(match_by_email=sender)
- signature = None
- if "" not in message:
- signature = get_signature(email_account)
-
rendered_email = frappe.get_template("templates/emails/standard.html").render({
"brand_logo": get_brand_logo(email_account) if with_container or header else None,
"with_container": with_container,
"site_url": get_url(),
"header": get_header(header),
"content": message,
- "signature": signature,
"footer": get_footer(email_account, footer),
"title": subject,
"print_html": print_html,
@@ -281,8 +276,7 @@ def get_formatted_html(subject, message, footer=None, print_html=None,
if unsubscribe_link:
html = html.replace("", unsubscribe_link.html)
- html = inline_style_in_html(html)
- return html
+ return inline_style_in_html(html)
@frappe.whitelist()
def get_email_html(template, args, subject, header=None, with_container=False):
diff --git a/frappe/event_streaming/doctype/event_update_log/event_update_log.py b/frappe/event_streaming/doctype/event_update_log/event_update_log.py
index f4871be312..cd5100623c 100644
--- a/frappe/event_streaming/doctype/event_update_log/event_update_log.py
+++ b/frappe/event_streaming/doctype/event_update_log/event_update_log.py
@@ -203,12 +203,17 @@ def get_unread_update_logs(consumer_name, dt, dn):
SELECT
update_log.name
FROM `tabEvent Update Log` update_log
- JOIN `tabEvent Update Log Consumer` consumer ON consumer.parent = update_log.name
+ JOIN `tabEvent Update Log Consumer` consumer ON consumer.parent = %(log_name)s
WHERE
consumer.consumer = %(consumer)s
AND update_log.ref_doctype = %(dt)s
AND update_log.docname = %(dn)s
- """, {'consumer': consumer_name, "dt": dt, "dn": dn}, as_dict=0)]
+ """, {
+ "consumer": consumer_name,
+ "dt": dt,
+ "dn": dn,
+ "log_name": "update_log.name" if frappe.conf.db_type == "mariadb" else "CAST(update_log.name AS VARCHAR)"
+ }, as_dict=0)]
logs = frappe.get_all(
'Event Update Log',
diff --git a/frappe/frappeclient.py b/frappe/frappeclient.py
index 59db38584c..7a1587aae0 100644
--- a/frappe/frappeclient.py
+++ b/frappe/frappeclient.py
@@ -7,6 +7,7 @@ import json
import requests
import frappe
+from frappe.utils.data import cstr
class AuthError(Exception):
@@ -122,7 +123,7 @@ class FrappeClient(object):
'''Update a remote document
:param doc: dict or Document object to be updated remotely. `name` is mandatory for this'''
- url = self.url + "/api/resource/" + doc.get("doctype") + "/" + doc.get("name")
+ url = self.url + "/api/resource/" + doc.get("doctype") + "/" + cstr(doc.get("name"))
res = self.session.put(url, data={"data":frappe.as_json(doc)}, verify=self.verify, headers=self.headers)
return frappe._dict(self.post_process(res))
@@ -207,7 +208,7 @@ class FrappeClient(object):
if fields:
params["fields"] = json.dumps(fields)
- res = self.session.get(self.url + "/api/resource/" + doctype + "/" + name,
+ res = self.session.get(self.url + "/api/resource/" + doctype + "/" + cstr(name),
params=params, verify=self.verify, headers=self.headers)
return self.post_process(res)
diff --git a/frappe/handler.py b/frappe/handler.py
old mode 100755
new mode 100644
index 3fd1c096e4..ebc72da937
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -225,11 +225,10 @@ def ping():
def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
"""run a whitelisted controller method"""
- import json
- import inspect
+ from inspect import getfullargspec
- if not args:
- args = arg or ""
+ if not args and arg:
+ args = arg
if dt: # not called from a doctype (from a page)
if not dn:
@@ -237,9 +236,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
doc = frappe.get_doc(dt, dn)
else:
- if isinstance(docs, str):
- docs = json.loads(docs)
-
+ docs = frappe.parse_json(docs)
doc = frappe.get_doc(docs)
doc._original_modified = doc.modified
doc.check_if_latest()
@@ -248,16 +245,16 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
throw_permission_error()
try:
- args = json.loads(args)
+ args = frappe.parse_json(args)
except ValueError:
- args = args
+ pass
method_obj = getattr(doc, method)
fn = getattr(method_obj, '__func__', method_obj)
is_whitelisted(fn)
is_valid_http_method(fn)
- fnargs = inspect.getfullargspec(method_obj).args
+ fnargs = getfullargspec(method_obj).args
if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"):
response = doc.run_method(method)
diff --git a/frappe/installer.py b/frappe/installer.py
index 6ebab95a7d..d10dc78286 100644
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -611,7 +611,7 @@ def is_downgrade(sql_file_path, verbose=False):
downgrade = backup_version > current_version
if verbose and downgrade:
- print("Your site will be downgraded from Frappe {0} to {1}".format(current_version, backup_version))
+ print(f"Your site will be downgraded from Frappe {backup_version} to {current_version}")
return downgrade
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 8a81aa5610..57b4777355 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -475,7 +475,7 @@ class BaseDocument(object):
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE)
# don't update name, as case might've been changed
- name = d['name']
+ name = cstr(d['name'])
del d['name']
columns = list(d)
@@ -963,7 +963,7 @@ class BaseDocument(object):
from frappe.model.meta import get_default_df
df = get_default_df(fieldname)
- if not currency and df:
+ if df.fieldtype == "Currency" and not currency:
currency = self.get(df.get("options"))
if not frappe.db.exists('Currency', currency, cache=True):
currency = None
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index a6b96e8fb5..16056d382a 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -164,7 +164,8 @@ class DatabaseQuery(object):
# left join parent, child tables
for child in self.tables[1:]:
- args.tables += f" {self.join} {child} on ({child}.parent = {self.tables[0]}.name)"
+ parent_name = self.cast_name(f"{self.tables[0]}.name")
+ args.tables += f" {self.join} {child} on ({child}.parent = {parent_name})"
if self.grouped_or_conditions:
self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})")
@@ -318,21 +319,60 @@ class DatabaseQuery(object):
]
# add tables from fields
if self.fields:
- for field in self.fields:
- if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field):
+ for i, field in enumerate(self.fields):
+ # add cast in locate/strpos
+ func_found = False
+ for func in sql_functions:
+ if func in field.lower():
+ self.fields[i] = self.cast_name(field, func)
+ func_found = True
+ break
+
+ if func_found or not ("tab" in field and "." in field):
continue
table_name = field.split('.')[0]
if table_name.lower().startswith('group_concat('):
table_name = table_name[13:]
- if table_name.lower().startswith('ifnull('):
- table_name = table_name[7:]
if not table_name[0]=='`':
table_name = f"`{table_name}`"
if table_name not in self.tables:
self.append_table(table_name)
+ def cast_name(self, column: str, sql_function: str = "",) -> str:
+ if frappe.db.db_type == "postgres":
+ if "name" in column.lower():
+ if "cast(" not in column.lower() or "::" not in column:
+ if not sql_function:
+ return f"cast({column} as varchar)"
+
+ elif sql_function == "locate(":
+ return re.sub(
+ r'locate\(([^,]+),([^)]+)\)',
+ r'locate(\1, cast(\2 as varchar))',
+ column,
+ flags=re.IGNORECASE
+ )
+
+ elif sql_function == "strpos(":
+ return re.sub(
+ r'strpos\(([^,]+),([^)]+)\)',
+ r'strpos(cast(\1 as varchar), \2)',
+ column,
+ flags=re.IGNORECASE
+ )
+
+ elif sql_function == "ifnull(":
+ return re.sub(
+ r"ifnull\(([^,]+)",
+ r"ifnull(cast(\1 as varchar)",
+ column,
+ flags=re.IGNORECASE
+ )
+
+ return column
+
def append_table(self, table_name):
self.tables.append(table_name)
doctype = table_name[4:-1]
@@ -423,6 +463,8 @@ class DatabaseQuery(object):
ifnull(`tabDocType`.`fieldname`, fallback) operator "value"
"""
+ # TODO: refactor
+
from frappe.boot import get_additional_filters_from_hooks
additional_filters_config = get_additional_filters_from_hooks()
f = get_filter(self.doctype, f, additional_filters_config)
@@ -432,15 +474,16 @@ class DatabaseQuery(object):
self.append_table(tname)
if 'ifnull(' in f.fieldname:
- column_name = f.fieldname
+ column_name = self.cast_name(f.fieldname, "ifnull(")
else:
- column_name = f"{tname}.{f.fieldname}"
-
- can_be_null = True
+ column_name = self.cast_name(f"{tname}.{f.fieldname}")
if f.operator.lower() in additional_filters_config:
f.update(get_additional_filter_field(additional_filters_config, f, f.value))
+ meta = frappe.get_meta(f.doctype)
+ can_be_null = True
+
# prepare in condition
if f.operator.lower() in ('ancestors of', 'descendants of', 'not ancestors of', 'not descendants of'):
values = f.value or ''
@@ -449,12 +492,8 @@ class DatabaseQuery(object):
# if not isinstance(values, (list, tuple)):
# values = values.split(",")
- ref_doctype = f.doctype
-
- if frappe.get_meta(f.doctype).get_field(f.fieldname) is not None :
- ref_doctype = frappe.get_meta(f.doctype).get_field(f.fieldname).options
-
- result=[]
+ field = meta.get_field(f.fieldname)
+ ref_doctype = field.options if field else f.doctype
lft, rgt = '', ''
if f.value:
@@ -474,29 +513,30 @@ class DatabaseQuery(object):
}, order_by='`lft` DESC')
fallback = "''"
- value = [frappe.db.escape((v.name or '').strip(), percent=False) for v in result]
+ value = [frappe.db.escape((cstr(v.name) or '').strip(), percent=False) for v in result]
if len(value):
value = f"({', '.join(value)})"
else:
value = "('')"
+
# changing operator to IN as the above code fetches all the parent / child values and convert into tuple
# which can be directly used with IN operator to query.
f.operator = 'not in' if f.operator.lower() in ('not ancestors of', 'not descendants of') else 'in'
-
elif f.operator.lower() in ('in', 'not in'):
values = f.value or ''
if isinstance(values, str):
values = values.split(",")
fallback = "''"
- value = [frappe.db.escape((v or '').strip(), percent=False) for v in values]
+ value = [frappe.db.escape((cstr(v) or '').strip(), percent=False) for v in values]
if len(value):
value = f"({', '.join(value)})"
else:
value = "('')"
+
else:
- df = frappe.get_meta(f.doctype).get("fields", {"fieldname": f.fieldname})
+ df = meta.get("fields", {"fieldname": f.fieldname})
df = df[0] if df else None
if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"):
@@ -513,7 +553,8 @@ class DatabaseQuery(object):
fallback = "'0001-01-01 00:00:00'"
elif f.operator.lower() in ('between') and \
- (f.fieldname in ('creation', 'modified') or (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))):
+ (f.fieldname in ('creation', 'modified') or
+ (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))):
value = get_between_date_filter(f.value, df)
fallback = "'0001-01-01 00:00:00'"
@@ -528,7 +569,7 @@ class DatabaseQuery(object):
fallback = "''"
can_be_null = True
- if 'ifnull' not in column_name:
+ if 'ifnull' not in column_name.lower():
column_name = f'ifnull({column_name}, {fallback})'
elif df and df.fieldtype=="Date":
@@ -570,7 +611,7 @@ class DatabaseQuery(object):
value = f"{tname}.{quote}{f.value.name}{quote}"
# escape value
- elif isinstance(value, str) and not f.operator.lower() == 'between':
+ elif isinstance(value, str) and f.operator.lower() != 'between':
value = f"{frappe.db.escape(value, percent=False)}"
if (
diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py
index ef73a349cc..f055cd79d0 100644
--- a/frappe/model/delete_doc.py
+++ b/frappe/model/delete_doc.py
@@ -158,7 +158,7 @@ def update_naming_series(doc):
and getattr(doc, "naming_series", None):
revert_series_if_last(doc.naming_series, doc.name, doc)
- elif doc.meta.autoname.split(":")[0] not in ("Prompt", "field", "hash"):
+ elif doc.meta.autoname.split(":")[0] not in ("Prompt", "field", "hash", "autoincrement"):
revert_series_if_last(doc.meta.autoname, doc.name, doc)
def delete_from_table(doctype, name, ignore_doctypes, doc):
diff --git a/frappe/model/naming.py b/frappe/model/naming.py
index 9024b3d7b4..013e5a19db 100644
--- a/frappe/model/naming.py
+++ b/frappe/model/naming.py
@@ -1,14 +1,18 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-from typing import Optional
+from typing import Optional, TYPE_CHECKING, Union
import frappe
from frappe import _
+from frappe.database.sequence import get_next_val, set_next_val
from frappe.utils import now_datetime, cint, cstr
import re
from frappe.model import log_types
from frappe.query_builder import DocType
+if TYPE_CHECKING:
+ from frappe.model.meta import Meta
+
def set_new_name(doc):
"""
@@ -24,11 +28,16 @@ def set_new_name(doc):
doc.run_method("before_naming")
- autoname = frappe.get_meta(doc.doctype).autoname or ""
+ meta = frappe.get_meta(doc.doctype)
+ autoname = meta.autoname or ""
if autoname.lower() != "prompt" and not frappe.flags.in_import:
doc.name = None
+ if is_autoincremented(doc.doctype, meta):
+ doc.name = get_next_val(doc.doctype)
+ return
+
if getattr(doc, "amended_from", None):
_set_amended_name(doc)
return
@@ -64,9 +73,37 @@ def set_new_name(doc):
doc.name = validate_name(
doc.doctype,
doc.name,
- frappe.get_meta(doc.doctype).get_field("name_case")
+ meta.get_field("name_case")
)
+def is_autoincremented(doctype: str, meta: "Meta" = None):
+ if doctype in log_types:
+ if frappe.local.autoincremented_status_map.get(frappe.local.site) is None or \
+ frappe.local.autoincremented_status_map[frappe.local.site] == -1:
+ if frappe.db.sql(
+ f"""select data_type FROM information_schema.columns
+ where column_name = 'name' and table_name = 'tab{doctype}'"""
+ )[0][0] == "bigint":
+ frappe.local.autoincremented_status_map[frappe.local.site] = 1
+ return True
+ else:
+ frappe.local.autoincremented_status_map[frappe.local.site] = 0
+
+ elif frappe.local.autoincremented_status_map[frappe.local.site]:
+ return True
+
+ else:
+ if not meta:
+ meta = frappe.get_meta(doctype)
+
+ if getattr(meta, "issingle", False):
+ return False
+
+ if meta.autoname == "autoincrement":
+ return True
+
+ return False
+
def set_name_from_naming_options(autoname, doc):
"""
Get a name based on the autoname field option
@@ -284,9 +321,19 @@ def get_default_naming_series(doctype):
return None
-def validate_name(doctype: str, name: str, case: Optional[str] = None):
+def validate_name(doctype: str, name: Union[int, str], case: Optional[str] = None):
if not name:
frappe.throw(_("No Name Specified for {0}").format(doctype))
+
+ if isinstance(name, int):
+ if is_autoincremented(doctype):
+ # this will set the sequence val to be the provided name and set it to be used
+ # so that the sequence will start from the next val of the setted val(name)
+ set_next_val(doctype, name, is_val_used=True)
+ return name
+
+ frappe.throw(_("Invalid name type (integer) for varchar name column"), frappe.NameError)
+
if name.startswith("New "+doctype):
frappe.throw(_("There were some errors setting the name, please contact the administrator"), frappe.NameError)
if case == "Title Case":
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index 6874ed1ab6..aad16664c8 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -54,8 +54,8 @@ def update_document_title(
title_field = doc.meta.get_title_field()
- title_updated = (title_field != "name") and (updated_title != doc.get(title_field))
- name_updated = updated_name != doc.name
+ title_updated = updated_title and (title_field != "name") and (updated_title != doc.get(title_field))
+ name_updated = updated_name and (updated_name != doc.name)
if name_updated:
if enqueue and not is_scheduler_inactive():
diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py
index 92e7523e6d..f9c7b55a99 100644
--- a/frappe/modules/import_file.py
+++ b/frappe/modules/import_file.py
@@ -11,7 +11,7 @@ from frappe.query_builder import DocType
from frappe.utils import get_datetime, now
-def caclulate_hash(path: str) -> str:
+def calculate_hash(path: str) -> str:
"""Calculate md5 hash of the file in binary mode
Args:
@@ -99,7 +99,7 @@ def import_file_by_path(path: str,force: bool = False,data_import: bool = False,
print(f"{path} missing")
return
- calculated_hash = caclulate_hash(path)
+ calculated_hash = calculate_hash(path)
if docs:
if not isinstance(docs, list):
diff --git a/frappe/patches.txt b/frappe/patches.txt
index a666480c90..82b1f497c2 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -146,7 +146,7 @@ frappe.patches.v13_0.update_duration_options
frappe.patches.v13_0.replace_old_data_import # 2020-06-24
frappe.patches.v13_0.create_custom_dashboards_cards_and_charts
frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart
-frappe.patches.v13_0.add_standard_navbar_items # 2020-12-15
+frappe.patches.v13_0.add_standard_navbar_items # 2022-03-15
frappe.patches.v13_0.generate_theme_files_in_public_folder
frappe.patches.v13_0.increase_password_length
frappe.patches.v12_0.fix_email_id_formatting
diff --git a/frappe/permissions.py b/frappe/permissions.py
index af17faba01..a6c17fb59f 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -594,4 +594,4 @@ def is_parent_valid(child_doctype, parent_doctype):
from frappe.core.utils import find
parent_meta = frappe.get_meta(parent_doctype)
child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype)
- return not parent_meta.istable and child_table_field_exists
\ No newline at end of file
+ return not parent_meta.istable and child_table_field_exists
diff --git a/frappe/public/css/tree.css b/frappe/public/css/tree.css
index 2aa411bc11..8b216bc321 100644
--- a/frappe/public/css/tree.css
+++ b/frappe/public/css/tree.css
@@ -24,7 +24,7 @@ ul.tree-children {
}
.tree-link .node-parent,
.tree-link .node-leaf {
- margin-right: 5px;
+ margin-right: 8px;
}
.tree-link.active i {
color: #5e64ff;
diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js
index bd66225171..a91058a208 100644
--- a/frappe/public/js/frappe/form/controls/attach.js
+++ b/frappe/public/js/frappe/form/controls/attach.js
@@ -37,8 +37,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro
if(this.frm) {
me.parse_validate_and_set_in_model(null);
me.refresh();
- me.frm.attachments.remove_attachment_by_filename(me.value, function() {
- me.parse_validate_and_set_in_model(null);
+ me.frm.attachments.remove_attachment_by_filename(me.value, async () => {
+ await me.parse_validate_and_set_in_model(null);
me.refresh();
me.frm.doc.docstatus == 1 ? me.frm.save('Update') : me.frm.save();
});
@@ -110,9 +110,9 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro
return this.value || null;
}
- on_upload_complete(attachment) {
+ async on_upload_complete(attachment) {
if(this.frm) {
- this.parse_validate_and_set_in_model(attachment.file_url);
+ await this.parse_validate_and_set_in_model(attachment.file_url);
this.frm.attachments.update_attachment(attachment);
this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save();
}
diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js
index 4e66ed6642..a509af4121 100644
--- a/frappe/public/js/frappe/form/controls/autocomplete.js
+++ b/frappe/public/js/frappe/form/controls/autocomplete.js
@@ -166,6 +166,9 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
}
parse_options(options) {
+ if (typeof options === 'string' && options[0] === '[') {
+ options = frappe.utils.parse_json(options);
+ }
if (typeof options === 'string') {
options = options.split('\n');
}
diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js
index d440874f36..0070d384d7 100644
--- a/frappe/public/js/frappe/form/footer/form_timeline.js
+++ b/frappe/public/js/frappe/form/footer/form_timeline.js
@@ -454,7 +454,10 @@ class FormTimeline extends BaseTimeline {
let edit_box = this.make_editable(edit_wrapper);
let content_wrapper = comment_wrapper.find('.content');
let more_actions_wrapper = comment_wrapper.find('.more-actions');
- if (frappe.model.can_delete("Comment")) {
+ if (frappe.model.can_delete("Comment") && (
+ frappe.session.user == doc.owner ||
+ frappe.user.has_role("System Manager")
+ )) {
const delete_option = $(`