diff --git a/cypress/integration/list_paging.js b/cypress/integration/list_paging.js
new file mode 100644
index 0000000000..b6832f5a53
--- /dev/null
+++ b/cypress/integration/list_paging.js
@@ -0,0 +1,35 @@
+context('List Paging', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/app/website');
+ return cy.window().its('frappe').then(frappe => {
+ return frappe.call("frappe.tests.ui_test_helpers.create_multiple_todo_records");
+ });
+ });
+
+ it('test load more with count selection buttons', () => {
+ cy.visit('/app/todo/view/report');
+
+ cy.get('.list-paging-area .list-count').should('contain.text', '20 of');
+ cy.get('.list-paging-area .btn-more').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '40 of');
+ cy.get('.list-paging-area .btn-more').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '60 of');
+
+ cy.get('.list-paging-area .btn-group .btn-paging[data-value="100"]').click();
+
+ cy.get('.list-paging-area .list-count').should('contain.text', '100 of');
+ cy.get('.list-paging-area .btn-more').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '200 of');
+ cy.get('.list-paging-area .btn-more').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
+
+ // check if refresh works after load more
+ cy.get('.page-head .standard-actions [data-original-title="Refresh"]').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
+
+ cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click();
+
+ cy.get('.list-paging-area .list-count').should('contain.text', '500 of');
+ });
+});
diff --git a/cypress/integration/number_card.js b/cypress/integration/number_card.js
new file mode 100644
index 0000000000..a01ff1152d
--- /dev/null
+++ b/cypress/integration/number_card.js
@@ -0,0 +1,22 @@
+context('Number Card', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/app/website');
+ });
+
+ it('Check filter populate for child table doctype', () => {
+ cy.visit('/app/number-card/new-number-card-1');
+ cy.get('[data-fieldname="parent_document_type"]').should('have.css', 'display', 'none');
+
+ cy.get_field('document_type', 'Link');
+ cy.fill_field('document_type', 'Workspace Link', 'Link').focus().blur();
+ cy.get_field('document_type', 'Link').should('have.value', 'Workspace Link');
+
+ cy.fill_field('label', 'Test Number Card', 'Data');
+
+ cy.get('[data-fieldname="filters_json"]').click().wait(200);
+ cy.get('.modal-body .filter-action-buttons .add-filter').click();
+ cy.get('.modal-body .fieldname-select-area').click();
+ cy.get('.modal-actions .btn-modal-close').click();
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js
index 6e3a28bbfc..bacbf9c172 100644
--- a/cypress/integration/report_view.js
+++ b/cypress/integration/report_view.js
@@ -13,9 +13,6 @@ context('Report View', () => {
'enabled': 0,
'docstatus': 1 // submit document
}, true);
- return cy.window().its('frappe').then(frappe => {
- return frappe.call("frappe.tests.ui_test_helpers.create_multiple_contact_records");
- });
});
it('Field with enabled allow_on_submit should be editable.', () => {
@@ -43,32 +40,4 @@ context('Report View', () => {
expect(r.message.enabled).to.equals(1);
});
});
-
- it('test load more with count selection buttons', () => {
- cy.visit('/app/contact/view/report');
-
- cy.get('.list-paging-area .list-count').should('contain.text', '20 of');
- cy.get('.list-paging-area .btn-more').click();
- cy.get('.list-paging-area .list-count').should('contain.text', '40 of');
- cy.get('.list-paging-area .btn-more').click();
- cy.get('.list-paging-area .list-count').should('contain.text', '60 of');
-
- cy.get('.list-paging-area .btn-group .btn-paging[data-value="100"]').click();
-
- cy.get('.list-paging-area .list-count').should('contain.text', '100 of');
- cy.get('.list-paging-area .btn-more').click();
- cy.get('.list-paging-area .list-count').should('contain.text', '200 of');
- cy.get('.list-paging-area .btn-more').click();
- cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
-
- // check if refresh works after load more
- cy.get('.page-head .standard-actions [data-original-title="Refresh"]').click();
- cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
-
- cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click();
-
- cy.get('.list-paging-area .list-count').should('contain.text', '500 of');
- cy.get('.list-paging-area .btn-more').click();
- cy.get('.list-paging-area .list-count').should('contain.text', '1000 of');
- });
});
diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js
index 792cb56198..43c01e88fb 100644
--- a/esbuild/esbuild.js
+++ b/esbuild/esbuild.js
@@ -9,7 +9,7 @@ const cliui = require("cliui")();
const chalk = require("chalk");
const html_plugin = require("./frappe-html");
const rtlcss = require('rtlcss');
-const postCssPlugin = require("esbuild-plugin-postcss2").default;
+const postCssPlugin = require("@frappe/esbuild-plugin-postcss2").default;
const ignore_assets = require("./ignore-assets");
const sass_options = require("./sass_options");
const build_cleanup_plugin = require("./build-cleanup");
diff --git a/esbuild/frappe-html.js b/esbuild/frappe-html.js
index 8c4b7ca3d7..9a7edb144d 100644
--- a/esbuild/frappe-html.js
+++ b/esbuild/frappe-html.js
@@ -20,7 +20,8 @@ module.exports = {
.then(content => {
content = scrub_html_template(content);
return {
- contents: `\n\tfrappe.templates['${filename}'] = \`${content}\`;\n`
+ contents: `\n\tfrappe.templates['${filename}'] = \`${content}\`;\n`,
+ watchFiles: [filepath]
};
})
.catch(() => {
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 8a8b70afe3..86f8be35ea 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -35,6 +35,7 @@ from frappe.query_builder import (
patch_query_execute,
patch_query_aggregation,
)
+from frappe.utils.data import cstr
__version__ = '14.0.0-dev'
@@ -214,6 +215,7 @@ def init(site, sites_path=None, new_site=False):
local.cache = {}
local.document_cache = {}
local.meta_cache = {}
+ local.autoincremented_status_map = {site: -1}
local.form_dict = _dict()
local.session = _dict()
local.dev_server = _dev_server
@@ -850,8 +852,7 @@ def set_value(doctype, docname, fieldname, value=None):
return frappe.client.set_value(doctype, docname, fieldname, value)
def get_cached_doc(*args, **kwargs):
- if args and len(args) > 1 and isinstance(args[1], str):
- key = get_document_cache_key(args[0], args[1])
+ if key := can_cache_doc(args):
# local cache
doc = local.document_cache.get(key)
if doc:
@@ -869,8 +870,24 @@ def get_cached_doc(*args, **kwargs):
return doc
+def can_cache_doc(args):
+ """
+ Determine if document should be cached based on get_doc params.
+ Returns cache key if doc can be cached, None otherwise.
+ """
+
+ if not args:
+ return
+
+ doctype = args[0]
+ name = doctype if len(args) == 1 else args[1]
+
+ # Only cache if both doctype and name are strings
+ if isinstance(doctype, str) and isinstance(name, str):
+ return get_document_cache_key(doctype, name)
+
def get_document_cache_key(doctype, name):
- return '{0}::{1}'.format(doctype, name)
+ return f'{doctype}::{name}'
def clear_document_cache(doctype, name):
cache().hdel("last_modified", doctype)
@@ -911,8 +928,7 @@ def get_doc(*args, **kwargs):
doc = frappe.model.document.get_doc(*args, **kwargs)
# set in cache
- if args and len(args) > 1:
- key = get_document_cache_key(args[0], args[1])
+ if key := can_cache_doc(args):
local.document_cache[key] = doc
cache().hset('document_cache', key, doc.as_dict())
@@ -1001,7 +1017,7 @@ def get_module(modulename):
def scrub(txt):
"""Returns sluggified string. e.g. `Sales Order` becomes `sales_order`."""
- return txt.replace(' ', '_').replace('-', '_').lower()
+ return cstr(txt).replace(' ', '_').replace('-', '_').lower()
def unscrub(txt):
"""Returns titlified string. e.g. `sales_order` becomes `Sales Order`."""
diff --git a/frappe/boot.py b/frappe/boot.py
index 524913059c..b5008f778a 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -325,6 +325,7 @@ def get_desk_settings():
def get_notification_settings():
return frappe.get_cached_doc('Notification Settings', frappe.session.user)
+@frappe.whitelist()
def get_link_title_doctypes():
dts = frappe.get_all("DocType", {"show_title_field_in_link": 1})
custom_dts = frappe.get_all(
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
old mode 100755
new mode 100644
index c5d2257d75..b54f369e34
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -1,7 +1,7 @@
# imports - standard imports
import os
-import sys
import shutil
+import sys
# imports - third party imports
import click
@@ -65,11 +65,11 @@ def restore(context, sql_file_path, encryption_key=None, db_root_username=None,
"Restore site database from an sql file"
from frappe.installer import (
_new_site,
- extract_sql_from_archive,
extract_files,
+ extract_sql_from_archive,
is_downgrade,
is_partial,
- validate_database_sql
+ validate_database_sql,
)
from frappe.utils.backups import Backup
if not os.path.exists(sql_file_path):
@@ -207,7 +207,7 @@ def restore(context, sql_file_path, encryption_key=None, db_root_username=None,
@click.option('--encryption-key', help='Backup encryption key')
@pass_context
def partial_restore(context, sql_file_path, verbose, encryption_key=None):
- from frappe.installer import partial_restore, extract_sql_from_archive
+ from frappe.installer import extract_sql_from_archive, partial_restore
from frappe.utils.backups import Backup
if not os.path.exists(sql_file_path):
@@ -545,7 +545,7 @@ def _use(site, sites_path='.'):
def use(site, sites_path='.'):
if os.path.exists(os.path.join(sites_path, site)):
- with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile:
+ with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile:
sitefile.write(site)
print("Current Site set to {}".format(site))
else:
@@ -751,6 +751,7 @@ def set_admin_password(context, admin_password=None, logout_all_sessions=False):
def set_user_password(site, user, password, logout_all_sessions=False):
import getpass
+
from frappe.utils.password import update_password
try:
@@ -881,15 +882,16 @@ def stop_recording(context):
raise SiteNotSpecifiedError
@click.command('ngrok')
+@click.option('--bind-tls', is_flag=True, default=False, help='Returns a reference to the https tunnel.')
@pass_context
-def start_ngrok(context):
+def start_ngrok(context, bind_tls):
from pyngrok import ngrok
site = get_site(context)
frappe.init(site=site)
port = frappe.conf.http_port or frappe.conf.webserver_port
- tunnel = ngrok.connect(addr=str(port), host_header=site)
+ tunnel = ngrok.connect(addr=str(port), host_header=site, bind_tls=bind_tls)
print(f'Public URL: {tunnel.public_url}')
print('Inspect logs at http://localhost:4040')
diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py
index f89f0d8765..475762f39d 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -18,6 +18,7 @@ from urllib.parse import unquote
from frappe.utils.user import is_system_user
from frappe.contacts.doctype.contact.contact import get_contact_name
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule
+from parse import compile
exclude_from_linked_with = True
@@ -114,6 +115,44 @@ class Communication(Document, CommunicationEmailMixin):
frappe.publish_realtime('new_message', self.as_dict(),
user=self.reference_name, after_commit=True)
+ def set_signature_in_email_content(self):
+ """Set sender's User.email_signature or default outgoing's EmailAccount.signature to the email
+ """
+ if not self.content:
+ return
+
+ quill_parser = compile('
{}
')
+ 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 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}"
+ )
+
+ 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,
+ )
- 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 not sender:
- sender = get_formatted_email(frappe.session.user)
+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/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/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/database/database.py b/frappe/database/database.py
index dc9f20d8c2..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)):
@@ -181,7 +179,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
@@ -1028,7 +1026,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/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")
-
- 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
+def format_duration_fields(data: frappe._dict) -> None:
+ for i, col in enumerate(data.columns):
+ if col.get("fieldtype") != "Duration":
+ continue
- 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..5ffde0c37b 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -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/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/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/model/rename_doc.py b/frappe/model/rename_doc.py
index faa3859c91..b4a53e3131 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -43,8 +43,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:
docname = rename_doc(doctype=doctype, old=docname, new=updated_name, merge=merge)
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/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/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/form.js b/frappe/public/js/frappe/form/form.js
index 56e909dd0c..269c54631f 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -248,7 +248,7 @@ frappe.ui.form.Form = class FrappeForm {
// on main doc
frappe.model.on(me.doctype, "*", function(fieldname, value, doc) {
// set input
- if(doc.name===me.docname) {
+ if (cstr(doc.name) === me.docname) {
me.dirty();
let field = me.fields_dict[fieldname];
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index ea90387922..e620caa244 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -501,9 +501,9 @@ export default class Grid {
}
set_column_disp(fieldname, show) {
- if ($.isArray(fieldname)) {
+ if (Array.isArray(fieldname)) {
for (let field of fieldname) {
- this.update_docfield_property(field, "hidden", show);
+ this.update_docfield_property(field, "hidden", show ? 0 : 1);
this.set_editable_grid_column_disp(field, show);
}
} else {
diff --git a/frappe/public/js/frappe/form/linked_with.js b/frappe/public/js/frappe/form/linked_with.js
index 20db7bdb7c..c47a6e0c86 100644
--- a/frappe/public/js/frappe/form/linked_with.js
+++ b/frappe/public/js/frappe/form/linked_with.js
@@ -1,9 +1,8 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// MIT License. See license.txt
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+// MIT License. See LICENSE
frappe.ui.form.LinkedWith = class LinkedWith {
-
constructor(opts) {
$.extend(this, opts);
}
@@ -21,29 +20,23 @@ frappe.ui.form.LinkedWith = class LinkedWith {
}
make_dialog() {
-
this.dialog = new frappe.ui.Dialog({
title: __("Linked With")
});
this.dialog.on_page_show = () => {
- // execute ajax calls sequentially
- // 1. get linked doctypes
- // 2. load all doctypes
- // 3. load linked docs
- this.get_linked_doctypes()
- .then(() => this.load_doctypes())
- .then(() => this.links_not_permitted_or_missing())
- .then(() => this.get_linked_docs())
- .then(() => this.make_html());
+ frappe.xcall(
+ "frappe.desk.form.linked_with.get",
+ {"doctype": cur_frm.doctype, "docname": cur_frm.docname},
+ ).then(r => {
+ this.frm.__linked_docs = r;
+ }).then(() => this.make_html());
};
}
make_html() {
- const linked_docs = this.frm.__linked_docs;
-
let html = '';
-
+ const linked_docs = this.frm.__linked_docs;
const linked_doctypes = Object.keys(linked_docs);
if (linked_doctypes.length === 0) {
@@ -63,88 +56,6 @@ frappe.ui.form.LinkedWith = class LinkedWith {
$(this.dialog.body).html(html);
}
- load_doctypes() {
- const already_loaded = Object.keys(locals.DocType);
- let doctypes_to_load = [];
-
- if (this.frm.__linked_doctypes) {
- doctypes_to_load =
- Object.keys(this.frm.__linked_doctypes)
- .filter(doctype => !already_loaded.includes(doctype));
- }
-
- // load all doctypes asynchronously using with_doctype
- const promises = doctypes_to_load.map(dt => {
- return frappe.model.with_doctype(dt, () => {
- if(frappe.listview_settings[dt]) {
- // add additional fields to __linked_doctypes
- this.frm.__linked_doctypes[dt].add_fields =
- frappe.listview_settings[dt].add_fields;
- }
- });
- });
-
- return Promise.all(promises);
- }
-
- links_not_permitted_or_missing() {
- let links = null;
-
- if (this.frm.__linked_doctypes) {
- links =
- Object.keys(this.frm.__linked_doctypes)
- .filter(frappe.model.can_get_report);
- }
-
- let flag;
- if(!links) {
- $(this.dialog.body).html(`${this.frm.__linked_doctypes
- ? __("Not enough permission to see links")
- : __("Not Linked to any record")}`);
- flag = true;
- }
- flag = false;
-
- // reject Promise if not_permitted or missing
- return new Promise(
- (resolve, reject) => flag ? reject() : resolve()
- );
- }
-
- get_linked_doctypes() {
- return new Promise((resolve) => {
- if (this.frm.__linked_doctypes) {
- resolve();
- }
-
- frappe.call({
- method: "frappe.desk.form.linked_with.get_linked_doctypes",
- args: {
- doctype: this.frm.doctype
- },
- callback: (r) => {
- this.frm.__linked_doctypes = r.message;
- resolve();
- }
- });
- });
- }
-
- get_linked_docs() {
- return frappe.call({
- method: "frappe.desk.form.linked_with.get_linked_docs",
- args: {
- doctype: this.frm.doctype,
- name: this.frm.docname,
- linkinfo: this.frm.__linked_doctypes,
- for_doctype: this.for_doctype
- },
- callback: (r) => {
- this.frm.__linked_docs = r.message || {};
- }
- });
- }
-
make_doc_head(heading) {
return `
diff --git a/frappe/public/js/frappe/form/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js
index 538534e5cf..0713d5dc43 100644
--- a/frappe/public/js/frappe/form/sidebar/attachments.js
+++ b/frappe/public/js/frappe/form/sidebar/attachments.js
@@ -44,8 +44,17 @@ frappe.ui.form.Attachments = class Attachments {
// add attachment objects
var attachments = this.get_attachments();
if(attachments.length) {
- attachments.forEach(function(attachment) {
- me.add_attachment(attachment)
+ let exists = {};
+ let unique_attachments = attachments.filter(attachment => {
+ return Object.prototype.hasOwnProperty.call(
+ exists,
+ attachment.file_name
+ )
+ ? false
+ : (exists[attachment.file_name] = true);
+ });
+ unique_attachments.forEach(attachment => {
+ me.add_attachment(attachment);
});
} else {
this.attachments_label.removeClass("has-attachments");
@@ -75,7 +84,19 @@ frappe.ui.form.Attachments = class Attachments {
remove_action = function(target_id) {
frappe.confirm(__("Are you sure you want to delete the attachment?"),
function() {
- me.remove_attachment(target_id);
+ let target_attachment = me
+ .get_attachments()
+ .find(attachment => attachment.name === target_id);
+ let to_be_removed = me
+ .get_attachments()
+ .filter(
+ attachment =>
+ attachment.file_name ===
+ target_attachment.file_name
+ );
+ to_be_removed.forEach(attachment =>
+ me.remove_attachment(attachment.name)
+ );
}
);
return false;
diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js
index 75063cc53f..d5ee82acce 100644
--- a/frappe/public/js/frappe/list/base_list.js
+++ b/frappe/public/js/frappe/list/base_list.js
@@ -760,6 +760,10 @@ class FilterArea {
const doctype_fields = this.list_view.meta.fields;
const title_field = this.list_view.meta.title_field;
+ const has_existing_filters = (
+ this.list_view.filters
+ && this.list_view.filters.length > 0
+ );
fields = fields.concat(
doctype_fields
@@ -794,13 +798,17 @@ class FilterArea {
options = options.join("\n");
}
}
- let default_value =
- fieldtype === "Link"
- ? frappe.defaults.get_user_default(options)
- : null;
+
+ let default_value;
+
+ if (fieldtype === "Link" && !has_existing_filters) {
+ default_value = frappe.defaults.get_user_default(options);
+ }
+
if (["__default", "__global"].includes(default_value)) {
default_value = null;
}
+
return {
fieldtype: fieldtype,
label: __(df.label),
diff --git a/frappe/public/js/frappe/list/list_factory.js b/frappe/public/js/frappe/list/list_factory.js
index acad85fdcb..ef48af4937 100644
--- a/frappe/public/js/frappe/list/list_factory.js
+++ b/frappe/public/js/frappe/list/list_factory.js
@@ -6,8 +6,8 @@ frappe.provide('frappe.views.list_view');
window.cur_list = null;
frappe.views.ListFactory = class ListFactory extends frappe.views.Factory {
make (route) {
- var me = this;
- var doctype = route[1];
+ const me = this;
+ const doctype = route[1];
// List / Gantt / Kanban / etc
// File is a special view
@@ -21,60 +21,58 @@ frappe.views.ListFactory = class ListFactory extends frappe.views.Factory {
}
frappe.provide('frappe.views.list_view.' + doctype);
- const page_name = frappe.get_route_str();
-
- if (!frappe.views.list_view[page_name]) {
- frappe.views.list_view[page_name] = new view_class({
- doctype: doctype,
- parent: me.make_page(true, page_name)
- });
- } else {
- frappe.container.change_to(page_name);
- }
- me.set_cur_list();
+ frappe.views.list_view[me.page_name] = new view_class({
+ doctype: doctype,
+ parent: me.make_page(true, me.page_name)
+ });
+ me.set_cur_list();
}
- show() {
+ before_show() {
if (this.re_route_to_view()) {
- return;
+ return false;
}
+
this.set_module_breadcrumb();
- super.show();
+ }
+
+ on_show() {
this.set_cur_list();
- cur_list && cur_list.show();
+ if (cur_list) cur_list.show();
}
re_route_to_view() {
- var route = frappe.get_route();
- var doctype = route[1];
- var last_route = frappe.route_history.slice(-2)[0];
- if (route[0] === 'List' && route.length === 2 && frappe.views.list_view[doctype]) {
- if(last_route && last_route[0]==='List' && last_route[1]===doctype) {
- // last route same as this route, so going back.
- // this happens because /app/List/Item will redirect to /app/List/Item/List
- // while coming from back button, the last 2 routes will be same, so
- // we know user is coming in the reverse direction (via back button)
+ const doctype = this.route[1];
+ const last_route = frappe.route_history.slice(-2)[0];
+ if (
+ this.route[0] === 'List' &&
+ this.route.length === 2 &&
+ frappe.views.list_view[doctype] &&
+ last_route &&
+ last_route[0] === 'List' &&
+ last_route[1] === doctype
+ ) {
+ // last route same as this route, so going back.
+ // this happens because /app/List/Item will redirect to /app/List/Item/List
+ // while coming from back button, the last 2 routes will be same, so
+ // we know user is coming in the reverse direction (via back button)
- // example:
- // Step 1: /app/List/Item redirects to /app/List/Item/List
- // Step 2: User hits "back" comes back to /app/List/Item
- // Step 3: Now we cannot send the user back to /app/List/Item/List so go back one more step
- window.history.go(-1);
- return true;
- } else {
- return false;
- }
+ // example:
+ // Step 1: /app/List/Item redirects to /app/List/Item/List
+ // Step 2: User hits "back" comes back to /app/List/Item
+ // Step 3: Now we cannot send the user back to /app/List/Item/List so go back one more step
+ window.history.go(-1);
+ return true;
}
}
set_module_breadcrumb() {
if (frappe.route_history.length > 1) {
- var prev_route = frappe.route_history[frappe.route_history.length - 2];
+ const prev_route = frappe.route_history[frappe.route_history.length - 2];
if (prev_route[0] === 'modules') {
- var doctype = frappe.get_route()[1],
- module = prev_route[1];
+ const doctype = this.route[1], module = prev_route[1];
if (frappe.module_links[module] && frappe.module_links[module].includes(doctype)) {
// save the last page from the breadcrumb was accessed
frappe.breadcrumbs.set_doctype_module(doctype, module);
@@ -84,10 +82,8 @@ frappe.views.ListFactory = class ListFactory extends frappe.views.Factory {
}
set_cur_list() {
- var route = frappe.get_route();
- var page_name = frappe.get_route_str();
- cur_list = frappe.views.list_view[page_name];
- if (cur_list && cur_list.doctype !== route[1]) {
+ cur_list = frappe.views.list_view[this.page_name];
+ if (cur_list && cur_list.doctype !== this.route[1]) {
// changing...
window.cur_list = null;
}
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 5cfc7c75d4..069f353368 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -83,32 +83,15 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
this.sort_by = this.view_user_settings.sort_by || "modified";
this.sort_order = this.view_user_settings.sort_order || "desc";
- // set filters from user_settings or list_settings
- if (
- this.view_user_settings.filters &&
- this.view_user_settings.filters.length
- ) {
- // Priority 1: user_settings
- const saved_filters = this.view_user_settings.filters;
- this.filters = this.validate_filters(saved_filters);
- } else {
- // Priority 2: filters in listview_settings
- this.filters = (this.settings.filters || []).map((f) => {
- if (f.length === 3) {
- f = [this.doctype, f[0], f[1], f[2]];
- }
- return f;
- });
- }
-
// build menu items
this.menu_items = this.menu_items.concat(this.get_menu_items());
+ // set filters from view_user_settings or list_settings
if (
this.view_user_settings.filters &&
this.view_user_settings.filters.length
) {
- // Priority 1: saved filters
+ // Priority 1: view_user_settings
const saved_filters = this.view_user_settings.filters;
this.filters = this.validate_filters(saved_filters);
} else {
@@ -932,7 +915,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return this.settings.get_form_link(doc);
}
- const docname = doc.name.match(/[%'"#\s]/)
+ const docname = cstr(doc.name).match(/[%'"#\s]/)
? encodeURIComponent(doc.name)
: doc.name;
@@ -1757,8 +1740,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
const docnames = this.get_checked_items(true).map(
(docname) => docname.toString()
);
+ let message = __("Delete {0} item permanently?", [docnames.length], "Title of confirmation dialog");
+ if (docnames.length > 1) {
+ message = __("Delete {0} items permanently?", [docnames.length], "Title of confirmation dialog");
+ }
frappe.confirm(
- __("Delete {0} items permanently?", [docnames.length], "Title of confirmation dialog"),
+ message,
() => {
this.disable_list_update = true;
bulk_operations.delete(docnames, () => {
diff --git a/frappe/public/js/frappe/microtemplate.js b/frappe/public/js/frappe/microtemplate.js
index 151d008d3e..176862e233 100644
--- a/frappe/public/js/frappe/microtemplate.js
+++ b/frappe/public/js/frappe/microtemplate.js
@@ -138,6 +138,7 @@ frappe.render_tree = function(opts) {
opts.base_url = frappe.urllib.get_base_url();
opts.landscape = false;
opts.print_css = frappe.boot.print_css;
+ opts.print_format_css_path = frappe.assets.bundled_asset('print_format.bundle.css');
var tree = frappe.render_template("print_tree", opts);
var w = window.open();
diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js
index 62f8ec2f1e..fe959b259d 100644
--- a/frappe/public/js/frappe/model/model.js
+++ b/frappe/public/js/frappe/model/model.js
@@ -577,13 +577,15 @@ $.extend(frappe.model, {
},
delete_doc: function(doctype, docname, callback) {
- var title = docname;
- var title_field = frappe.get_meta(doctype).title_field;
+ let title = docname;
+ const title_field = frappe.get_meta(doctype).title_field;
if (frappe.get_meta(doctype).autoname == "hash" && title_field) {
- var title = frappe.model.get_value(doctype, docname, title_field);
- title += " (" + docname + ")";
+ const value = frappe.model.get_value(doctype, docname, title_field);
+ if (value) {
+ title = `${value} (${docname})`;
+ }
}
- frappe.confirm(__("Permanently delete {0}?", [title]), function() {
+ frappe.confirm(__("Permanently delete {0}?", [title.bold()]), function() {
return frappe.call({
method: 'frappe.client.delete',
args: {
diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js
index f0d03f0743..504c534665 100644
--- a/frappe/public/js/frappe/ui/messages.js
+++ b/frappe/public/js/frappe/ui/messages.js
@@ -134,7 +134,17 @@ frappe.msgprint = function(msg, title, is_minimizable) {
}
if(data.message instanceof Array) {
- data.message.forEach(function(m) {
+ let messages = data.message;
+ const exceptions = messages
+ .map(m => JSON.parse(m))
+ .filter(m => m.raise_exception);
+
+ // only show exceptions if any exceptions exist
+ if (exceptions.length) {
+ messages = exceptions;
+ }
+
+ messages.forEach(function(m) {
frappe.msgprint(m);
});
return;
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index ff55f5578f..6971d3bc20 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -196,6 +196,15 @@ Object.assign(frappe.utils, {
}
return true;
},
+ parse_json: function(str) {
+ let parsed_json = '';
+ try {
+ parsed_json = JSON.parse(str);
+ } catch (e) {
+ return str;
+ }
+ return parsed_json;
+ },
strip_whitespace: function(html) {
return (html || "").replace(/