diff --git a/cypress/fixtures/child_table_doctype_1.js b/cypress/fixtures/child_table_doctype_1.js
new file mode 100644
index 0000000000..4657d63e2e
--- /dev/null
+++ b/cypress/fixtures/child_table_doctype_1.js
@@ -0,0 +1,59 @@
+export default {
+ name: "Child Table Doctype 1",
+ actions: [],
+ custom: 1,
+ autoname: "format: Test-{####}",
+ creation: "2022-02-09 20:15:21.242213",
+ doctype: "DocType",
+ editable_grid: 1,
+ engine: "InnoDB",
+ fields: [
+ {
+ fieldname: "data",
+ fieldtype: "Data",
+ in_list_view: 1,
+ label: "Data"
+ },
+ {
+ fieldname: "barcode",
+ fieldtype: "Barcode",
+ in_list_view: 1,
+ label: "Barcode"
+ },
+ {
+ fieldname: "check",
+ fieldtype: "Check",
+ in_list_view: 1,
+ label: "Check"
+ },
+ {
+ fieldname: "rating",
+ fieldtype: "Rating",
+ in_list_view: 1,
+ label: "Rating"
+ },
+ {
+ fieldname: "duration",
+ fieldtype: "Duration",
+ in_list_view: 1,
+ label: "Duration"
+ },
+ {
+ fieldname: "date",
+ fieldtype: "Date",
+ in_list_view: 1,
+ label: "Date"
+ }
+ ],
+ links: [],
+ istable: 1,
+ modified: "2022-02-10 12:03:12.603763",
+ modified_by: "Administrator",
+ module: "Custom",
+ naming_rule: "By fieldname",
+ owner: "Administrator",
+ permissions: [],
+ sort_field: 'modified',
+ sort_order: 'ASC',
+ track_changes: 1
+};
\ No newline at end of file
diff --git a/cypress/fixtures/doctype_with_child_table.js b/cypress/fixtures/doctype_with_child_table.js
index bbb2127448..014074b0b5 100644
--- a/cypress/fixtures/doctype_with_child_table.js
+++ b/cypress/fixtures/doctype_with_child_table.js
@@ -20,6 +20,12 @@ export default {
label: "Child Table",
options: "Child Table Doctype",
reqd: 1
+ },
+ {
+ fieldname: "child_table_1",
+ fieldtype: "Table",
+ label: "Child Table 1",
+ options: "Child Table Doctype 1"
}
],
links: [],
diff --git a/cypress/integration/dashboard_links.js b/cypress/integration/dashboard_links.js
index 93d10cf1fd..019de1991d 100644
--- a/cypress/integration/dashboard_links.js
+++ b/cypress/integration/dashboard_links.js
@@ -1,5 +1,6 @@
import doctype_with_child_table from '../fixtures/doctype_with_child_table';
import child_table_doctype from '../fixtures/child_table_doctype';
+import child_table_doctype_1 from '../fixtures/child_table_doctype_1';
import doctype_to_link from '../fixtures/doctype_to_link';
const doctype_to_link_name = doctype_to_link.name;
const child_table_doctype_name = child_table_doctype.name;
@@ -9,6 +10,7 @@ context('Dashboard links', () => {
cy.visit('/login');
cy.login();
cy.insert_doc('DocType', child_table_doctype, true);
+ cy.insert_doc('DocType', child_table_doctype_1, true);
cy.insert_doc('DocType', doctype_with_child_table, true);
cy.insert_doc('DocType', doctype_to_link, true);
return cy.window().its('frappe').then(frappe => {
diff --git a/cypress/integration/grid_search.js b/cypress/integration/grid_search.js
new file mode 100644
index 0000000000..d30545a2e1
--- /dev/null
+++ b/cypress/integration/grid_search.js
@@ -0,0 +1,107 @@
+import doctype_with_child_table from '../fixtures/doctype_with_child_table';
+import child_table_doctype from '../fixtures/child_table_doctype';
+import child_table_doctype_1 from '../fixtures/child_table_doctype_1';
+const doctype_with_child_table_name = doctype_with_child_table.name;
+
+context('Grid Search', () => {
+ before(() => {
+ cy.visit('/login');
+ cy.login();
+ cy.visit('/app/website');
+ cy.insert_doc('DocType', child_table_doctype, true);
+ cy.insert_doc('DocType', child_table_doctype_1, true);
+ cy.insert_doc('DocType', doctype_with_child_table, true);
+ return cy.window().its('frappe').then(frappe => {
+ return frappe.xcall("frappe.tests.ui_test_helpers.insert_doctype_with_child_table_record", {
+ name: doctype_with_child_table_name
+ });
+ });
+ });
+
+ it('Test search row visibility', () => {
+ cy.window().its('frappe').then(frappe => {
+ frappe.model.user_settings.save('Doctype With Child Table', 'GridView', {
+ 'Child Table Doctype 1': [
+ {'fieldname': 'data', 'columns': 2},
+ {'fieldname': 'barcode', 'columns': 1},
+ {'fieldname': 'check', 'columns': 1},
+ {'fieldname': 'rating', 'columns': 2},
+ {'fieldname': 'duration', 'columns': 2},
+ {'fieldname': 'date', 'columns': 2}
+ ]
+ });
+ });
+
+ cy.visit(`/app/doctype-with-child-table/Test Grid Search`);
+
+ cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table');
+ cy.get('@table').find('.grid-row-check:last').click();
+ cy.get('@table').find('.grid-footer').contains('Delete').click();
+ cy.get('.grid-heading-row .grid-row .search').should('not.exist');
+ });
+
+ it('test search field for different fieldtypes', () => {
+ cy.visit(`/app/doctype-with-child-table/Test Grid Search`);
+
+ cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table');
+
+ // Index Column
+ cy.get('@table').find('.grid-heading-row .row-index.search input').type('3');
+ cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2);
+ cy.get('@table').find('.grid-heading-row .row-index.search input').clear();
+
+ // Data Column
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('Data');
+ cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 1);
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').clear();
+
+ // Barcode Column
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('092');
+ cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4);
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').clear();
+
+ // Check Column
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('1');
+ cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 9);
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear();
+
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('0');
+ cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 11);
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear();
+
+ // Rating Column
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').type('3');
+ cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3);
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').clear();
+
+ // Duration Column
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('3d');
+ cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3);
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').clear();
+
+ // Date Column
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('2022');
+ cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4);
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').clear();
+ });
+
+ it('test with multiple filter', () => {
+ cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table');
+
+ // Data Column
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('a');
+ cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 10);
+
+ // Barcode Column
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('0');
+ cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 8);
+
+ // Duration Column
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('d');
+ cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 5);
+
+ // Date Column
+ cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('02-');
+ cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2);
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/list_paging.js b/cypress/integration/list_paging.js
index b6832f5a53..4a59024a7b 100644
--- a/cypress/integration/list_paging.js
+++ b/cypress/integration/list_paging.js
@@ -31,5 +31,8 @@ context('List Paging', () => {
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/cypress/integration/web_form.js b/cypress/integration/web_form.js
index 8346c96313..bd1c7e147e 100644
--- a/cypress/integration/web_form.js
+++ b/cypress/integration/web_form.js
@@ -7,8 +7,8 @@ context('Web Form', () => {
cy.visit('/update-profile');
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
cy.get('.web-form-actions .btn-primary').click();
- cy.wait(500);
- cy.get('.modal.show > .modal-dialog').should('be.visible');
+ cy.wait(5000);
+ cy.url().should('include', '/me');
});
it('Navigate and Submit a MultiStep WebForm', () => {
@@ -16,14 +16,12 @@ context('Web Form', () => {
cy.visit('/update-profile-duplicate');
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
cy.get('.btn-next').should('be.visible');
- cy.get('.web-form-footer .btn-primary').should('not.be.visible');
cy.get('.btn-next').click();
cy.get('.btn-previous').should('be.visible');
cy.get('.btn-next').should('not.be.visible');
- cy.get('.web-form-footer .btn-primary').should('be.visible');
cy.get('.web-form-actions .btn-primary').click();
- cy.wait(500);
- cy.get('.modal.show > .modal-dialog').should('be.visible');
+ cy.wait(5000);
+ cy.url().should('include', '/me');
});
});
});
diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js
index 43c01e88fb..ff31aa4b74 100644
--- a/esbuild/esbuild.js
+++ b/esbuild/esbuild.js
@@ -286,7 +286,7 @@ function get_watch_config() {
notify_redis({ error });
} else {
let {
- assets_json,
+ new_assets_json,
prev_assets_json
} = await write_assets_json(result.metafile);
@@ -294,7 +294,7 @@ function get_watch_config() {
if (prev_assets_json) {
changed_files = get_rebuilt_assets(
prev_assets_json,
- assets_json
+ new_assets_json
);
let timestamp = new Date().toLocaleTimeString();
@@ -384,6 +384,7 @@ let prev_assets_json;
let curr_assets_json;
async function write_assets_json(metafile) {
+ let rtl = false;
prev_assets_json = curr_assets_json;
let out = {};
for (let output in metafile.outputs) {
@@ -392,13 +393,14 @@ async function write_assets_json(metafile) {
if (info.entryPoint) {
let key = path.basename(info.entryPoint);
if (key.endsWith('.css') && asset_path.includes('/css-rtl/')) {
+ rtl = true;
key = `rtl_${key}`;
}
out[key] = asset_path;
}
}
- let assets_json_path = path.resolve(assets_path, "assets.json");
+ let assets_json_path = path.resolve(assets_path, `assets${rtl?'-rtl':''}.json`);
let assets_json;
try {
assets_json = await fs.promises.readFile(assets_json_path, "utf-8");
@@ -407,21 +409,21 @@ async function write_assets_json(metafile) {
}
assets_json = JSON.parse(assets_json);
// update with new values
- assets_json = Object.assign({}, assets_json, out);
- curr_assets_json = assets_json;
+ let new_assets_json = Object.assign({}, assets_json, out);
+ curr_assets_json = new_assets_json;
await fs.promises.writeFile(
assets_json_path,
- JSON.stringify(assets_json, null, 4)
+ JSON.stringify(new_assets_json, null, 4)
);
- await update_assets_json_in_cache(assets_json);
+ await update_assets_json_in_cache();
return {
- assets_json,
+ new_assets_json,
prev_assets_json
};
}
-function update_assets_json_in_cache(assets_json) {
+function update_assets_json_in_cache() {
// update assets_json cache in redis, so that it can be read directly by python
return new Promise(resolve => {
let client = get_redis_subscriber("redis_cache");
@@ -429,7 +431,7 @@ function update_assets_json_in_cache(assets_json) {
client.on("error", _ => {
log_warn("Cannot connect to redis_cache to update assets_json");
});
- client.set("assets_json", JSON.stringify(assets_json), err => {
+ client.del("assets_json", err => {
client.unref();
resolve();
});
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 1f5c7811a0..8cbe60fb43 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -36,6 +36,7 @@ from frappe.query_builder import (
patch_query_execute,
patch_query_aggregation,
)
+from frappe.utils.data import cstr
__version__ = '14.0.0-dev'
@@ -215,6 +216,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
@@ -858,8 +860,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:
@@ -877,8 +878,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)
@@ -919,8 +936,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())
@@ -970,8 +986,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
def delete_doc_if_exists(doctype, name, force=0):
"""Delete document if exists."""
- if db.exists(doctype, name):
- delete_doc(doctype, name, force=force)
+ delete_doc(doctype, name, force=force, ignore_missing=True)
def reload_doctype(doctype, force=False, reset_permissions=False):
"""Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files."""
@@ -1009,7 +1024,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`."""
@@ -1244,9 +1259,10 @@ def get_newargs(fn, kwargs):
if hasattr(fn, 'fnargs'):
fnargs = fn.fnargs
else:
- fnargs = inspect.getfullargspec(fn).args
- fnargs.extend(inspect.getfullargspec(fn).kwonlyargs)
- varkw = inspect.getfullargspec(fn).varkw
+ fullargspec = inspect.getfullargspec(fn)
+ fnargs = fullargspec.args
+ fnargs.extend(fullargspec.kwonlyargs)
+ varkw = fullargspec.varkw
newargs = {}
for a in kwargs:
@@ -1258,7 +1274,7 @@ def get_newargs(fn, kwargs):
return newargs
-def make_property_setter(args, ignore_validate=False, validate_fields_for_doctype=True):
+def make_property_setter(args, ignore_validate=False, validate_fields_for_doctype=True, is_system_generated=True):
"""Create a new **Property Setter** (for overriding DocType and DocField properties).
If doctype is not specified, it will create a property setter for all fields with the
@@ -1289,6 +1305,7 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp
'property': args.property,
'value': args.value,
'property_type': args.property_type or "Data",
+ 'is_system_generated': is_system_generated,
'__islocal': 1
})
ps.flags.ignore_validate = ignore_validate
@@ -1456,7 +1473,7 @@ def get_list(doctype, *args, **kwargs):
:param fields: List of fields or `*`.
:param filters: List of filters (see example).
:param order_by: Order By e.g. `modified desc`.
- :param limit_page_start: Start results at record #. Default 0.
+ :param limit_start: Start results at record #. Default 0.
:param limit_page_length: No of records in the page. Default 20.
Example usage:
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index b54f369e34..e788c7ec4d 100644
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -677,7 +677,9 @@ def _drop_site(site, db_root_username=None, db_root_password=None, archived_site
try:
if not no_backup:
- scheduled_backup(ignore_files=False, force=True)
+ click.secho(f"Taking backup of {site}", fg="green")
+ odb = scheduled_backup(ignore_files=False, force=True, verbose=True)
+ odb.print_summary()
except Exception as err:
if force:
pass
@@ -692,6 +694,7 @@ def _drop_site(site, db_root_username=None, db_root_password=None, archived_site
click.echo("\n".join(messages))
sys.exit(1)
+ click.secho("Dropping site database and user", fg="green")
drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password)
archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites')
@@ -725,7 +728,7 @@ def move(dest_dir, site):
@click.command('set-password')
@click.argument('user')
@click.argument('password', required=False)
-@click.option('--logout-all-sessions', help='Logout from all sessions', is_flag=True, default=False)
+@click.option('--logout-all-sessions', help='Log out from all sessions', is_flag=True, default=False)
@pass_context
def set_password(context, user, password=None, logout_all_sessions=False):
"Set password for a user on a site"
@@ -738,7 +741,7 @@ def set_password(context, user, password=None, logout_all_sessions=False):
@click.command('set-admin-password')
@click.argument('admin-password', required=False)
-@click.option('--logout-all-sessions', help='Logout from all sessions', is_flag=True, default=False)
+@click.option('--logout-all-sessions', help='Log out from all sessions', is_flag=True, default=False)
@pass_context
def set_admin_password(context, admin_password=None, logout_all_sessions=False):
"Set Administrator password for a site"
diff --git a/frappe/core/doctype/activity_log/activity_log.py b/frappe/core/doctype/activity_log/activity_log.py
index 69565a2c2a..70d4ca3ffe 100644
--- a/frappe/core/doctype/activity_log/activity_log.py
+++ b/frappe/core/doctype/activity_log/activity_log.py
@@ -1,15 +1,14 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, Frappe Technologies and contributors
+# Copyright (c) 2022, Frappe Technologies and contributors
# License: MIT. See LICENSE
-from frappe import _
-from frappe.utils import get_fullname, now
-from frappe.model.document import Document
-from frappe.core.utils import set_timeline_doc
import frappe
+from frappe import _
+from frappe.core.utils import set_timeline_doc
+from frappe.model.document import Document
from frappe.query_builder import DocType, Interval
from frappe.query_builder.functions import Now
-from pypika.terms import PseudoColumn
+from frappe.utils import get_fullname, now
+
class ActivityLog(Document):
def before_insert(self):
@@ -49,5 +48,5 @@ def clear_activity_logs(days=None):
days = 90
doctype = DocType("Activity Log")
frappe.db.delete(doctype, filters=(
- doctype.creation < PseudoColumn(f"({Now() - Interval(days=days)})")
- ))
\ No newline at end of file
+ doctype.creation < (Now() - Interval(days=days))
+ ))
diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py
index d933c2f494..8012d8facf 100644
--- a/frappe/core/doctype/communication/test_communication.py
+++ b/frappe/core/doctype/communication/test_communication.py
@@ -4,8 +4,8 @@ import unittest
from urllib.parse import quote
import frappe
-from frappe.email.doctype.email_queue.email_queue import EmailQueue
from frappe.core.doctype.communication.communication import get_emails
+from frappe.email.doctype.email_queue.email_queue import EmailQueue
test_records = frappe.get_test_records('Communication')
@@ -202,7 +202,7 @@ class TestCommunication(unittest.TestCase):
self.assertIn(("Note", note.name), doc_links)
- def parse_emails(self):
+ def test_parse_emails(self):
emails = get_emails(
[
'comm_recipient+DocType+DocName@example.com',
diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py
index 79570d5048..9f1492af19 100644
--- a/frappe/core/doctype/data_export/exporter.py
+++ b/frappe/core/doctype/data_export/exporter.py
@@ -324,7 +324,7 @@ class DataExporter:
d = doc.copy()
meta = frappe.get_meta(dt)
if self.all_doctypes:
- d.name = '"'+ d.name+'"'
+ d.name = f'"{d.name}"'
if len(rows) < rowidx + 1:
rows.append([""] * (len(self.columns) + 1))
diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js
index f250a6a109..88cc5577a6 100644
--- a/frappe/core/doctype/doctype/doctype.js
+++ b/frappe/core/doctype/doctype/doctype.js
@@ -61,6 +61,13 @@ frappe.ui.form.on('DocType', {
frm.events.set_naming_rule_description(frm);
},
+ istable: (frm) => {
+ if (frm.doc.istable && frm.is_new()) {
+ frm.set_value('autoname', 'autoincrement');
+ frm.set_value('allow_rename', 0);
+ }
+ },
+
naming_rule: function(frm) {
// set the "autoname" property based on naming_rule
if (frm.doc.naming_rule && !frm.__from_autoname) {
@@ -70,6 +77,10 @@ frappe.ui.form.on('DocType', {
if (frm.doc.naming_rule=='Set by user') {
frm.set_value('autoname', 'Prompt');
+ } else if (frm.doc.naming_rule === 'Autoincrement') {
+ frm.set_value('autoname', 'autoincrement');
+ // set allow rename to be false when using autoincrement
+ frm.set_value('allow_rename', 0);
} else if (frm.doc.naming_rule=='By fieldname') {
frm.set_value('autoname', 'field:');
} else if (frm.doc.naming_rule=='By "Naming Series" field') {
@@ -91,6 +102,7 @@ frappe.ui.form.on('DocType', {
set_naming_rule_description(frm) {
let naming_rule_description = {
'Set by user': '',
+ 'Autoincrement': 'Uses Auto Increment feature of database. WARNING: After using this option, any other naming option will not be accessible.',
'By fieldname': 'Format: field:[fieldname]. Valid fieldname must exist',
'By "Naming Series" field': 'Format: naming_series:[fieldname]. Fieldname called naming_series must exist',
'Expression': 'Format: format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.',
@@ -111,6 +123,8 @@ frappe.ui.form.on('DocType', {
frm.__from_autoname = true;
if (frm.doc.autoname.toLowerCase() === 'prompt') {
frm.set_value('naming_rule', 'Set by user');
+ } else if (frm.doc.autoname.toLowerCase() === 'autoincrement') {
+ frm.set_value('naming_rule', 'Autoincrement');
} else if (frm.doc.autoname.startsWith('field:')) {
frm.set_value('naming_rule', 'By fieldname');
} else if (frm.doc.autoname.startsWith('naming_series:')) {
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 2bba4127bb..8169a59566 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -208,7 +208,7 @@
"label": "Naming"
},
{
- "description": "Naming Options:\n
field:[fieldname] - By Field
naming_series: - By Naming Series (field called naming_series must be present
Prompt - Prompt user for a name
[series] - Series by prefix (separated by a dot); for example PRE.#####
\n
format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
",
+ "description": "Naming Options:\n
field:[fieldname] - By Field
autoincrement - Uses Databases' Auto Increment feature
naming_series: - By Naming Series (field called naming_series must be present
Prompt - Prompt user for a name
[series] - Series by prefix (separated by a dot); for example PRE.#####
\n
format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
",
"fieldname": "autoname",
"fieldtype": "Data",
"label": "Auto Name",
@@ -216,6 +216,7 @@
"oldfieldtype": "Data"
},
{
+ "depends_on": "eval:doc.naming_rule !== \"Autoincrement\"",
"fieldname": "name_case",
"fieldtype": "Select",
"label": "Name Case",
@@ -282,6 +283,7 @@
},
{
"default": "1",
+ "depends_on": "eval:doc.naming_rule !== \"Autoincrement\"",
"fieldname": "allow_rename",
"fieldtype": "Check",
"label": "Allow Rename",
@@ -565,7 +567,7 @@
"fieldtype": "Select",
"label": "Naming Rule",
"length": 40,
- "options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
+ "options": "\nSet by user\nAutoincrement\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
},
{
"fieldname": "migration_hash",
@@ -593,6 +595,7 @@
],
"icon": "fa fa-bolt",
"idx": 6,
+ "index_web_pages_for_search": 1,
"links": [
{
"group": "Views",
@@ -670,10 +673,11 @@
"link_fieldname": "reference_doctype"
}
],
- "modified": "2022-01-07 16:07:06.196534",
+ "modified": "2022-02-15 21:47:16.467217",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@@ -703,5 +707,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 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/file/test_file.py b/frappe/core/doctype/file/test_file.py
index f6310ef454..971cd455f5 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -383,7 +383,7 @@ class TestFile(unittest.TestCase):
}).insert(ignore_permissions=True)
test_file.make_thumbnail()
- self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg')
+ self.assertEqual(test_file.thumbnail_url, '/files/image_small.jpg')
# test web image without extension
test_file = frappe.get_doc({
@@ -400,7 +400,7 @@ class TestFile(unittest.TestCase):
test_file.reload()
test_file.file_url = "/files/image_small.jpg"
test_file.make_thumbnail(suffix="xs", crop=True)
- self.assertEquals(test_file.thumbnail_url, '/files/image_small_xs.jpg')
+ self.assertEqual(test_file.thumbnail_url, '/files/image_small_xs.jpg')
frappe.clear_messages()
test_file.db_set('thumbnail_url', None)
@@ -408,7 +408,7 @@ class TestFile(unittest.TestCase):
test_file.file_url = frappe.utils.get_url('unknown.jpg')
test_file.make_thumbnail(suffix="xs")
self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found")
- self.assertEquals(test_file.thumbnail_url, None)
+ self.assertEqual(test_file.thumbnail_url, None)
def test_file_unzip(self):
file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip')
diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py
index 5c9bc6c265..dec33070c0 100644
--- a/frappe/core/doctype/log_settings/log_settings.py
+++ b/frappe/core/doctype/log_settings/log_settings.py
@@ -7,7 +7,6 @@ from frappe import _
from frappe.model.document import Document
from frappe.query_builder import DocType, Interval
from frappe.query_builder.functions import Now
-from pypika.terms import PseudoColumn
class LogSettings(Document):
@@ -19,7 +18,7 @@ class LogSettings(Document):
def clear_error_logs(self):
table = DocType("Error Log")
frappe.db.delete(table, filters=(
- table.creation < PseudoColumn(f"({Now() - Interval(days=self.clear_error_log_after)})")
+ table.creation < (Now() - Interval(days=self.clear_error_log_after))
))
def clear_activity_logs(self):
diff --git a/frappe/core/doctype/log_settings/test_log_settings.py b/frappe/core/doctype/log_settings/test_log_settings.py
index 40287948fd..f398577665 100644
--- a/frappe/core/doctype/log_settings/test_log_settings.py
+++ b/frappe/core/doctype/log_settings/test_log_settings.py
@@ -1,8 +1,100 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2020, Frappe Technologies and Contributors
+# Copyright (c) 2022, Frappe Technologies and Contributors
# License: MIT. See LICENSE
-# import frappe
-import unittest
-class TestLogSettings(unittest.TestCase):
- pass
+from datetime import datetime
+
+import frappe
+from frappe.utils import now_datetime, add_to_date
+from frappe.core.doctype.log_settings.log_settings import run_log_clean_up
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestLogSettings(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ frappe.db.set_single_value(
+ "Log Settings",
+ {
+ "clear_error_log_after": 1,
+ "clear_activity_log_after": 1,
+ "clear_email_queue_after": 1,
+ },
+ )
+
+ def setUp(self) -> None:
+ if self._testMethodName == "test_delete_logs":
+ self.datetime = frappe._dict()
+ self.datetime.current = now_datetime()
+ self.datetime.past = add_to_date(self.datetime.current, days=-4)
+ setup_test_logs(self.datetime.past)
+
+ def tearDown(self) -> None:
+ if self._testMethodName == "test_delete_logs":
+ del self.datetime
+
+ def test_delete_logs(self):
+ # make sure test data is present
+ activity_log_count = frappe.db.count(
+ "Activity Log", {"creation": ("<=", self.datetime.past)}
+ )
+ error_log_count = frappe.db.count(
+ "Error Log", {"creation": ("<=", self.datetime.past)}
+ )
+ email_queue_count = frappe.db.count(
+ "Email Queue", {"creation": ("<=", self.datetime.past)}
+ )
+
+ self.assertNotEqual(activity_log_count, 0)
+ self.assertNotEqual(error_log_count, 0)
+ self.assertNotEqual(email_queue_count, 0)
+
+ # run clean up job
+ run_log_clean_up()
+
+ # test if logs are deleted
+ activity_log_count = frappe.db.count(
+ "Activity Log", {"creation": ("<", self.datetime.past)}
+ )
+ error_log_count = frappe.db.count(
+ "Error Log", {"creation": ("<", self.datetime.past)}
+ )
+ email_queue_count = frappe.db.count(
+ "Email Queue", {"creation": ("<", self.datetime.past)}
+ )
+
+ self.assertEqual(activity_log_count, 0)
+ self.assertEqual(error_log_count, 0)
+ self.assertEqual(email_queue_count, 0)
+
+
+def setup_test_logs(past: datetime) -> None:
+ activity_log = frappe.get_doc(
+ {
+ "doctype": "Activity Log",
+ "subject": "Test subject",
+ "full_name": "test user2",
+ }
+ ).insert(ignore_permissions=True)
+ activity_log.db_set("creation", past)
+
+ error_log = frappe.get_doc(
+ {
+ "doctype": "Error Log",
+ "method": "test_method",
+ "error": "traceback",
+ }
+ ).insert(ignore_permissions=True)
+ error_log.db_set("creation", past)
+
+ doc1 = frappe.get_doc(
+ {
+ "doctype": "Email Queue",
+ "sender": "test1@example.com",
+ "message": "This is a test email1",
+ "priority": 1,
+ "expose_recipients": "test@receiver.com",
+ }
+ ).insert(ignore_permissions=True)
+ doc1.db_set("creation", past)
diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py
index 9cb40dffd4..bf82a3f684 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -11,7 +11,7 @@ from frappe.modules import make_boilerplate
from frappe.core.doctype.page.page import delete_custom_role
from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles
from frappe.desk.reportview import append_totals_row
-from frappe.utils.safe_exec import safe_exec
+from frappe.utils.safe_exec import safe_exec, check_safe_sql_query
class Report(Document):
@@ -110,8 +110,7 @@ class Report(Document):
if not self.query:
frappe.throw(_("Must specify a Query to run"), title=_('Report Document Error'))
- if not self.query.lower().startswith("select"):
- frappe.throw(_("Query must be a SELECT"), title=_('Report Document Error'))
+ check_safe_sql_query(self.query)
result = [list(t) for t in frappe.db.sql(self.query, filters)]
columns = self.get_columns() or [cstr(c[0]) for c in frappe.db.get_description()]
diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py
index a077956d71..e58d038993 100644
--- a/frappe/core/doctype/report/test_report.py
+++ b/frappe/core/doctype/report/test_report.py
@@ -1,17 +1,19 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
+import textwrap
+
import frappe, json, os
-import unittest
from frappe.desk.query_report import run, save_report, add_total_row
from frappe.desk.reportview import delete_report, save_report as _save_report
from frappe.custom.doctype.customize_form.customize_form import reset_customization
from frappe.core.doctype.user_permission.test_user_permission import create_user
+from frappe.tests.utils import FrappeTestCase
test_records = frappe.get_test_records('Report')
test_dependencies = ['User']
-class TestReport(unittest.TestCase):
+class TestReport(FrappeTestCase):
def test_report_builder(self):
if frappe.db.exists('Report', 'User Activity Report'):
frappe.delete_doc('Report', 'User Activity Report')
@@ -335,3 +337,29 @@ result = [
self.assertEqual(result[-1][0], "Total")
self.assertEqual(result[-1][1], 200)
self.assertEqual(result[-1][2], 150.50)
+
+ def test_cte_in_query_report(self):
+ cte_query = textwrap.dedent("""
+ with enabled_users as (
+ select name
+ from `tabUser`
+ where enabled = 1
+ )
+ select * from enabled_users;
+ """)
+
+ report = frappe.get_doc({
+ "doctype": "Report",
+ "ref_doctype": "User",
+ "report_name": "Enabled Users List",
+ "report_type": "Query Report",
+ "is_standard": "No",
+ "query": cte_query,
+ }).insert()
+
+ if frappe.db.db_type == "mariadb":
+ col, rows = report.execute_query_report(filters={})
+ self.assertEqual(col[0], "name")
+ self.assertGreaterEqual(len(rows), 1)
+ elif frappe.db.db_type == "postgres":
+ self.assertRaises(frappe.PermissionError, report.execute_query_report, filters={})
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.py b/frappe/core/doctype/user/user.py
index d08755f9a8..1ad977547c 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -253,8 +253,8 @@ class User(Document):
self.email_new_password(new_password)
except frappe.OutgoingEmailError:
- print(frappe.get_traceback())
- pass # email server not set, don't send email
+ # email server not set, don't send email
+ frappe.log_error(frappe.get_traceback())
@Document.hook
def validate_reset_password(self):
diff --git a/frappe/core/doctype/user_permission/user_permission.js b/frappe/core/doctype/user_permission/user_permission.js
index 8d5c5c1a23..f6989db5d8 100644
--- a/frappe/core/doctype/user_permission/user_permission.js
+++ b/frappe/core/doctype/user_permission/user_permission.js
@@ -44,8 +44,9 @@ frappe.ui.form.on('User Permission', {
set_applicable_for_constraint: frm => {
frm.toggle_reqd('applicable_for', !frm.doc.apply_to_all_doctypes);
+
if (frm.doc.apply_to_all_doctypes && frm.doc.applicable_for) {
- frm.set_value('applicable_for', null);
+ frm.set_value('applicable_for', null, null, true);
}
},
diff --git a/frappe/core/web_form/edit_profile/edit_profile.json b/frappe/core/web_form/edit_profile/edit_profile.json
index 7072584670..c04e705820 100644
--- a/frappe/core/web_form/edit_profile/edit_profile.json
+++ b/frappe/core/web_form/edit_profile/edit_profile.json
@@ -1,133 +1,159 @@
{
- "accept_payment": 0,
- "allow_comments": 0,
- "allow_delete": 0,
- "allow_edit": 1,
- "allow_incomplete": 0,
- "allow_multiple": 0,
- "allow_print": 0,
- "amount": 0.0,
- "amount_based_on_field": 0,
- "breadcrumbs": "[{\"title\": _(\"My Account\"), \"route\": \"me\"}]",
- "creation": "2016-09-19 05:16:59.242754",
- "doc_type": "User",
- "docstatus": 0,
- "doctype": "Web Form",
- "idx": 0,
- "introduction_text": "",
- "is_standard": 1,
- "login_required": 1,
- "max_attachment_size": 0,
- "modified": "2019-01-28 12:45:17.158069",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "edit-profile",
- "owner": "Administrator",
- "published": 1,
- "route": "update-profile",
- "show_in_grid": 0,
- "show_sidebar": 1,
- "sidebar_items": [],
- "success_message": "Profile updated successfully.",
- "success_url": "/me",
- "title": "Update Profile",
+ "accept_payment": 0,
+ "allow_comments": 0,
+ "allow_delete": 0,
+ "allow_edit": 1,
+ "allow_incomplete": 0,
+ "allow_multiple": 0,
+ "allow_print": 0,
+ "amount": 0.0,
+ "amount_based_on_field": 0,
+ "apply_document_permissions": 0,
+ "breadcrumbs": "[{\"title\": _(\"My Account\"), \"route\": \"me\"}]",
+ "creation": "2016-09-19 05:16:59.242754",
+ "doc_type": "User",
+ "docstatus": 0,
+ "doctype": "Web Form",
+ "idx": 0,
+ "introduction_text": "",
+ "is_multi_step_form": 0,
+ "is_standard": 1,
+ "login_required": 1,
+ "max_attachment_size": 0,
+ "modified": "2022-03-22 15:00:43.456738",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "edit-profile",
+ "owner": "Administrator",
+ "published": 1,
+ "route": "update-profile",
+ "route_to_success_link": 0,
+ "show_attachments": 0,
+ "show_in_grid": 0,
+ "show_sidebar": 0,
+ "sidebar_items": [],
+ "success_message": "Profile updated successfully.",
+ "success_url": "/me",
+ "title": "Update Profile",
"web_form_fields": [
{
- "allow_read_on_all_link_options": 0,
- "fieldname": "first_name",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "First Name",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 1,
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "first_name",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "First Name",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 1,
"show_in_filter": 0
- },
+ },
{
- "allow_read_on_all_link_options": 0,
- "fieldname": "middle_name",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Middle Name (Optional)",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0,
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "middle_name",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Middle Name (Optional)",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
"show_in_filter": 0
- },
+ },
{
- "allow_read_on_all_link_options": 0,
- "fieldname": "last_name",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Last Name",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0,
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "last_name",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Last Name",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
"show_in_filter": 0
- },
+ },
{
- "allow_read_on_all_link_options": 0,
- "description": "",
- "fieldname": "user_image",
- "fieldtype": "Attach Image",
- "hidden": 0,
- "label": "User Image",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0,
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
"show_in_filter": 0
- },
+ },
{
- "allow_read_on_all_link_options": 0,
- "fieldtype": "Section Break",
- "hidden": 0,
- "label": "More Information",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0,
+ "allow_read_on_all_link_options": 0,
+ "description": "",
+ "fieldname": "user_image",
+ "fieldtype": "Attach Image",
+ "hidden": 0,
+ "label": "Profile Picture",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
"show_in_filter": 0
- },
+ },
{
- "allow_read_on_all_link_options": 0,
- "fieldname": "phone",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Phone",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0,
+ "allow_read_on_all_link_options": 0,
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "label": "More Information",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
"show_in_filter": 0
- },
+ },
{
- "allow_read_on_all_link_options": 0,
- "fieldname": "mobile_no",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Mobile Number",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0,
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "phone",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Phone",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
"show_in_filter": 0
- },
+ },
{
- "allow_read_on_all_link_options": 0,
- "description": "",
- "fieldname": "language",
- "fieldtype": "Link",
- "hidden": 0,
- "label": "Language",
- "max_length": 0,
- "max_value": 0,
- "options": "Language",
- "read_only": 0,
- "reqd": 0,
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "mobile_no",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Mobile Number",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "description": "",
+ "fieldname": "language",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "label": "Language",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "Language",
+ "read_only": 0,
+ "reqd": 0,
"show_in_filter": 0
}
]
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index f09829a688..5632da2149 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -7,6 +7,7 @@
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
+ "is_system_generated",
"dt",
"module",
"label",
@@ -425,13 +426,20 @@
"fieldtype": "Link",
"label": "Module (for export)",
"options": "Module Def"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_system_generated",
+ "fieldtype": "Check",
+ "label": "Is System Generated",
+ "read_only": 1
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2022-02-14 15:42:21.885999",
+ "modified": "2022-02-28 22:22:54.893269",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index cb1ea2c54d..af10c6d76a 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -119,7 +119,7 @@ def create_custom_field_if_values_exist(doctype, df):
frappe.db.count(dt=doctype, filters=IfNull(df.fieldname, "") != ""):
create_custom_field(doctype, df)
-def create_custom_field(doctype, df, ignore_validate=False):
+def create_custom_field(doctype, df, ignore_validate=False, is_system_generated=True):
df = frappe._dict(df)
if not df.fieldname and df.label:
df.fieldname = frappe.scrub(df.label)
@@ -130,8 +130,7 @@ def create_custom_field(doctype, df, ignore_validate=False):
"permlevel": 0,
"fieldtype": 'Data',
"hidden": 0,
- # Looks like we always use this programatically?
- # "is_standard": 1
+ "is_system_generated": is_system_generated
})
custom_field.update(df)
custom_field.flags.ignore_validate = ignore_validate
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 4862185b99..7c2a7a2aff 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);
}
},
@@ -243,7 +242,8 @@ frappe.ui.form.on("Customize Form Field", {
},
fields_add: function(frm, cdt, cdn) {
var f = frappe.model.get_doc(cdt, cdn);
- f.is_custom_field = 1;
+ f.is_system_generated = false;
+ f.is_custom_field = true;
}
});
@@ -341,11 +341,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/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 81cd38ff87..b6bd7ba787 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -402,7 +402,7 @@ class CustomizeForm(Document):
"property": prop,
"value": value,
"property_type": property_type
- })
+ }, is_system_generated=False)
def get_existing_property_value(self, property_name, fieldname=None):
# check if there is any need to make property setter!
@@ -487,12 +487,21 @@ def reset_customization(doctype):
setters = frappe.get_all("Property Setter", filters={
'doc_type': doctype,
'field_name': ['!=', 'naming_series'],
- 'property': ['!=', 'options']
+ 'property': ['!=', 'options'],
+ 'is_system_generated': False
}, pluck='name')
for setter in setters:
frappe.delete_doc("Property Setter", setter)
+ custom_fields = frappe.get_all("Custom Field", filters={
+ 'dt': doctype,
+ 'is_system_generated': False
+ }, pluck='name')
+
+ for field in custom_fields:
+ frappe.delete_doc("Custom Field", field)
+
frappe.clear_cache(doctype=doctype)
doctype_properties = {
diff --git a/frappe/custom/doctype/property_setter/property_setter.json b/frappe/custom/doctype/property_setter/property_setter.json
index 9707f1ee1c..039826b3b7 100644
--- a/frappe/custom/doctype/property_setter/property_setter.json
+++ b/frappe/custom/doctype/property_setter/property_setter.json
@@ -6,6 +6,7 @@
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
+ "is_system_generated",
"help",
"sb0",
"doctype_or_field",
@@ -103,13 +104,20 @@
{
"fieldname": "section_break_9",
"fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_system_generated",
+ "fieldtype": "Check",
+ "label": "Is System Generated",
+ "read_only": 1
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-12-14 14:15:41.929071",
+ "modified": "2022-02-28 22:24:12.377693",
"modified_by": "Administrator",
"module": "Custom",
"name": "Property Setter",
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 4152e32cd7..cf924d0a57 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -10,7 +10,7 @@ import re
import string
from contextlib import contextmanager
from time import time
-from typing import Dict, List, Tuple, Union
+from typing import Dict, List, Optional, Tuple, Union
from pypika.terms import Criterion, NullValue, PseudoColumn
@@ -119,6 +119,9 @@ class Database(object):
if not run:
return query
+ # remove \n \t from start and end of query
+ query = re.sub(r'^\s*|\s*$', '', query)
+
if re.search(r'ifnull\(', query, flags=re.IGNORECASE):
# replaces ifnull in query with coalesce
query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE)
@@ -142,8 +145,6 @@ class Database(object):
self.log_query(query, values, debug, explain)
if values!=():
- if isinstance(values, dict):
- values = dict(values)
# MySQL-python==1.2.5 hack!
if not isinstance(values, (dict, tuple, list)):
@@ -386,7 +387,7 @@ class Database(object):
"""
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug,
- order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct)
+ order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct, limit=1)
if not run:
return ret
@@ -395,7 +396,7 @@ class Database(object):
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False,
- run=True, pluck=False, distinct=False):
+ run=True, pluck=False, distinct=False, limit=None):
"""Returns multiple document properties.
:param doctype: DocType name.
@@ -425,14 +426,15 @@ class Database(object):
if isinstance(filters, list):
out = self._get_value_for_many_names(
- doctype,
- filters,
- fieldname,
- order_by,
+ doctype=doctype,
+ names=filters,
+ field=fieldname,
+ order_by=order_by,
debug=debug,
run=run,
pluck=pluck,
distinct=distinct,
+ limit=limit,
)
else:
@@ -446,17 +448,18 @@ class Database(object):
if order_by:
order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by
out = self._get_values_from_table(
- fields,
- filters,
- doctype,
- as_dict,
- debug,
- order_by,
- update,
+ fields=fields,
+ filters=filters,
+ doctype=doctype,
+ as_dict=as_dict,
+ debug=debug,
+ order_by=order_by,
+ update=update,
for_update=for_update,
run=run,
pluck=pluck,
- distinct=distinct
+ distinct=distinct,
+ limit=limit,
)
except Exception as e:
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
@@ -558,7 +561,7 @@ class Database(object):
def get_list(*args, **kwargs):
return frappe.get_list(*args, **kwargs)
- def set_single_value(self, doctype, fieldname, value, *args, **kwargs):
+ def set_single_value(self, doctype: str, fieldname: Union[str, Dict], value: Optional[Union[str, int]] = None, *args, **kwargs):
"""Set field value of Single DocType.
:param doctype: DocType of the single object
@@ -625,6 +628,7 @@ class Database(object):
run=True,
pluck=False,
distinct=False,
+ limit=None,
):
field_objects = []
@@ -643,6 +647,7 @@ class Database(object):
field_objects=field_objects,
fields=fields,
distinct=distinct,
+ limit=limit,
)
if (
fields == "*"
@@ -656,7 +661,7 @@ class Database(object):
)
return r
- def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False):
+ def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False, limit=None):
names = list(filter(None, names))
if names:
return self.get_all(
@@ -669,6 +674,7 @@ class Database(object):
as_list=1,
run=run,
distinct=distinct,
+ limit_page_length=limit
)
else:
return {}
@@ -884,27 +890,39 @@ class Database(object):
return self.sql("select name from `tab{doctype}` limit 1".format(doctype=doctype))
def exists(self, dt, dn=None, cache=False):
- """Returns true if document exists.
+ """Return the document name of a matching document, or None.
- :param dt: DocType name.
- :param dn: Document name or filter dict."""
- if isinstance(dt, str):
- if dt!="DocType" and dt==dn:
- return True # single always exists (!)
- try:
- return self.get_value(dt, dn, "name", cache=cache)
- except Exception:
- return None
+ Note: `cache` only works if `dt` and `dn` are of type `str`.
- elif isinstance(dt, dict) and dt.get('doctype'):
- try:
- conditions = []
- for d in dt:
- if d == 'doctype': continue
- conditions.append([d, '=', dt[d]])
- return self.get_all(dt['doctype'], filters=conditions, as_list=1)
- except Exception:
- return None
+ ## Examples
+
+ Pass doctype and docname (only in this case we can cache the result)
+
+ ```
+ exists("User", "jane@example.org", cache=True)
+ ```
+
+ Pass a dict of filters including the `"doctype"` key:
+
+ ```
+ exists({"doctype": "User", "full_name": "Jane Doe"})
+ ```
+
+ Pass the doctype and a dict of filters:
+
+ ```
+ exists("User", {"full_name": "Jane Doe"})
+ ```
+ """
+ if dt != "DocType" and dt == dn:
+ # single always exists (!)
+ return dn
+
+ if isinstance(dt, dict):
+ dt = dt.copy() # don't modify the original dict
+ dt, dn = dt.pop("doctype"), dt
+
+ return self.get_value(dt, dn, ignore=True, cache=cache)
def count(self, dt, filters=None, debug=False, cache=False):
"""Returns `COUNT(*)` for given DocType and filters."""
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/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py
index e689faafbe..3b84b94754 100644
--- a/frappe/desk/doctype/todo/todo.py
+++ b/frappe/desk/doctype/todo/todo.py
@@ -72,7 +72,8 @@ class ToDo(Document):
assignments = frappe.get_all("ToDo", filters={
"reference_type": self.reference_type,
"reference_name": self.reference_name,
- "status": ("!=", "Cancelled")
+ "status": ("!=", "Cancelled"),
+ "allocated_to": ("is", "set")
}, pluck="allocated_to")
assignments.reverse()
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index dba84e5175..9abf64c2cc 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
@@ -311,6 +312,7 @@ def get_assignments(dt, dn):
'reference_type': dt,
'reference_name': dn,
'status': ('!=', 'Cancelled'),
+ 'allocated_to': ("is", "set")
})
@frappe.whitelist()
@@ -355,7 +357,7 @@ def get_document_email(doctype, name):
return None
email = email.split("@")
- return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(name), email[1])
+ return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(cstr(name)), email[1])
def get_automatic_email_link():
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index b344763916..f5f50b14fe 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -352,14 +352,10 @@ def export_query():
)
return
- columns = get_columns_dict(data.columns)
-
from frappe.utils.xlsxutils import make_xlsx
- data["result"] = handle_duration_fieldtype_values(
- data.get("result"), data.get("columns")
- )
- xlsx_data, column_widths = build_xlsx_data(columns, data, visible_idx, include_indentation)
+ format_duration_fields(data)
+ xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation)
xlsx_file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths)
frappe.response["filename"] = report_name + ".xlsx"
@@ -367,39 +363,18 @@ def export_query():
frappe.response["type"] = "binary"
-def handle_duration_fieldtype_values(result, columns):
- for i, col in enumerate(columns):
- fieldtype = None
- if isinstance(col, str):
- col = col.split(":")
- if len(col) > 1:
- if col[1]:
- fieldtype = col[1]
- if "/" in fieldtype:
- fieldtype, options = fieldtype.split("/")
- else:
- fieldtype = "Data"
- else:
- fieldtype = col.get("fieldtype")
+def format_duration_fields(data: frappe._dict) -> None:
+ for i, col in enumerate(data.columns):
+ if col.get("fieldtype") != "Duration":
+ continue
- if fieldtype == "Duration":
- for entry in range(0, len(result)):
- row = result[entry]
- if isinstance(row, dict):
- val_in_seconds = row[col.fieldname]
- if val_in_seconds:
- duration_val = format_duration(val_in_seconds)
- row[col.fieldname] = duration_val
- else:
- val_in_seconds = row[i]
- if val_in_seconds:
- duration_val = format_duration(val_in_seconds)
- row[i] = duration_val
-
- return result
+ for row in data.result:
+ index = col.fieldname if isinstance(row, dict) else i
+ if row[index]:
+ row[index] = format_duration(row[index])
-def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visible_idx=False):
+def build_xlsx_data(data, visible_idx, include_indentation, ignore_visible_idx=False):
result = [[]]
column_widths = []
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index 3b76953ed1..dd22f821cf 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -9,7 +9,6 @@ from frappe import _, is_whitelisted
import re
import wrapt
-UNTRANSLATED_DOCTYPES = ["DocType", "Role"]
def sanitize_searchfield(searchfield):
blacklisted_keywords = ['select', 'delete', 'drop', 'update', 'case', 'and', 'or', 'like']
@@ -114,6 +113,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
or_filters = []
+ translated_search_doctypes = frappe.get_hooks("translated_search_doctypes")
# build from doctype
if txt:
search_fields = ["name"]
@@ -125,7 +125,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
for f in search_fields:
fmeta = meta.get_field(f.strip())
- if (doctype not in UNTRANSLATED_DOCTYPES) and (f == "name" or (fmeta and fmeta.fieldtype in ["Data", "Text", "Small Text", "Long Text",
+ if (doctype not in translated_search_doctypes) and (f == "name" or (fmeta and fmeta.fieldtype in ["Data", "Text", "Small Text", "Long Text",
"Link", "Select", "Read Only", "Text Editor"])):
or_filters.append([doctype, f.strip(), "like", "%{0}%".format(txt)])
@@ -160,7 +160,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype))
- if doctype in UNTRANSLATED_DOCTYPES:
+ if doctype in translated_search_doctypes:
page_length = None
values = frappe.get_list(doctype,
@@ -175,7 +175,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
as_list=not as_dict,
strict=False)
- if doctype in UNTRANSLATED_DOCTYPES:
+ if doctype in translated_search_doctypes:
# Filtering the values array so that query is included in very element
values = (
v for v in values
@@ -257,7 +257,7 @@ def scrub_custom_query(query, key, txt):
def relevance_sorter(key, query, as_dict):
value = _(key.name if as_dict else key[0])
return (
- value.lower().startswith(query.lower()) is not True,
+ cstr(value).lower().startswith(query.lower()) is not True,
value
)
diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py
index 682f0df7cf..abeb681a25 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -15,8 +15,6 @@ from frappe.utils.csvutils import to_csv
from frappe.utils.xlsxutils import make_xlsx
from frappe.desk.query_report import build_xlsx_data
-max_reports_per_user = frappe.local.conf.max_reports_per_user or 3
-
class AutoEmailReport(Document):
def autoname(self):
@@ -46,6 +44,8 @@ class AutoEmailReport(Document):
def validate_report_count(self):
'''check that there are only 3 enabled reports per user'''
count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0]
+ max_reports_per_user = frappe.local.conf.max_reports_per_user or 3
+
if count > max_reports_per_user + (-1 if self.flags.in_insert else 0):
frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user))
@@ -104,7 +104,7 @@ class AutoEmailReport(Document):
report_data['columns'] = columns
report_data['result'] = data
- xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
+ xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True)
xlsx_file = make_xlsx(xlsx_data, "Auto Email Report", column_widths=column_widths)
return xlsx_file.getvalue()
@@ -113,7 +113,7 @@ class AutoEmailReport(Document):
report_data['columns'] = columns
report_data['result'] = data
- xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
+ xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True)
return to_csv(xlsx_data)
else:
diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py
index 9b4f3b984c..dd8623cae2 100644
--- a/frappe/email/doctype/email_queue/email_queue.py
+++ b/frappe/email/doctype/email_queue/email_queue.py
@@ -111,7 +111,6 @@ class EmailQueue(Document):
""" Send emails to recipients.
"""
if not self.can_send_now():
- frappe.db.rollback()
return
with SendMailContext(self, is_background_task) as ctx:
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/test_notification.py b/frappe/email/doctype/notification/test_notification.py
index f05d35be3e..f6f216ada2 100644
--- a/frappe/email/doctype/notification/test_notification.py
+++ b/frappe/email/doctype/notification/test_notification.py
@@ -240,7 +240,7 @@ class TestNotification(unittest.TestCase):
self.assertTrue(email_queue)
# check if description is changed after alert since set_property_after_alert is set
- self.assertEquals(todo.description, 'Changed by Notification')
+ self.assertEqual(todo.description, 'Changed by Notification')
recipients = [d.recipient for d in email_queue.recipients]
self.assertTrue('test2@example.com' in recipients)
diff --git a/frappe/email/queue.py b/frappe/email/queue.py
index 16e3fecf48..f6f52e79e2 100755
--- a/frappe/email/queue.py
+++ b/frappe/email/queue.py
@@ -1,10 +1,12 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
from frappe import msgprint, _
from frappe.utils.verified_command import get_signed_params, verify_request
from frappe.utils import get_url, now_datetime, cint
+from frappe.query_builder import DocType, Interval
+from frappe.query_builder.functions import Now
def get_emails_sent_this_month(email_account=None):
"""Get count of emails sent from a specific email account.
@@ -162,15 +164,16 @@ def get_queue():
by priority desc, creation asc
limit 500''', { 'now': now_datetime() }, as_dict=True)
-def clear_outbox(days=None):
+def clear_outbox(days: int = None) -> None:
"""Remove low priority older than 31 days in Outbox or configured in Log Settings.
Note: Used separate query to avoid deadlock
"""
- if not days:
- days=31
+ days = days or 31
+ email_queue = DocType("Email Queue")
- email_queues = frappe.db.sql_list("""SELECT `name` FROM `tabEmail Queue`
- WHERE `priority`=0 AND `modified` < (NOW() - INTERVAL '{0}' DAY)""".format(days))
+ email_queues = frappe.qb.from_(email_queue).select(email_queue.name).where(
+ email_queue.modified < (Now() - Interval(days=days))
+ ).run(pluck=True)
if email_queues:
frappe.db.delete("Email Queue", {"name": ("in", email_queues)})
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/exceptions.py b/frappe/exceptions.py
index 6ee72b5f81..fcac349708 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -110,3 +110,6 @@ class InvalidAuthorizationPrefix(CSRFTokenError): pass
class InvalidAuthorizationToken(CSRFTokenError): pass
class InvalidDatabaseFile(ValidationError): pass
class ExecutableNotFound(FileNotFoundError): pass
+
+class InvalidRemoteException(Exception):
+ pass
diff --git a/frappe/frappeclient.py b/frappe/frappeclient.py
index 59db38584c..7a1587aae0 100644
--- a/frappe/frappeclient.py
+++ b/frappe/frappeclient.py
@@ -7,6 +7,7 @@ import json
import requests
import frappe
+from frappe.utils.data import cstr
class AuthError(Exception):
@@ -122,7 +123,7 @@ class FrappeClient(object):
'''Update a remote document
:param doc: dict or Document object to be updated remotely. `name` is mandatory for this'''
- url = self.url + "/api/resource/" + doc.get("doctype") + "/" + doc.get("name")
+ url = self.url + "/api/resource/" + doc.get("doctype") + "/" + cstr(doc.get("name"))
res = self.session.put(url, data={"data":frappe.as_json(doc)}, verify=self.verify, headers=self.headers)
return frappe._dict(self.post_process(res))
@@ -207,7 +208,7 @@ class FrappeClient(object):
if fields:
params["fields"] = json.dumps(fields)
- res = self.session.get(self.url + "/api/resource/" + doctype + "/" + name,
+ res = self.session.get(self.url + "/api/resource/" + doctype + "/" + cstr(name),
params=params, verify=self.verify, headers=self.headers)
return self.post_process(res)
diff --git a/frappe/handler.py b/frappe/handler.py
old mode 100755
new mode 100644
index bba2f3c057..b4ec077947
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -251,11 +251,10 @@ def ping():
def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
"""run a whitelisted controller method"""
- import inspect
- import json
+ from inspect import getfullargspec
- if not args:
- args = arg or ""
+ if not args and arg:
+ args = arg
if dt: # not called from a doctype (from a page)
if not dn:
@@ -263,9 +262,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
doc = frappe.get_doc(dt, dn)
else:
- if isinstance(docs, str):
- docs = json.loads(docs)
-
+ docs = frappe.parse_json(docs)
doc = frappe.get_doc(docs)
doc._original_modified = doc.modified
doc.check_if_latest()
@@ -274,16 +271,16 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
throw_permission_error()
try:
- args = json.loads(args)
+ args = frappe.parse_json(args)
except ValueError:
- args = args
+ pass
method_obj = getattr(doc, method)
fn = getattr(method_obj, '__func__', method_obj)
is_whitelisted(fn)
is_valid_http_method(fn)
- fnargs = inspect.getfullargspec(method_obj).args
+ fnargs = getfullargspec(method_obj).args
if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"):
response = doc.run_method(method)
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 1ecd47b535..74473966c1 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -394,4 +394,6 @@ override_whitelisted_methods = {
"frappe.core.doctype.file.file.create_new_folder": "frappe.core.api.file.create_new_folder",
"frappe.core.doctype.file.file.move_file": "frappe.core.api.file.move_file",
"frappe.core.doctype.file.file.zip_files": "frappe.core.api.file.zip_files",
-}
\ No newline at end of file
+}
+
+translated_search_doctypes = ["DocType", "Role", "Country", "Gender", "Salutation"]
diff --git a/frappe/installer.py b/frappe/installer.py
index 6ebab95a7d..e28a942f01 100644
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -5,10 +5,11 @@ import json
import os
import sys
from collections import OrderedDict
-from typing import List, Dict
+from typing import List, Dict, Tuple
import frappe
from frappe.defaults import _clear_cache
+from frappe.utils import is_git_url
def _new_site(
@@ -34,7 +35,6 @@ def _new_site(
from frappe.commands.scheduler import _is_scheduler_enabled
from frappe.utils import get_site_path, scheduler, touch_file
-
if not force and os.path.exists(site):
print("Site {0} already exists".format(site))
sys.exit(1)
@@ -124,6 +124,86 @@ def install_db(root_login=None, root_password=None, db_name=None, source_sql=Non
frappe.flags.in_install_db = False
+def find_org(org_repo: str) -> Tuple[str, str]:
+ """ find the org a repo is in
+
+ find_org()
+ ref -> https://github.com/frappe/bench/blob/develop/bench/utils/__init__.py#L390
+
+ :param org_repo:
+ :type org_repo: str
+
+ :raises InvalidRemoteException: if the org is not found
+
+ :return: organisation and repository
+ :rtype: Tuple[str, str]
+ """
+ from frappe.exceptions import InvalidRemoteException
+ import requests
+
+ for org in ["frappe", "erpnext"]:
+ res = requests.head(f"https://api.github.com/repos/{org}/{org_repo}")
+ if res.ok:
+ return org, org_repo
+
+ raise InvalidRemoteException
+
+
+def fetch_details_from_tag(_tag: str) -> Tuple[str, str, str]:
+ """ parse org, repo, tag from string
+
+ fetch_details_from_tag()
+ ref -> https://github.com/frappe/bench/blob/develop/bench/utils/__init__.py#L403
+
+ :param _tag: input string
+ :type _tag: str
+
+ :return: organisation, repostitory, tag
+ :rtype: Tuple[str, str, str]
+ """
+ app_tag = _tag.split("@")
+ org_repo = app_tag[0].split("/")
+
+ try:
+ repo, tag = app_tag
+ except ValueError:
+ repo, tag = app_tag + [None]
+
+ try:
+ org, repo = org_repo
+ except Exception:
+ org, repo = find_org(org_repo[0])
+
+ return org, repo, tag
+
+
+def parse_app_name(name: str) -> str:
+ """parse repo name from name
+
+ __setup_details_from_git()
+ ref -> https://github.com/frappe/bench/blob/develop/bench/app.py#L114
+
+
+ :param name: git tag
+ :type name: str
+
+ :return: repository name
+ :rtype: str
+ """
+ name = name.rstrip("/")
+ if os.path.exists(name):
+ repo = os.path.split(name)[-1]
+ elif is_git_url(name):
+ if name.startswith("git@") or name.startswith("ssh://"):
+ _repo = name.split(":")[1].rsplit("/", 1)[1]
+ else:
+ _repo = name.rsplit("/", 2)[2]
+ repo = _repo.split(".")[0]
+ else:
+ _, repo, _ = fetch_details_from_tag(name)
+ return repo
+
+
def install_app(name, verbose=False, set_as_patched=True):
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.model.sync import sync_for
@@ -140,7 +220,8 @@ def install_app(name, verbose=False, set_as_patched=True):
# install pre-requisites
if app_hooks.required_apps:
for app in app_hooks.required_apps:
- install_app(app, verbose=verbose)
+ name = parse_app_name(name)
+ install_app(name, verbose=verbose)
frappe.flags.in_install = name
frappe.clear_cache()
@@ -611,7 +692,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 0b9f6e0211..be233eb7e3 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -475,7 +475,7 @@ class BaseDocument(object):
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE)
# don't update name, as case might've been changed
- name = d['name']
+ name = cstr(d['name'])
del d['name']
columns = list(d)
@@ -963,10 +963,13 @@ class BaseDocument(object):
from frappe.model.meta import get_default_df
df = get_default_df(fieldname)
- if not currency and df:
- currency = self.get(df.get("options"))
- if not frappe.db.exists('Currency', currency, cache=True):
- currency = None
+ if (
+ df.fieldtype == "Currency"
+ and not currency
+ and (currency_field := df.get("options"))
+ and (currency_value := self.get(currency_field))
+ ):
+ currency = frappe.db.get_value('Currency', currency_value, cache=True)
val = self.get(fieldname)
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/modules/utils.py b/frappe/modules/utils.py
index 13b52d2020..4768faff48 100644
--- a/frappe/modules/utils.py
+++ b/frappe/modules/utils.py
@@ -272,7 +272,7 @@ def make_boilerplate(template, doc, opts=None):
frappe.utils.cstr(source.read()).format(
app_publisher=app_publisher,
year=frappe.utils.nowdate()[:4],
- classname=doc.name.replace(" ", ""),
+ classname=doc.name.replace(" ", "").replace("-", ""),
base_class_import=base_class_import,
base_class=base_class,
doctype=doc.name, **opts,
diff --git a/frappe/patches.txt b/frappe/patches.txt
index a666480c90..bc2bc22637 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -197,4 +197,5 @@ frappe.patches.v14_0.copy_mail_data #08.03.21
frappe.patches.v14_0.update_github_endpoints #08-11-2021
frappe.patches.v14_0.remove_db_aggregation
frappe.patches.v14_0.update_color_names_in_kanban_board_column
+frappe.patches.v14_0.update_is_system_generated_flag
frappe.patches.v14_0.update_auto_account_deletion_duration
diff --git a/frappe/patches/v14_0/update_is_system_generated_flag.py b/frappe/patches/v14_0/update_is_system_generated_flag.py
new file mode 100644
index 0000000000..657e02aebc
--- /dev/null
+++ b/frappe/patches/v14_0/update_is_system_generated_flag.py
@@ -0,0 +1,17 @@
+import frappe
+
+def execute():
+ # assuming all customization generated by Admin is system generated customization
+ custom_field = frappe.qb.DocType("Custom Field")
+ (
+ frappe.qb.update(custom_field)
+ .set(custom_field.is_system_generated, True)
+ .where(custom_field.owner == 'Administrator').run()
+ )
+
+ property_setter = frappe.qb.DocType("Property Setter")
+ (
+ frappe.qb.update(property_setter)
+ .set(property_setter.is_system_generated, True)
+ .where(property_setter.owner == 'Administrator').run()
+ )
diff --git a/frappe/permissions.py b/frappe/permissions.py
index af17faba01..a6c17fb59f 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -594,4 +594,4 @@ def is_parent_valid(child_doctype, parent_doctype):
from frappe.core.utils import find
parent_meta = frappe.get_meta(parent_doctype)
child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype)
- return not parent_meta.istable and child_table_field_exists
\ No newline at end of file
+ return not parent_meta.istable and child_table_field_exists
diff --git a/frappe/public/css/tree.css b/frappe/public/css/tree.css
index 2aa411bc11..8b216bc321 100644
--- a/frappe/public/css/tree.css
+++ b/frappe/public/css/tree.css
@@ -24,7 +24,7 @@ ul.tree-children {
}
.tree-link .node-parent,
.tree-link .node-leaf {
- margin-right: 5px;
+ margin-right: 8px;
}
.tree-link.active i {
color: #5e64ff;
diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue
index 97ed5585e2..c671b28160 100644
--- a/frappe/public/js/frappe/file_uploader/FileUploader.vue
+++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue
@@ -526,11 +526,13 @@ export default {
error: true
});
capture.show();
- capture.submit(data_url => {
- let filename = `capture_${frappe.datetime.now_datetime().replaceAll(/[: -]/g, '_')}.png`;
- this.url_to_file(data_url, filename, 'image/png').then((file) =>
- this.add_files([file])
- );
+ capture.submit(data_urls => {
+ data_urls.forEach(data_url => {
+ let filename = `capture_${frappe.datetime.now_datetime().replaceAll(/[: -]/g, '_')}.png`;
+ this.url_to_file(data_url, filename, 'image/png').then((file) =>
+ this.add_files([file])
+ );
+ });
});
},
show_google_drive_picker() {
diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js
index bd66225171..a91058a208 100644
--- a/frappe/public/js/frappe/form/controls/attach.js
+++ b/frappe/public/js/frappe/form/controls/attach.js
@@ -37,8 +37,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro
if(this.frm) {
me.parse_validate_and_set_in_model(null);
me.refresh();
- me.frm.attachments.remove_attachment_by_filename(me.value, function() {
- me.parse_validate_and_set_in_model(null);
+ me.frm.attachments.remove_attachment_by_filename(me.value, async () => {
+ await me.parse_validate_and_set_in_model(null);
me.refresh();
me.frm.doc.docstatus == 1 ? me.frm.save('Update') : me.frm.save();
});
@@ -110,9 +110,9 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro
return this.value || null;
}
- on_upload_complete(attachment) {
+ async on_upload_complete(attachment) {
if(this.frm) {
- this.parse_validate_and_set_in_model(attachment.file_url);
+ await this.parse_validate_and_set_in_model(attachment.file_url);
this.frm.attachments.update_attachment(attachment);
this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save();
}
diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js
index d440874f36..0070d384d7 100644
--- a/frappe/public/js/frappe/form/footer/form_timeline.js
+++ b/frappe/public/js/frappe/form/footer/form_timeline.js
@@ -454,7 +454,10 @@ class FormTimeline extends BaseTimeline {
let edit_box = this.make_editable(edit_wrapper);
let content_wrapper = comment_wrapper.find('.content');
let more_actions_wrapper = comment_wrapper.find('.more-actions');
- if (frappe.model.can_delete("Comment")) {
+ if (frappe.model.can_delete("Comment") && (
+ frappe.session.user == doc.owner ||
+ frappe.user.has_role("System Manager")
+ )) {
const delete_option = $(`