diff --git a/cypress/fixtures/custom_submittable_doctype.js b/cypress/fixtures/custom_submittable_doctype.js new file mode 100644 index 0000000000..c88d37b373 --- /dev/null +++ b/cypress/fixtures/custom_submittable_doctype.js @@ -0,0 +1,53 @@ +export default { + name: 'Custom Submittable DocType', + custom: 1, + actions: [], + is_submittable: 1, + creation: '2019-12-10 06:29:07.215072', + doctype: 'DocType', + editable_grid: 1, + engine: 'InnoDB', + fields: [ + { + fieldname: 'enabled', + fieldtype: 'Check', + label: 'Enabled', + allow_on_submit: 1, + reqd: 1 + }, + { + fieldname: 'title', + fieldtype: 'Data', + label: 'title', + reqd: 1 + }, + { + fieldname: 'description', + fieldtype: 'Text Editor', + label: 'Description' + } + ], + links: [], + modified: '2019-12-10 14:40:53.127615', + modified_by: 'Administrator', + module: 'Custom', + owner: 'Administrator', + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: 'System Manager', + share: 1, + write: 1, + submit: 1, + cancel: 1 + } + ], + quick_entry: 1, + sort_field: 'modified', + sort_order: 'ASC', + track_changes: 1 +}; \ No newline at end of file diff --git a/cypress/integration/form.js b/cypress/integration/form.js index b7ddd6ecb7..ed5ff21ff1 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -2,8 +2,13 @@ context('Form', () => { before(() => { cy.login(); cy.visit('/desk'); + cy.window().its('frappe').then(frappe => { + frappe.call("frappe.tests.ui_test_helpers.create_contact_records"); + }); + }); + beforeEach(() => { + cy.visit('/desk'); }); - it('create a new form', () => { cy.visit('/desk#Form/ToDo/New ToDo 1'); cy.fill_field('description', 'this is a test todo', 'Text Editor').blur(); @@ -13,4 +18,22 @@ context('Form', () => { cy.location('hash').should('eq', '#List/ToDo/List'); cy.get('.list-row').should('contain', 'this is a test todo'); }); + it('navigates between documents with child table list filters applied', () => { + cy.visit('/desk#List/Contact'); + cy.get('.tag-filters-area .btn:contains("Add Filter")').click(); + cy.get('.fieldname-select-area').should('exist'); + cy.get('.fieldname-select-area input').type('Number{enter}', { force: true }); + cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true }); + cy.get('.filter-box .btn:contains("Apply")').click({ force: true }); + cy.visit('/desk#Form/Contact/Test Form Contact 3'); + cy.get('.prev-doc').click({ force: true }); + cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible'); + cy.get('.btn-modal-close:visible').click(); + cy.get('.next-doc').click({ force: true }); + cy.wait(200); + cy.contains('Test Form Contact 2').should('not.exist'); + cy.get('.page-title .title-text').should('contain', 'Test Form Contact 1'); + cy.visit('/desk#List/Contact'); + cy.get('.clear-filters.btn').click(); + }); }); diff --git a/cypress/integration/grid_pagination.js b/cypress/integration/grid_pagination.js index cf41e31ee6..67fdb8acf0 100644 --- a/cypress/integration/grid_pagination.js +++ b/cypress/integration/grid_pagination.js @@ -14,15 +14,15 @@ context('Grid Pagination', () => { cy.visit('/desk#Form/Contact/Test Contact'); cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); cy.get('@table').find('.current-page-number').should('contain', '1'); - cy.get('@table').find('.total-page-number').should('contain', '50'); - cy.get('@table').find('.grid-body .grid-row').should('have.length', 20); + cy.get('@table').find('.total-page-number').should('contain', '20'); + cy.get('@table').find('.grid-body .grid-row').should('have.length', 50); }); it('goes to the next and previous page', () => { cy.visit('/desk#Form/Contact/Test Contact'); cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); cy.get('@table').find('.next-page').click(); cy.get('@table').find('.current-page-number').should('contain', '2'); - cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '21'); + cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '51'); cy.get('@table').find('.prev-page').click(); cy.get('@table').find('.current-page-number').should('contain', '1'); cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '1'); @@ -32,19 +32,20 @@ context('Grid Pagination', () => { cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); cy.get('@table').find('button.grid-add-row').click(); cy.get('@table').find('.grid-body .row-index').should('contain', 1001); - cy.get('@table').find('.current-page-number').should('contain', '51'); - cy.get('@table').find('.total-page-number').should('contain', '51'); + cy.get('@table').find('.current-page-number').should('contain', '21'); + cy.get('@table').find('.total-page-number').should('contain', '21'); cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({force: true}); cy.get('@table').find('button.grid-remove-rows').click(); cy.get('@table').find('.grid-body .row-index').last().should('contain', 1000); - cy.get('@table').find('.current-page-number').should('contain', '50'); - cy.get('@table').find('.total-page-number').should('contain', '50'); + cy.get('@table').find('.current-page-number').should('contain', '20'); + cy.get('@table').find('.total-page-number').should('contain', '20'); }); it('deletes all rows', ()=> { cy.visit('/desk#Form/Contact/Test Contact'); cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true}); cy.get('@table').find('button.grid-remove-all-rows').click(); + cy.get('.modal-dialog .btn-primary').contains('Yes').click(); cy.get('@table').find('.grid-body .grid-row').should('have.length', 0); }); }); \ No newline at end of file diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js new file mode 100644 index 0000000000..c7aeaa92de --- /dev/null +++ b/cypress/integration/report_view.js @@ -0,0 +1,40 @@ +import custom_submittable_doctype from '../fixtures/custom_submittable_doctype'; +const doctype_name = custom_submittable_doctype.name; + +context('Report View', () => { + before(() => { + cy.login(); + cy.visit('/desk'); + cy.insert_doc('DocType', custom_submittable_doctype, true); + cy.clear_cache(); + cy.insert_doc(doctype_name, { + 'title': 'Doc 1', + 'description': 'Random Text', + 'enabled': 0, + // submit document + 'docstatus': 1 + }, true).as('doc'); + }); + it('Field with enabled allow_on_submit should be editable.', () => { + cy.server(); + cy.route('POST', 'api/method/frappe.client.set_value').as('value-update'); + cy.visit(`/desk#List/${doctype_name}/Report`); + let cell = cy.get('.dt-row-0 > .dt-cell--col-3'); + // select the cell + cell.dblclick(); + cell.find('input[data-fieldname="enabled"]').check({force: true}); + cy.get('.dt-row-0 > .dt-cell--col-4').click(); + cy.wait('@value-update'); + cy.get('@doc').then(doc => { + cy.call('frappe.client.get_value', { + doctype: doc.doctype, + filters: { + name: doc.name, + }, + fieldname: 'enabled' + }).then(r => { + expect(r.message.enabled).to.equals(1); + }); + }); + }); +}); \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 464cbbe1d5..41d9c16d7b 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -183,3 +183,31 @@ Cypress.Commands.add('hide_dialog', () => { cy.get_open_dialog().find('.btn-modal-close').click(); cy.get('.modal:visible').should('not.exist'); }); + +Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'POST', + url: `/api/resource/${doctype}`, + body: args, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + failOnStatusCode: !ignore_duplicate + }) + .then(res => { + let status_codes = [200]; + if (ignore_duplicate) { + status_codes.push(409); + } + expect(res.status).to.be.oneOf(status_codes); + return res.body.data; + }); + }); +}); \ No newline at end of file diff --git a/frappe/__init__.py b/frappe/__init__.py index cb1b6e5358..b383ae958e 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -23,7 +23,7 @@ if sys.version[0] == '2': reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '12.0.20' +__version__ = '12.1.0' __title__ = "Frappe Framework" local = Local() diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index b431c7c473..55792b2648 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -31,12 +31,6 @@ class AssignmentRule(Document): return False - def apply_close(self, doc, assignments): - if (self.close_assignments and - self.name in [d.assignment_rule for d in assignments]): - return self.close_assignments(doc) - - return False def apply_assign(self, doc): if self.safe_eval('assign_condition', doc): @@ -157,16 +151,17 @@ def bulk_apply(doctype, docnames): apply(None, doctype=doctype, name=name) def reopen_closed_assignment(doc): - todo = frappe.db.exists('ToDo', dict( + todo_list = frappe.db.get_all('ToDo', filters = dict( reference_type = doc.doctype, reference_name = doc.name, status = 'Closed' )) - if not todo: + if not todo_list: return False - todo = frappe.get_doc("ToDo", todo) - todo.status = 'Open' - todo.save(ignore_permissions=True) + for todo in todo_list: + todo_doc = frappe.get_doc('ToDo', todo.name) + todo_doc.status = 'Open' + todo_doc.save(ignore_permissions=True) return True def apply(doc, method=None, doctype=None, name=None): @@ -225,13 +220,12 @@ def apply(doc, method=None, doctype=None, name=None): continue if not new_apply: - reopen = reopen_closed_assignment(doc) - if reopen: - break - close = assignment_rule.apply_close(doc, assignments) - if close: - break - + # only reopen if close condition is not satisfied + if not assignment_rule.safe_eval('close_condition', doc): + reopen = reopen_closed_assignment(doc) + if reopen: + break + assignment_rule.close_assignments(doc) def get_assignment_rules(): return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))] diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 27f17a1a62..e618c7d63e 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -117,8 +117,8 @@ class AutoRepeat(Document): start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day) if self.end_date: - start_date = start_date = get_next_schedule_date( - start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True) + start_date = get_next_schedule_date( + start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True) while (getdate(start_date) < getdate(end_date)): row = { "reference_document" : self.reference_document, @@ -126,10 +126,9 @@ class AutoRepeat(Document): "next_scheduled_date" : start_date } schedule_details.append(row) - start_date = start_date = get_next_schedule_date( + start_date = get_next_schedule_date( start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True) - return schedule_details def create_documents(self): diff --git a/frappe/commands/site.py b/frappe/commands/site.py index e28fd36346..89e9ab7f34 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -13,6 +13,8 @@ from six import text_type @click.argument('site') @click.option('--db-name', help='Database name') @click.option('--db-type', default='mariadb', type=click.Choice(['mariadb', 'postgres']), help='Optional "postgres" or "mariadb". Default is "mariadb"') +@click.option('--db-host', help='Database Host') +@click.option('--db-port', type=int, help='Database Port') @click.option('--mariadb-root-username', default='root', help='Root username for MariaDB') @click.option('--mariadb-root-password', help='Root password for MariaDB') @click.option('--admin-password', help='Administrator password for new site', default=None) @@ -21,22 +23,22 @@ from six import text_type @click.option('--source_sql', help='Initiate database with a SQL file') @click.option('--install-app', multiple=True, help='Install app after installation') def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None, - verbose=False, install_apps=None, source_sql=None, force=None, install_app=None, - db_name=None, db_type=None): + verbose=False, install_apps=None, source_sql=None, force=None, install_app=None, + db_name=None, db_type=None, db_host=None, db_port=None): "Create a new site" frappe.init(site=site, new_site=True) _new_site(db_name, site, mariadb_root_username=mariadb_root_username, - mariadb_root_password=mariadb_root_password, admin_password=admin_password, - verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force, - db_type=db_type) + mariadb_root_password=mariadb_root_password, admin_password=admin_password, + verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force, + db_type=db_type, db_host=db_host, db_port=db_port) if len(frappe.utils.get_sites()) == 1: use(site) def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None, - admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False, - reinstall=False, db_type=None): + admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False, + reinstall=False, db_type=None, db_host=None, db_port=None): """Install a new Frappe site""" if not force and os.path.exists(site): @@ -65,8 +67,8 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N installing = touch_file(get_site_path('locks', 'installing.lock')) install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, - db_name=db_name, admin_password=admin_password, verbose=verbose, - source_sql=source_sql, force=force, reinstall=reinstall, db_type=db_type) + db_name=db_name, admin_password=admin_password, verbose=verbose, + source_sql=source_sql, force=force, reinstall=reinstall, db_type=db_type, db_host=db_host, db_port=db_port) apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or []) for app in apps_to_install: diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 361030d07a..1848136bee 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -398,7 +398,7 @@ def get_bcc(doc, recipients=None, fetched_from_email_account=False): return bcc def add_attachments(name, attachments): - '''Add attachments to the given Communiction''' + '''Add attachments to the given Communication''' # loop through attachments for a in attachments: if isinstance(a, string_types): @@ -411,7 +411,9 @@ def add_attachments(name, attachments): "file_url": attach.file_url, "attached_to_doctype": "Communication", "attached_to_name": name, - "folder": "Home/Attachments"}) + "folder": "Home/Attachments", + "is_private": attach.is_private + }) _file.save(ignore_permissions=True) def filter_email_list(doc, email_list, exclude, is_cc=False, is_bcc=False): diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index ee139000e1..041b8c3011 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -89,8 +89,9 @@ class File(Document): def validate(self): if self.is_new(): + self.set_is_private() + self.set_file_name() self.validate_duplicate_entry() - self.validate_file_name() self.validate_folder() if not self.file_url and not self.flags.ignore_file_validate: @@ -133,6 +134,9 @@ class File(Document): frappe.db.set_value(self.attached_to_doctype, self.attached_to_name, self.attached_to_field, self.file_url) + if self.file_url and (self.is_private != self.file_url.startswith('/private')): + frappe.throw(_('Invalid file URL. Please contact System Administrator.')) + def set_folder_name(self): """Make parent folders if not exists based on reference doctype and name""" if self.attached_to_doctype and not self.folder: @@ -157,9 +161,11 @@ class File(Document): def validate_duplicate_entry(self): if not self.flags.ignore_duplicate_entry_error and not self.is_folder: - # check duplicate name + if not self.content_hash: + self.generate_content_hash() - # check duplicate assignement + # check duplicate name + # check duplicate assignment filters = { 'content_hash': self.content_hash, 'is_private': self.is_private, @@ -184,21 +190,20 @@ class File(Document): else: self.file_url = duplicate_file.file_url - def validate_file_name(self): + def set_file_name(self): if not self.file_name and self.file_url: self.file_name = self.file_url.split('/')[-1] def generate_content_hash(self): - if self.content_hash or not self.file_url: + if self.content_hash or not self.file_url or self.file_url.startswith('http'): return - if self.file_url.startswith("/files/"): - try: - with open(get_files_path(self.file_name.lstrip("/")), "rb") as f: - self.content_hash = get_content_hash(f.read()) - except IOError: - frappe.msgprint(_("File {0} does not exist").format(self.file_url)) - raise + try: + with open(get_files_path(self.file_name.lstrip("/"), is_private=self.is_private), "rb") as f: + self.content_hash = get_content_hash(f.read()) + except IOError: + frappe.msgprint(_("File {0} does not exist").format(self.file_url)) + raise def on_trash(self): if self.is_home_folder or self.is_attachments_folder: @@ -563,6 +568,9 @@ class File(Document): except frappe.DoesNotExistError: frappe.clear_messages() + def set_is_private(self): + if self.file_url: + self.is_private = cint(self.file_url.startswith('/private')) def on_doctype_update(): frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"]) diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 82fff31394..36c297cc26 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "Prompt", "creation": "2019-09-30 11:56:57.943241", "doctype": "DocType", @@ -43,7 +44,7 @@ "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete" + "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)" }, { "depends_on": "eval:doc.script_type==='API'", @@ -73,7 +74,8 @@ "fieldtype": "Section Break" } ], - "modified": "2019-10-09 15:08:40.085059", + "links": [], + "modified": "2019-12-17 12:55:07.389775", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 878810f459..2e1a5ae8bb 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -14,6 +14,8 @@ EVENT_MAP = { 'on_cancel': 'After Cancel', 'on_trash': 'Before Delete', 'after_delete': 'After Delete', + 'before_update_after_submit': 'Before Save (Submitted Document)', + 'on_update_after_submit': 'After Save (Submitted Document)' } def run_server_script_api(method): diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py index 0447f97273..80236b2dc2 100644 --- a/frappe/database/db_manager.py +++ b/frappe/database/db_manager.py @@ -80,12 +80,14 @@ class DbManager: if pipe: print('Creating Database...') - command = '{pipe} mysql -u {user} -p{password} -h{host} {target} {source}'.format( + command = '{pipe} mysql -u {user} -p{password} -h{host} ' + ('-P{port}' if frappe.db.port else '') + ' {target} {source}' + command = command.format( pipe=pipe, user=esc(user), password=esc(password), host=esc(frappe.db.host), target=esc(target), - source=source + source=source, + port=frappe.db.port ) os.system(command) diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.py b/frappe/desk/doctype/global_search_settings/global_search_settings.py index 1cdbf11224..85c9687ab3 100644 --- a/frappe/desk/doctype/global_search_settings/global_search_settings.py +++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py @@ -61,7 +61,7 @@ def update_global_search_doctypes(): if search_doctypes.get(domain): global_search_doctypes.extend(search_doctypes.get(domain)) - doctype_list = set([dt.name for dt in frappe.get_list("DocType")]) + doctype_list = set([dt.name for dt in frappe.get_all("DocType")]) allowed_in_global_search = [] for dt in global_search_doctypes: diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index 6676bd1908..cf2a8f0879 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -105,7 +105,7 @@ def get_next(doctype, value, prev, filters, sort_order, sort_field): res = frappe.get_list(doctype, fields = ["name"], filters = filters, - order_by = sort_field + " " + sort_order, + order_by = "`tab{0}`.{1}".format(doctype, sort_field) + " " + sort_order, limit_start=0, limit_page_length=1, as_list=True) if not res: diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index b142047059..3a8815ca71 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -182,6 +182,8 @@ def get_notification_info(): return out def get_notification_config(): + user = frappe.session.user or 'Guest' + def _get(): subscribed_documents = get_subscribed_documents() config = frappe._dict() @@ -205,7 +207,7 @@ def get_notification_config(): config[key].update(nc.get(key, {})) return config - return frappe.cache().hget("notification_config", frappe.session.user, _get) + return frappe.cache().hget("notification_config", user, _get) def get_filters_for(doctype): '''get open filters for doctype''' diff --git a/frappe/desk/page/leaderboard/leaderboard.css b/frappe/desk/page/leaderboard/leaderboard.css index dbe9cca5b8..a3cb4d09c4 100644 --- a/frappe/desk/page/leaderboard/leaderboard.css +++ b/frappe/desk/page/leaderboard/leaderboard.css @@ -19,6 +19,14 @@ background: #f0f4f7; } +.from-date-field .clearfix{ + display: none; +} + +.from-date-field { + margin-left: 10px; +} + .select-time:focus, .select-doctype:focus, .select-filter:focus, .select-sort:focus { background: #f0f4f7; } diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js index 3e4c36add0..c64d2dcb4f 100644 --- a/frappe/desk/page/leaderboard/leaderboard.js +++ b/frappe/desk/page/leaderboard/leaderboard.js @@ -41,7 +41,11 @@ class Leaderboard { return field; }); } - this.timespans = ["Week", "Month", "Quarter", "Year", "All Time"]; + this.timespans = [ + "This Week", "This Month", "This Quarter", "This Year", + "Last Week", "Last Month", "Last Quarter", "Last Year", + "All Time", "Select From Date" + ]; // for saving current selected filters const _initial_doctype = frappe.get_route()[1] || this.doctypes[0]; @@ -103,7 +107,8 @@ class Leaderboard { this.timespans.map(d => { return {"label": __(d), value: d }; }) - ); + ); + this.create_from_date_field(); this.type_select = this.page.add_select(__("Field"), this.options.selected_filter.map(d => { @@ -113,7 +118,12 @@ class Leaderboard { this.timespan_select.on("change", (e) => { this.options.selected_timespan = e.currentTarget.value; - this.make_request(); + if (this.options.selected_timespan === 'Select From Date') { + this.from_date_field.show(); + } else { + this.from_date_field.hide(); + this.make_request(); + } }); this.type_select.on("change", (e) => { @@ -122,6 +132,28 @@ class Leaderboard { }); } + create_from_date_field() { + let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`); + this.from_date_field = $(`
`).insertAfter(timespan_field).hide(); + + let date_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'Date', + fieldname: 'selected_from_date', + placeholder: frappe.datetime.month_start(), + default: frappe.datetime.month_start(), + input_class: 'input-sm', + reqd: 1, + change: () => { + this.selected_from_date = date_field.get_value(); + if (this.selected_from_date) this.make_request(); + } + }, + parent: $(this.parent).find('.from-date-field'), + render_input: 1 + }); + } + render_selected_doctype() { this.$sidebar_list.on("click", "li", (e)=> { @@ -207,7 +239,6 @@ class Leaderboard { this.leaderboard_config[this.options.selected_doctype].method, { 'from_date': this.get_from_date(), - 'timespan': this.options.selected_timespan, 'company': this.options.selected_company, 'field': this.options.selected_filter_item, 'limit': this.leaderboard_limit, @@ -360,17 +391,20 @@ class Leaderboard { get_from_date() { let timespan = this.options.selected_timespan.toLowerCase(); let current_date = frappe.datetime.now_date(); - let date = ''; - if (timespan === "month") { - date = frappe.datetime.add_months(current_date, -1); - } else if (timespan === "quarter") { - date = frappe.datetime.add_months(current_date, -3); - } else if (timespan === "year") { - date = frappe.datetime.add_months(current_date, -12); - } else if (timespan === "week") { - date = frappe.datetime.add_days(current_date, -7); + let get_from_date = { + "this week": frappe.datetime.week_start(), + "this month": frappe.datetime.month_start(), + "this quarter": frappe.datetime.quarter_start(), + "this year": frappe.datetime.year_start(), + "last week": frappe.datetime.add_days(current_date, -7), + "last month": frappe.datetime.add_months(current_date, -1), + "last quarter": frappe.datetime.add_months(current_date, -3), + "last year": frappe.datetime.add_months(current_date, -12), + "all time": "", + "select from date": this.selected_from_date || frappe.datetime.month_start() } - return date; + + return get_from_date[timespan]; } } diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py index bb598ab180..e7e147fb7d 100644 --- a/frappe/desk/page/setup_wizard/install_fixtures.py +++ b/frappe/desk/page/setup_wizard/install_fixtures.py @@ -10,6 +10,7 @@ from frappe.desk.doctype.global_search_settings.global_search_settings import up def install(): update_genders_and_salutations() update_global_search_doctypes() + setup_email_linking() @frappe.whitelist() def update_genders_and_salutations(): @@ -20,13 +21,12 @@ def update_genders_and_salutations(): for record in records: doc = frappe.new_doc(record.get("doctype")) doc.update(record) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) - try: - doc.insert(ignore_permissions=True) - except frappe.DuplicateEntryError as e: - # pass DuplicateEntryError and continue - if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name: - # make sure DuplicateEntryError is for the exact same doc and not a related doc - pass - else: - raise \ No newline at end of file +def setup_email_linking(): + doc = frappe.get_doc({ + "doctype": "Email Account", + "email_id": "email_linking@example.com", + }) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + \ No newline at end of file diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 21a69f5111..7dc561193f 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -510,7 +510,7 @@ def has_match(row, linked_doctypes, doctype_match_filters, ref_doctype, if_owner cell_value = None if isinstance(row, dict): cell_value = row.get(idx) - elif isinstance(row, list): + elif isinstance(row, (list, tuple)): cell_value = row[idx] if dt in match_filters and cell_value not in match_filters.get(dt) and frappe.db.exists(dt, cell_value): diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 5db6ae18bf..9caf72d3bd 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -267,18 +267,14 @@ def get_sidebar_stats(stats, doctype, filters=[]): data = frappe._dict(frappe.local.form_dict) filters = json.loads(data["filters"]) - if not frappe.cache().hget("Tags", doctype): - tags = set([tag.tag for tag in frappe.get_list("Tag Link", filters={"document_type": doctype}, fields=["tag"])]) - frappe.cache().hset("Tags", doctype, tags) - - for tag in list(frappe.cache().hget("Tags", doctype)): + for tag in frappe.get_all("Tag Link", filters={"document_type": doctype}, fields=["tag"]): tag_filters = [] tag_filters.extend(filters) - tag_filters.extend([['Tag Link', 'tag', '=', tag]]) + tag_filters.extend([['Tag Link', 'tag', '=', tag.tag]]) count = frappe.get_all(doctype, filters=tag_filters, fields=["count(*)"]) if count[0].get("count(*)") > 0: - _user_tags.append([tag, count[0].get("count(*)")]) + _user_tags.append([tag.tag, count[0].get("count(*)")]) return {"stats": {"_user_tags": _user_tags}} diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 7bcfe646ab..c70b650945 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -50,7 +50,7 @@ def sanitize_searchfield(searchfield): # this is called by the Link Field @frappe.whitelist() def search_link(doctype, txt, query=None, filters=None, page_length=20, searchfield=None, reference_doctype=None, ignore_user_permissions=False): - search_widget(doctype, txt, query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions) + search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions) frappe.response['results'] = build_for_autosuggest(frappe.response["values"]) del frappe.response["values"] diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index c05a0f3fe4..495644f652 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -322,16 +322,16 @@ class EmailAccount(Document): unhandled_email.insert(ignore_permissions=True) frappe.db.commit() - def insert_communication(self, msg, args={}): + def insert_communication(self, msg, args=None): if isinstance(msg, list): raw, uid, seen = msg else: raw = msg uid = -1 seen = 0 - - if args.get("uid", -1): uid = args.get("uid", -1) - if args.get("seen", 0): seen = args.get("seen", 0) + if isinstance(args, dict): + if args.get("uid", -1): uid = args.get("uid", -1) + if args.get("seen", 0): seen = args.get("seen", 0) email = Email(raw) @@ -355,7 +355,7 @@ class EmailAccount(Document): name = names[0].get("name") # email is already available update communication uid instead frappe.db.set_value("Communication", name, "uid", uid, update_modified=False) - return + return frappe.get_doc("Communication", name) if email.content_type == 'text/html': email.content = clean_email_html(email.content) diff --git a/frappe/email/queue.py b/frappe/email/queue.py index 792b47296a..1c9a2fd3de 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -3,22 +3,25 @@ from __future__ import unicode_literals import frappe +import sys from six.moves import html_parser as HTMLParser import smtplib, quopri, json -from frappe import msgprint, throw, _, safe_decode +from frappe import msgprint, _, safe_decode from frappe.email.smtp import SMTPServer, get_outgoing_email_account from frappe.email.email_body import get_email, get_formatted_html, add_attachment from frappe.utils.verified_command import get_signed_params, verify_request from html2text import html2text -from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails, cstr, cint +from frappe.utils import get_url, nowdate, now_datetime, add_days, split_emails, cstr, cint from rq.timeouts import JobTimeoutException -from six import text_type, string_types +from six import text_type, string_types, PY3 +from email.parser import Parser + class EmailLimitCrossedError(frappe.ValidationError): pass def send(recipients=None, sender=None, subject=None, message=None, text_content=None, reference_doctype=None, reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, - attachments=None, reply_to=None, cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, + attachments=None, reply_to=None, cc=None, bcc=None, message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, send_priority=1, communication=None, now=False, read_receipt=None, queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None, header=None, print_letterhead=False): @@ -52,6 +55,11 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= if not recipients and not cc: return + if not cc: + cc = [] + if not bcc: + bcc = [] + if isinstance(recipients, string_types): recipients = split_emails(recipients) @@ -68,7 +76,6 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= if not sender or sender == "Administrator": sender = email_account.default_sender - if not text_content: try: text_content = html2text(message) @@ -404,7 +411,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals message = prepare_message(email, recipient.recipient, recipients_list) if not frappe.flags.in_test: - smtpserver.sess.sendmail(email.sender, recipient.recipient, encode(message)) + smtpserver.sess.sendmail(email.sender, recipient.recipient, message) recipient.status = "Sent" frappe.db.sql("""update `tabEmail Queue Recipient` set status='Sent', modified=%s where name=%s""", @@ -509,37 +516,41 @@ def prepare_message(email, recipient, recipients_list): message = (message and message.encode('utf8')) or '' message = safe_decode(message) - if not email.attachments: - return message - # On-demand attachments - from email.parser import Parser + if PY3: + from email.policy import SMTPUTF8 + message = Parser(policy=SMTPUTF8).parsestr(message) + else: + message = Parser().parsestr(message) - msg_obj = Parser().parsestr(message) - attachments = json.loads(email.attachments) + if email.attachments: + # On-demand attachments - for attachment in attachments: - if attachment.get('fcontent'): continue + attachments = json.loads(email.attachments) - fid = attachment.get("fid") - if fid: - _file = frappe.get_doc("File", fid) - fcontent = _file.get_content() - attachment.update({ - 'fname': _file.file_name, - 'fcontent': fcontent, - 'parent': msg_obj - }) - attachment.pop("fid", None) - add_attachment(**attachment) + for attachment in attachments: + if attachment.get('fcontent'): + continue - elif attachment.get("print_format_attachment") == 1: - attachment.pop("print_format_attachment", None) - print_format_file = frappe.attach_print(**attachment) - print_format_file.update({"parent": msg_obj}) - add_attachment(**print_format_file) + fid = attachment.get("fid") + if fid: + _file = frappe.get_doc("File", fid) + fcontent = _file.get_content() + attachment.update({ + 'fname': _file.file_name, + 'fcontent': fcontent, + 'parent': message + }) + attachment.pop("fid", None) + add_attachment(**attachment) - return msg_obj.as_string() + elif attachment.get("print_format_attachment") == 1: + attachment.pop("print_format_attachment", None) + print_format_file = frappe.attach_print(**attachment) + print_format_file.update({"parent": message}) + add_attachment(**print_format_file) + + return message.as_string() def clear_outbox(): """Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days. diff --git a/frappe/email/receive.py b/frappe/email/receive.py index bccad1fec5..e5c8457b4e 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -456,9 +456,9 @@ class Email: def show_attached_email_headers_in_content(self, part): # get the multipart/alternative message try: - from html import escape # python 3.x + from html import escape # python 3.x except ImportError: - from cgi import escape # python 2.x + from cgi import escape # python 2.x message = list(part.walk())[1] headers = [] @@ -480,7 +480,7 @@ class Email: """Detect chartset.""" charset = part.get_content_charset() if not charset: - charset = chardet.detect(frappe.safe_encode(part))['encoding'] + charset = chardet.detect(cstr(part))['encoding'] return charset @@ -514,7 +514,7 @@ class Email: 'fcontent': fcontent, }) - cid = (part.get("Content-Id") or "").strip("><") + cid = (cstr(part.get("Content-Id")) or "").strip("><") if cid: self.cid_map[fname] = cid diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index feb8e80007..26c4e5ba5d 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -5,7 +5,10 @@ from __future__ import unicode_literals import unittest, os, base64 from frappe.email.receive import Email from frappe.email.email_body import (replace_filename_with_cid, - get_email, inline_style_in_html, get_header) + get_email, inline_style_in_html, get_header) +from frappe.email.queue import prepare_message, get_email_queue +from six import PY3 + class TestEmailBody(unittest.TestCase): def setUp(self): @@ -37,6 +40,53 @@ This is the text version of this email text_content=email_text ).as_string() + def test_prepare_message_returns_already_encoded_string(self): + + if PY3: + uni_chr1 = chr(40960) + uni_chr2 = chr(1972) + else: + uni_chr1 = unichr(40960) + uni_chr2 = unichr(1972) + + email = get_email_queue( + recipients=['test@example.com'], + sender='me@example.com', + subject='Test Subject', + content='

' + uni_chr1 + 'abcd' + uni_chr2 + '

', + formatted='

' + uni_chr1 + 'abcd' + uni_chr2 + '

', + text_content='whatever') + result = prepare_message(email=email, recipient='test@test.com', recipients_list=[]) + self.assertTrue("

=EA=80=80abcd=DE=B4

" in result) + + def test_prepare_message_returns_cr_lf(self): + email = get_email_queue( + recipients=['test@example.com'], + sender='me@example.com', + subject='Test Subject', + content='

\n this is a test of newlines\n' + '

', + formatted='

\n this is a test of newlines\n' + '

', + text_content='whatever') + result = prepare_message(email=email, recipient='test@test.com', recipients_list=[]) + if PY3: + self.assertTrue(result.count('\n') == result.count("\r")) + else: + self.assertTrue(True) + + def test_rfc_5322_header_is_wrapped_at_998_chars(self): + # unfortunately the db can only hold 140 chars so this can't be tested properly. test at max chars anyway. + email = get_email_queue( + recipients=['test@example.com'], + sender='me@example.com', + subject='Test Subject', + content='

Whatever

', + text_content='whatever', + message_id= "a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" + + ".really.long.message.id.that.should.not.wrap.unti") + result = prepare_message(email=email, recipient='test@test.com', recipients_list=[]) + self.assertTrue( + "a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" + + ".really.long.message.id.that.should.not.wrap.unti" in result) def test_image(self): img_signature = ''' @@ -49,7 +99,6 @@ Content-Disposition: inline; filename="favicon.png" self.assertTrue(img_signature in self.email_string) self.assertTrue(self.img_base64 in self.email_string) - def test_text_content(self): text_content = ''' Content-Type: text/plain; charset="utf-8" @@ -62,7 +111,6 @@ This is the text version of this email ''' self.assertTrue(text_content in self.email_string) - def test_email_content(self): html_head = ''' Content-Type: text/html; charset="utf-8" @@ -79,7 +127,6 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> self.assertTrue(html_head in self.email_string) self.assertTrue(html in self.email_string) - def test_replace_filename_with_cid(self): original_message = '''
@@ -152,6 +199,7 @@ Reply-To: test2_@erpnext.com mail = Email(content_bytes) self.assertEqual(mail.text_content, text_content) + def fixed_column_width(string, chunk_size): - parts = [string[0+i:chunk_size+i] for i in range(0, len(string), chunk_size)] - return '\n'.join(parts) \ No newline at end of file + parts = [string[0 + i:chunk_size + i] for i in range(0, len(string), chunk_size)] + return '\n'.join(parts) diff --git a/frappe/geo/doctype/currency/currency.js b/frappe/geo/doctype/currency/currency.js index 1bc9865999..af2d6ebc4e 100644 --- a/frappe/geo/doctype/currency/currency.js +++ b/frappe/geo/doctype/currency/currency.js @@ -7,10 +7,5 @@ frappe.ui.form.on('Currency', { if(!frm.doc.enabled) { frm.set_intro(__("This Currency is disabled. Enable to use in transactions")); } - }, - - after_save(frm) { - if (frm.doc.enabled) - locals[':Currency'][frm.doc.name] = Object.assign(frm.doc, { doctype: ':Currency' }); } }); diff --git a/frappe/installer.py b/frappe/installer.py index f691a6cb22..4b07ab8ce8 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -21,13 +21,13 @@ from frappe.database import setup_database from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs def install_db(root_login="root", root_password=None, db_name=None, source_sql=None, - admin_password=None, verbose=True, force=0, site_config=None, reinstall=False, - db_type=None): + admin_password=None, verbose=True, force=0, site_config=None, reinstall=False, + db_type=None, db_host=None, db_port=None): if not db_type: db_type = frappe.conf.db_type or 'mariadb' - make_conf(db_name, site_config=site_config, db_type=db_type) + make_conf(db_name, site_config=site_config, db_type=db_type, db_host=db_host, db_port=db_port) frappe.flags.in_install_db = True frappe.flags.root_login = root_login @@ -191,14 +191,14 @@ def init_singles(): doc.flags.ignore_validate=True doc.save() -def make_conf(db_name=None, db_password=None, site_config=None, db_type=None): +def make_conf(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None): site = frappe.local.site - make_site_config(db_name, db_password, site_config, db_type=db_type) + make_site_config(db_name, db_password, site_config, db_type=db_type, db_host=db_host, db_port=db_port) sites_path = frappe.local.sites_path frappe.destroy() frappe.init(site, sites_path=sites_path) -def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None): +def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None): frappe.create_folder(os.path.join(frappe.local.site_path)) site_file = get_site_config_path() @@ -209,6 +209,12 @@ def make_site_config(db_name=None, db_password=None, site_config=None, db_type=N if db_type: site_config['db_type'] = db_type + if db_host: + site_config['db_host'] = db_host + + if db_port: + site_config['db_port'] = db_port + with open(site_file, "w") as f: f.write(json.dumps(site_config, indent=1, sort_keys=True)) diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.json b/frappe/integrations/doctype/social_login_key/social_login_key.json index 290aae0e4e..6c0fbdb26c 100644 --- a/frappe/integrations/doctype/social_login_key/social_login_key.json +++ b/frappe/integrations/doctype/social_login_key/social_login_key.json @@ -157,7 +157,7 @@ "label": "User ID Property" } ], - "modified": "2019-12-03 12:35:55.115260", + "modified": "2019-12-03 13:13:46.989099", "modified_by": "Administrator", "module": "Integrations", "name": "Social Login Key", diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index 6dd0ff3eec..f697d8051a 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -61,7 +61,9 @@ def set_user_and_static_default_values(doc): user_default_value = get_user_default_value(df, defaults, doctype_user_permissions, allowed_records, default_doc) if user_default_value is not None: - doc.set(df.fieldname, user_default_value) + # if fieldtype is link check if doc exists + if not df.fieldtype == "Link" or frappe.db.exists(df.options, user_default_value): + doc.set(df.fieldname, user_default_value) else: if df.fieldname != doc.meta.title_field: diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 64e242d6af..f9016d7fcf 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -501,6 +501,10 @@ class DatabaseQuery(object): value = f.value or "''" fallback = "''" + elif f.fieldname == 'name': + value = f.value or "''" + fallback = "''" + else: value = flt(f.value) fallback = 0 diff --git a/frappe/patches.txt b/frappe/patches.txt index 90abc8c31d..1599fd50ac 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -260,4 +260,6 @@ frappe.patches.v12_0.update_auto_repeat_status_and_not_submittable frappe.patches.v12_0.copy_to_parent_for_tags frappe.patches.v12_0.create_notification_settings_for_user frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26 +frappe.patches.v12_0.setup_email_linking +frappe.patches.v12_0.fix_home_settings_for_all_users execute:frappe.delete_doc("Test Runner") \ No newline at end of file diff --git a/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py b/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py index dd212d157e..f7b9e476a9 100644 --- a/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py +++ b/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py @@ -3,6 +3,9 @@ import frappe def execute(): + if frappe.db.count("File", filters={"attached_to_doctype": "Prepared Report", "is_private": 0}) > 10000: + frappe.db.auto_commit_on_many_writes = True + files = frappe.get_all("File", fields=["name", "attached_to_name"], filters={"attached_to_doctype": "Prepared Report", "is_private": 0}) for file_dict in files: # For some reason Prepared Report doc might not exist, check if it exists first @@ -17,3 +20,7 @@ def execute(): else: # If Prepared Report doc doesn't exist then the file doc is useless. Delete it. frappe.delete_doc("File", file_dict.name) + + if frappe.db.auto_commit_on_many_writes: + frappe.db.auto_commit_on_many_writes = False + diff --git a/frappe/patches/v12_0/fix_home_settings_for_all_users.py b/frappe/patches/v12_0/fix_home_settings_for_all_users.py new file mode 100644 index 0000000000..e26cbd9eef --- /dev/null +++ b/frappe/patches/v12_0/fix_home_settings_for_all_users.py @@ -0,0 +1,41 @@ +import frappe +from frappe.config import get_modules_from_all_apps_for_user +import json +def execute(): + users = frappe.get_all('User', fields=['name', 'home_settings']) + + for user in users: + + if not user.home_settings: + continue + + home_settings = json.loads(user.home_settings) + + modules_by_category = home_settings.get('modules_by_category') + if not modules_by_category: + continue + visible_modules = [] + category_to_check = [] + + for category, modules in modules_by_category.items(): + visible_modules += modules + category_to_check.append(category) + + all_modules = get_modules_from_all_apps_for_user(user.name) + all_modules = set([m.get('name') or m.get('module_name') or m.get('label') \ + for m in all_modules if m.get('category') in category_to_check]) + + hidden_modules = home_settings.get("hidden_modules", []) + + modules_in_home_settings = set(visible_modules + hidden_modules) + + all_modules = all_modules.union(modules_in_home_settings) + + missing_modules = all_modules - modules_in_home_settings + + if missing_modules: + home_settings['hidden_modules'] = hidden_modules + list(missing_modules) + home_settings = json.dumps(home_settings) + frappe.set_value('User', user.name, 'home_settings', home_settings) + + frappe.cache().delete_key('home_settings') diff --git a/frappe/patches/v12_0/setup_email_linking.py b/frappe/patches/v12_0/setup_email_linking.py new file mode 100644 index 0000000000..08f57ca5e4 --- /dev/null +++ b/frappe/patches/v12_0/setup_email_linking.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals + +from frappe.desk.page.setup_wizard.install_fixtures import setup_email_linking + +def execute(): + setup_email_linking() \ No newline at end of file diff --git a/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json b/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json index 2f51d2e18a..e0125bad3c 100644 --- a/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json +++ b/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json @@ -12,9 +12,10 @@ } ], "idx": 0, - "image_src": "/assets/erpnext/images/illustrations/letterhead-onboard.png", + "image_src": "", + "is_completed": 1, "max_count": 0, - "modified": "2019-12-03 22:54:57.618989", + "modified": "2019-12-09 15:12:45.588567", "modified_by": "Administrator", "name": "Company Letter Head", "owner": "Administrator", diff --git a/frappe/public/js/frappe/change_log.html b/frappe/public/js/frappe/change_log.html index c05aadfe27..fce6539abc 100644 --- a/frappe/public/js/frappe/change_log.html +++ b/frappe/public/js/frappe/change_log.html @@ -6,11 +6,13 @@ {{ app_info.title }} {{ __("updated to {0}", [app_info.version]) }} +
{% for (var x=0, y=app_info.change_log.length; x < y; x++) { var version_info = app_info.change_log[x]; if(version_info) { %}

{{ frappe.markdown(version_info[1]) }}

{% } } %} +
{% } %} diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 92194acdca..23d7bffec2 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -466,12 +466,27 @@ frappe.Application = Class.extend({ show_change_log: function() { var me = this; - var d = frappe.msgprint( - frappe.render_template("change_log", {"change_log": frappe.boot.change_log}), - __("Updated To New Version") - ); - d.keep_open = true; - d.custom_onhide = function() { + let change_log = frappe.boot.change_log; + + // frappe.boot.change_log = [{ + // "change_log": [ + // [, ], + // [, ], + // ], + // "description": "ERP made simple", + // "title": "ERPNext", + // "version": "12.2.0" + // }]; + + // Iterate over changelog + var change_log_dialog = frappe.msgprint({ + message: frappe.render_template("change_log", {"change_log": change_log}), + title: __("Updated To New Version 🎉"), + wide: true, + scroll: true + }); + change_log_dialog.keep_open = true; + change_log_dialog.custom_onhide = function() { frappe.call({ "method": "frappe.utils.change_log.update_last_known_versions" }); diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index 6411c2fb2b..819ecb526e 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -291,7 +291,7 @@ frappe.get_modal = function(title, content) {