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 5f82abac1f..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
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index cb22f581c6..dc6d14b451 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -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/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_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 a4eca64d8d..1251a323d3 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -142,8 +142,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)):
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 a20ffe17a5..eb3e33d39c 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -99,8 +99,13 @@ class PostgresDatabase(Database):
return db_size[0].get('database_size')
# pylint: disable=W0221
- def sql(self, query, *args, **kwargs):
- return super(PostgresDatabase, self).sql(modify_query(query), *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
@@ -333,10 +338,45 @@ def modify_query(query):
if re.search('from tab', 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/form/load.py b/frappe/desk/form/load.py
index dba84e5175..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
@@ -355,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/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/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/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/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..3564b1ae11 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)
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/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/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index 56e909dd0c..7ec6677c7f 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -246,10 +246,12 @@ frappe.ui.form.Form = class FrappeForm {
var me = this;
// on main doc
- frappe.model.on(me.doctype, "*", function(fieldname, value, doc) {
+ frappe.model.on(me.doctype, "*", function(fieldname, value, doc, skip_dirty_trigger=false) {
// set input
- if(doc.name===me.docname) {
- me.dirty();
+ if (cstr(doc.name) === me.docname) {
+ if (!skip_dirty_trigger) {
+ me.dirty();
+ }
let field = me.fields_dict[fieldname];
field && field.refresh(fieldname);
@@ -953,10 +955,12 @@ frappe.ui.form.Form = class FrappeForm {
this.toolbar.set_primary_action();
}
- disable_save() {
+ disable_save(set_dirty=false) {
// IMPORTANT: this function should be called in refresh event
this.save_disabled = true;
this.toolbar.current_status = null;
+ // field changes should make form dirty
+ this.set_dirty = set_dirty;
this.page.clear_primary_action();
}
@@ -1447,7 +1451,7 @@ frappe.ui.form.Form = class FrappeForm {
return doc;
}
- set_value(field, value, if_missing) {
+ set_value(field, value, if_missing, skip_dirty_trigger=false) {
var me = this;
var _set = function(f, v) {
var fieldobj = me.fields_dict[f];
@@ -1467,7 +1471,7 @@ frappe.ui.form.Form = class FrappeForm {
me.refresh_field(f);
return Promise.resolve();
} else {
- return frappe.model.set_value(me.doctype, me.doc.name, f, v);
+ return frappe.model.set_value(me.doctype, me.doc.name, f, v, me.fieldtype, skip_dirty_trigger);
}
}
} else {
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index e620caa244..7bbe4b123a 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -35,7 +35,7 @@ export default class Grid {
&& this.frm.meta.__form_grid_templates[this.df.fieldname]) {
this.template = this.frm.meta.__form_grid_templates[this.df.fieldname];
}
-
+ this.filter = {};
this.is_grid = true;
this.debounced_refresh = this.refresh.bind(this);
this.debounced_refresh = frappe.utils.debounce(this.debounced_refresh, 100);
@@ -274,6 +274,8 @@ export default class Grid {
}
make_head() {
+ if (this.prevent_build) return;
+
// labels
if (this.header_row) {
$(this.parent).find(".grid-heading-row .grid-row").remove();
@@ -286,12 +288,42 @@ export default class Grid {
grid: this,
configure_columns: true
});
+
+ this.header_search = new GridRow({
+ parent: $(this.parent).find(".grid-heading-row"),
+ parent_df: this.df,
+ docfields: this.docfields,
+ frm: this.frm,
+ grid: this,
+ show_search: true
+ });
+
+ Object.keys(this.filter).length !== 0 &&
+ this.update_search_columns();
+ }
+
+ update_search_columns() {
+ for (const field in this.filter) {
+ if (this.filter[field] && !this.header_search.search_columns[field]) {
+ delete this.filter[field];
+ this.data = this.get_data(Object.keys(this.filter).length !== 0);
+ break;
+ }
+
+ if (this.filter[field] && this.filter[field].value) {
+ let $input = this.header_search.row_index.find('input');
+ if (field && field !== 'row-index') {
+ $input = this.header_search.search_columns[field].find('input');
+ }
+ $input.val(this.filter[field].value);
+ }
+ }
}
- refresh(force) {
+ refresh() {
if (this.frm && this.frm.setting_dependency) return;
- this.data = this.get_data();
+ this.data = this.get_data(Object.keys(this.filter).length !== 0);
!this.wrapper && this.make();
let $rows = $(this.parent).find('.rows');
@@ -453,7 +485,7 @@ export default class Grid {
}
make_sortable($rows) {
- new Sortable($rows.get(0), {
+ this.grid_sortable = new Sortable($rows.get(0), {
group: { name: this.df.fieldname },
handle: '.sortable-handle',
draggable: '.grid-row',
@@ -484,14 +516,78 @@ export default class Grid {
$(this.frm.wrapper).trigger("grid-make-sortable", [this.frm]);
}
- get_data() {
- var data = this.frm ?
- this.frm.doc[this.df.fieldname] || []
- : this.df.data || this.get_modal_data();
- // data.sort(function(a, b) { return a.idx - b.idx});
+ get_data(filter_field) {
+ let data = [];
+ if (filter_field) {
+ data = this.get_filtered_data();
+ } else {
+ data = this.frm ?
+ this.frm.doc[this.df.fieldname] || []
+ : this.df.data || this.get_modal_data();
+ }
return data;
}
+ get_filtered_data() {
+ if (!this.frm) return;
+
+ let all_data = this.frm.doc[this.df.fieldname];
+
+ for (const field in this.filter) {
+ all_data = all_data.filter(data => {
+ let {df, value} = this.filter[field];
+ return this.get_data_based_on_fieldtype(df, data, value.toLowerCase());
+ });
+ }
+
+ return all_data;
+ }
+
+ get_data_based_on_fieldtype(df, data, value) {
+ let fieldname = df.fieldname;
+ let fieldtype = df.fieldtype;
+ let fieldvalue = data[fieldname];
+
+ if (fieldtype === "Check") {
+ value = frappe.utils.string_to_boolean(value);
+ return (Boolean(fieldvalue) === value) && data;
+ } else if (fieldtype === "Sr No" && data.idx.toString().includes(value)) {
+ return data;
+ } else if (fieldtype === "Duration" && fieldvalue) {
+ let formatted_duration = frappe.utils.get_formatted_duration(fieldvalue);
+
+ if (formatted_duration.includes(value)) {
+ return data;
+ }
+ } else if (fieldtype === "Barcode" && fieldvalue) {
+ let barcode = fieldvalue.startsWith('