diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 0000000000..4754a63e7e --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,2 @@ +exclude_paths: + - '**.sql' \ No newline at end of file diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000000..c11c0ab6a3 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,2 @@ +disable=access-member-before-definition +disable=no-member \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index a5c81f9631..4e35a97208 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python dist: trusty -sudo: required addons: hosts: @@ -54,6 +53,16 @@ matrix: env: DB=mariadb TYPE=server script: bench --site test_site run-tests --coverage --junit-xml-output frappe_unit_tests.xml +before_install: + # install wkhtmltopdf + - wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz + - tar -xf /tmp/wkhtmltox.tar.xz -C /tmp + - sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf + - sudo chmod o+x /usr/local/bin/wkhtmltopdf + + # install cups + - sudo apt-get install libcups2-dev + install: - cd ~ - source ./.nvm/nvm.sh @@ -67,23 +76,20 @@ install: - mkdir ~/frappe-bench/sites/test_site - cp $TRAVIS_BUILD_DIR/.travis/$DB.json ~/frappe-bench/sites/test_site/site_config.json - - mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'" - - mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + - if [ $DB == "mariadb" ];then + mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"; + mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; + mysql -u root -e "CREATE DATABASE test_frappe"; + mysql -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"; + mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"; + mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"; + mysql -u root -e "FLUSH PRIVILEGES"; + fi - - mysql -u root -e "CREATE DATABASE test_frappe" - - mysql -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'" - - mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'" - - - mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'" - - mysql -u root -e "FLUSH PRIVILEGES" - - - psql -c "CREATE DATABASE test_frappe" -U postgres - - psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres - - - wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz - - tar -xf /tmp/wkhtmltox.tar.xz -C /tmp - - sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf - - sudo chmod o+x /usr/local/bin/wkhtmltopdf + - if [ $DB == "postgres" ];then + psql -c "CREATE DATABASE test_frappe" -U postgres; + psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres; + fi - cd ./frappe-bench diff --git a/cypress/integration/form.js b/cypress/integration/form.js index b7ddd6ecb7..5a8b85d19e 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -1,9 +1,15 @@ context('Form', () => { - before(() => { + beforeEach(() => { cy.login(); cy.visit('/desk'); }); - + before(() => { + cy.login(); + cy.visit('/desk'); + cy.window().its('frappe').then(frappe => { + frappe.call("frappe.tests.ui_test_helpers.create_contact_records"); + }); + }); 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 +19,21 @@ 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(); + cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible'); + cy.get('.modal-backdrop').click(); + cy.get('.next-doc').click(); + 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 new file mode 100644 index 0000000000..67fdb8acf0 --- /dev/null +++ b/cypress/integration/grid_pagination.js @@ -0,0 +1,51 @@ +context('Grid Pagination', () => { + beforeEach(() => { + cy.login(); + cy.visit('/desk'); + }); + before(() => { + cy.login(); + cy.visit('/desk'); + cy.window().its('frappe').then(frappe => { + frappe.call("frappe.tests.ui_test_helpers.create_contact_phone_nos_records"); + }); + }); + it('creates pages for child table', () => { + 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', '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', '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'); + }); + it('adds and deletes rows and changes page', ()=> { + cy.visit('/desk#Form/Contact/Test Contact'); + 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', '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', '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/list_view.js b/cypress/integration/list_view.js index 20a973c1dd..d6627ea9c4 100644 --- a/cypress/integration/list_view.js +++ b/cypress/integration/list_view.js @@ -2,10 +2,9 @@ context('List View', () => { before(() => { cy.login(); cy.visit('/desk'); - cy.window().its('frappe').then(frappe => { - frappe.call("frappe.tests.ui_test_helpers.setup_workflow"); + return cy.window().its('frappe').then(frappe => { + return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow"); }); - cy.clear_cache(); }); it('enables "Actions" button', () => { const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Print', 'Delete']; diff --git a/frappe/__init__.py b/frappe/__init__.py index 1ee5db416b..cb1b6e5358 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.17' +__version__ = '12.0.20' __title__ = "Frappe Framework" local = Local() @@ -123,7 +123,6 @@ def init(site, sites_path=None, new_site=False): local.debug_log = [] local.realtime_log = [] local.flags = _dict({ - "ran_schedulers": [], "currently_saving": [], "redirect_location": "", "in_install_db": False, @@ -1508,7 +1507,22 @@ def logger(module=None, with_more_info=True): def log_error(message=None, title=None): '''Log error to Error Log''' - return get_doc(dict(doctype='Error Log', error=as_unicode(message or get_traceback()), + + # AI ALERT: + # the title and message may be swapped + # the better API for this is log_error(title, message), and used in many cases this way + # this hack tries to be smart about whats a title (single line ;-)) and fixes it + + if message: + if '\n' not in message: + title = message + error = get_traceback() + else: + error = message + else: + error = get_traceback() + + return get_doc(dict(doctype='Error Log', error=as_unicode(error), method=title)).insert(ignore_permissions=True) def get_desk_link(doctype, name): diff --git a/frappe/api.py b/frappe/api.py index b61c535edd..95a9a408a5 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -82,7 +82,7 @@ def handle(): if frappe.local.request.method=="PUT": if frappe.local.form_dict.data is None: - data = json.loads(frappe.local.request.get_data()) + data = json.loads(frappe.safe_decode(frappe.local.request.get_data())) else: data = json.loads(frappe.local.form_dict.data) doc = frappe.get_doc(doctype, name) @@ -117,7 +117,7 @@ def handle(): if frappe.local.request.method=="POST": if frappe.local.form_dict.data is None: - data = json.loads(frappe.local.request.get_data()) + data = json.loads(frappe.safe_decode(frappe.local.request.get_data())) else: data = json.loads(frappe.local.form_dict.data) data.update({ diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index a950669d12..27f17a1a62 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -105,10 +105,6 @@ class AutoRepeat(Document): schedule_details = [] start_date = getdate(self.start_date) end_date = getdate(self.end_date) - today = frappe.utils.datetime.date.today() - - if start_date < today: - start_date = today if not self.end_date: start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day) @@ -121,7 +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 = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day) + 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) while (getdate(start_date) < getdate(end_date)): row = { "reference_document" : self.reference_document, @@ -129,7 +126,8 @@ class AutoRepeat(Document): "next_scheduled_date" : start_date } schedule_details.append(row) - start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, 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) return schedule_details @@ -271,18 +269,28 @@ class AutoRepeat(Document): ) -def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_day = False, end_date = None): +def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_day=False, end_date=None, for_full_schedule=False): month_count = month_map.get(frequency) + day_count = 0 if month_count and repeat_on_last_day: next_date = get_next_date(start_date, month_count, 31) + day_count = 31 + next_date = get_next_date(start_date, month_count, day_count) elif month_count and repeat_on_day: next_date = get_next_date(start_date, month_count, repeat_on_day) + day_count = repeat_on_day + next_date = get_next_date(start_date, month_count, day_count) elif month_count: next_date = get_next_date(start_date, month_count) else: days = 7 if frequency == 'Weekly' else 1 next_date = add_days(start_date, days) + # next schedule date should be after or on current date + if not for_full_schedule: + while getdate(next_date) < getdate(today()): + next_date = get_next_date(next_date, month_count, day_count) + return next_date def get_next_date(dt, mcount, day=None): @@ -307,7 +315,7 @@ def create_repeated_entries(data): current_date = getdate(today()) schedule_date = getdate(doc.next_schedule_date) - while schedule_date <= current_date and not doc.disabled: + if schedule_date == current_date and not doc.disabled: doc.create_documents() schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date) diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index cb98d4a8fd..95f95f3e7d 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -96,6 +96,21 @@ class TestAutoRepeat(unittest.TestCase): linked_comm = frappe.db.exists("Communication", dict(reference_doctype="ToDo", reference_name=new_todo)) self.assertTrue(linked_comm) + def test_next_schedule_date(self): + current_date = getdate(today()) + todo = frappe.get_doc( + dict(doctype='ToDo', description='test next schedule date todo', assigned_by='Administrator')).insert() + doc = make_auto_repeat(frequency='Monthly', reference_document=todo.name, start_date=add_months(today(), -2)) + + #check next_schedule_date is set as per current date + #it should not be a previous month's date + self.assertEqual(doc.next_schedule_date, current_date) + data = get_auto_repeat_entries(current_date) + create_repeated_entries(data) + docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name}) + #the original doc + the repeated doc + self.assertEqual(len(docnames), 2) + def make_auto_repeat(**args): args = frappe._dict(args) 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/commands/utils.py b/frappe/commands/utils.py index 9a408430e7..d29f0a9c97 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -507,26 +507,6 @@ def run_ui_tests(context, app, headless=False): formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open) frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True) -@click.command('run-setup-wizard-ui-test') -@click.option('--app', help="App to run tests on, leave blank for all apps") -@click.option('--profile', is_flag=True, default=False) -@pass_context -def run_setup_wizard_ui_test(context, app=None, profile=False): - "Run setup wizard UI test" - import frappe.test_runner - - site = get_site(context) - frappe.init(site=site) - frappe.connect() - - ret = frappe.test_runner.run_setup_wizard_ui_test(app=app, verbose=context.verbose, - profile=profile) - if len(ret.failures) == 0 and len(ret.errors) == 0: - ret = 0 - - if os.environ.get('CI'): - sys.exit(ret) - @click.command('serve') @click.option('--port', default=8000) @click.option('--profile', is_flag=True, default=False) @@ -752,7 +732,6 @@ commands = [ reset_perms, run_tests, run_ui_tests, - run_setup_wizard_ui_test, serve, set_config, show_config, diff --git a/frappe/config/settings.py b/frappe/config/settings.py index 2422f2fae2..a0a7dcd65f 100644 --- a/frappe/config/settings.py +++ b/frappe/config/settings.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import frappe from frappe import _ from frappe.desk.moduleview import add_setup_section @@ -88,7 +89,7 @@ def get_data(): ] }, { - "label": _("Email"), + "label": _("Email / Notifications"), "icon": "fa fa-envelope", "items": [ { @@ -120,6 +121,12 @@ def get_data(): "type": "doctype", "name": "Newsletter", "description": _("Create and manage newsletter") + }, + { + "type": "doctype", + "route": "Form/Notification Settings/{}".format(frappe.session.user), + "name": "Notification Settings", + "description": _("Configure notifications for mentions, assignments, energy points and more.") } ] }, diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 21ed88addb..75fd0ad8c6 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -60,10 +60,6 @@ class Address(Document): if not [row for row in self.links if row.link_doctype == "Company"]: frappe.throw(_("Company is mandatory, as it is your company address")) - # removing other links - to_remove = [row for row in self.links if row.link_doctype != "Company"] - [ self.remove(row) for row in to_remove ] - def get_display(self): return get_address_display(self.as_dict()) diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index a285941c68..1848136bee 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -10,7 +10,6 @@ from email.utils import formataddr from frappe.core.utils import get_parent_doc from frappe.utils import (get_url, get_formatted_email, cint, validate_email_address, split_emails, time_diff_in_seconds, parse_addr, get_datetime) -from frappe.utils.scheduler import log from frappe.email.email_body import get_message_id import frappe.email.smtp import time @@ -399,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): @@ -412,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): @@ -509,17 +510,7 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments break except: - traceback = log("frappe.core.doctype.communication.email.sendmail", frappe.as_json({ - "communication_name": communication_name, - "print_html": print_html, - "print_format": print_format, - "attachments": attachments, - "recipients": recipients, - "cc": cc, - "bcc": bcc, - "lang": lang - })) - frappe.logger(__name__).error(traceback) + traceback = frappe.log_error("frappe.core.doctype.communication.email.sendmail") raise def update_mins_to_first_communication(parent, communication): diff --git a/frappe/core/doctype/data_import/importer_new.py b/frappe/core/doctype/data_import/importer_new.py index 6fccbc89ef..f14f38c56d 100644 --- a/frappe/core/doctype/data_import/importer_new.py +++ b/frappe/core/doctype/data_import/importer_new.py @@ -54,8 +54,10 @@ class Importer: extension = None if self.data_import and self.data_import.import_file: file_doc = frappe.get_doc("File", {"file_url": self.data_import.import_file}) + parts = file_doc.get_extension() + extension = parts[1] content = file_doc.get_content() - extension = file_doc.file_name.split(".")[1] + extension = extension.lstrip(".") if file_path: content, extension = self.read_file(file_path) @@ -79,6 +81,12 @@ class Importer: return file_content, extn def read_content(self, content, extension): + error_title = _("Template Error") + if extension not in ("csv", "xlsx", "xls"): + frappe.throw( + _("Import template should be of type .csv, .xlsx or .xls"), title=error_title + ) + if extension == "csv": data = read_csv_content(content) elif extension == "xlsx": @@ -86,6 +94,11 @@ class Importer: elif extension == "xls": data = read_xls_file_from_attached_file(content) + if len(data) <= 1: + frappe.throw( + _("Import template should contain a Header and atleast one row."), title=error_title + ) + self.header_row = data[0] self.data = data[1:] @@ -862,15 +875,15 @@ class Importer: if failed_records: print("Failed to import {0} records".format(len(failed_records))) - file_name = '{0}_import_on_{1}.txt'.format(self.doctype, frappe.utils.now()) - print('Check {0} for errors'.format(os.path.join('sites', file_name))) + file_name = "{0}_import_on_{1}.txt".format(self.doctype, frappe.utils.now()) + print("Check {0} for errors".format(os.path.join("sites", file_name))) text = "" for w in failed_records: - text += "Row Indexes: {0}\n".format(str(w.get('row_indexes', []))) - text += "Messages:\n{0}\n".format('\n'.join(w.get('messages', []))) - text += "Traceback:\n{0}\n\n".format(w.get('exception')) + text += "Row Indexes: {0}\n".format(str(w.get("row_indexes", []))) + text += "Messages:\n{0}\n".format("\n".join(w.get("messages", []))) + text += "Traceback:\n{0}\n\n".format(w.get("exception")) - with open(file_name, 'w') as f: + with open(file_name, "w") as f: f.write(text) diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index a6a5e258ee..4e3f2fd84a 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_rename": 1, "autoname": "Prompt", "creation": "2013-02-18 13:36:19", @@ -28,6 +29,7 @@ "name_case", "column_break_15", "description", + "documentation", "form_settings_section", "image_field", "timeline_field", @@ -57,6 +59,10 @@ "restrict_to_domain", "read_only", "in_create", + "actions_section", + "actions", + "links_section", + "links", "web_view", "has_web_view", "allow_guest_to_view", @@ -454,11 +460,39 @@ "fieldname": "nsm_parent_field", "fieldtype": "Data", "label": "Parent Field (Tree)" + }, + { + "description": "URL for documentation or help", + "fieldname": "documentation", + "fieldtype": "Data", + "label": "Documentation Link" + }, + { + "fieldname": "actions_section", + "fieldtype": "Section Break", + "label": "Actions" + }, + { + "fieldname": "actions", + "fieldtype": "Table", + "label": "Actions", + "options": "DocType Action" + }, + { + "fieldname": "links_section", + "fieldtype": "Section Break", + "label": "Links Section" + }, + { + "fieldname": "links", + "fieldtype": "Table", + "label": "Links", + "options": "DocType Link" } ], "icon": "fa fa-bolt", "idx": 6, - "modified": "2019-09-07 14:28:05.392490", + "modified": "2019-11-25 17:24:03.690192", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/core/doctype/test_runner/__init__.py b/frappe/core/doctype/doctype_action/__init__.py similarity index 100% rename from frappe/core/doctype/test_runner/__init__.py rename to frappe/core/doctype/doctype_action/__init__.py diff --git a/frappe/core/doctype/doctype_action/doctype_action.json b/frappe/core/doctype/doctype_action/doctype_action.json new file mode 100644 index 0000000000..7a1b845af3 --- /dev/null +++ b/frappe/core/doctype/doctype_action/doctype_action.json @@ -0,0 +1,57 @@ +{ + "actions": [], + "creation": "2019-09-23 16:28:13.953520", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "action_type", + "action", + "group" + ], + "fields": [ + { + "columns": 2, + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "fieldname": "group", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Group" + }, + { + "columns": 2, + "fieldname": "action_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Action Type", + "options": "Server Action", + "reqd": 1 + }, + { + "columns": 4, + "fieldname": "action", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Action", + "reqd": 1 + } + ], + "istable": 1, + "modified": "2019-09-24 09:11:39.860100", + "modified_by": "Administrator", + "module": "Core", + "name": "DocType Action", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/doctype_action/doctype_action.py b/frappe/core/doctype/doctype_action/doctype_action.py new file mode 100644 index 0000000000..a745c7da40 --- /dev/null +++ b/frappe/core/doctype/doctype_action/doctype_action.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class DocTypeAction(Document): + pass diff --git a/frappe/tests/ui/__init__.py b/frappe/core/doctype/doctype_link/__init__.py similarity index 100% rename from frappe/tests/ui/__init__.py rename to frappe/core/doctype/doctype_link/__init__.py diff --git a/frappe/core/doctype/doctype_link/doctype_link.json b/frappe/core/doctype/doctype_link/doctype_link.json new file mode 100644 index 0000000000..752b4bb5da --- /dev/null +++ b/frappe/core/doctype/doctype_link/doctype_link.json @@ -0,0 +1,46 @@ +{ + "actions": [], + "creation": "2019-09-24 11:41:25.291377", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "link_doctype", + "link_fieldname", + "group" + ], + "fields": [ + { + "fieldname": "link_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Link DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "link_fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Link Fieldname", + "reqd": 1 + }, + { + "fieldname": "group", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Group" + } + ], + "istable": 1, + "modified": "2019-09-24 11:41:25.291377", + "modified_by": "Administrator", + "module": "Core", + "name": "DocType Link", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/doctype_link/doctype_link.py b/frappe/core/doctype/doctype_link/doctype_link.py new file mode 100644 index 0000000000..efe8b09809 --- /dev/null +++ b/frappe/core/doctype/doctype_link/doctype_link.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class DocTypeLink(Document): + pass 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/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index 071bb4afc0..bc9a1fcdcd 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -25,7 +25,7 @@ class PreparedReport(Document): enqueue(run_background, prepared_report=self.name, timeout=6000) def on_trash(self): - remove_all("PreparedReport", self.name, from_delete=True) + remove_all("Prepared Report", self.name) def run_background(prepared_report): @@ -85,7 +85,8 @@ def create_json_gz_file(data, dt, dn): "file_name": json_filename, "attached_to_doctype": dt, "attached_to_name": dn, - "content": compressed_content + "content": compressed_content, + "is_private": 1 }) _file.save(ignore_permissions=True) diff --git a/frappe/core/doctype/scheduled_job_log/__init__.py b/frappe/core/doctype/scheduled_job_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.js b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.js new file mode 100644 index 0000000000..d43160c658 --- /dev/null +++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Scheduled Job Log', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json new file mode 100644 index 0000000000..9e7f72a722 --- /dev/null +++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.json @@ -0,0 +1,64 @@ +{ + "actions": [], + "creation": "2019-09-23 14:36:36.935869", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "status", + "scheduled_job_type", + "details" + ], + "fields": [ + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Scheduled\nSuccess\nFailed", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "details", + "fieldtype": "Code", + "label": "Details", + "read_only": 1 + }, + { + "fieldname": "scheduled_job_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Scheduled Job", + "options": "Scheduled Job Type", + "read_only": 1, + "reqd": 1 + } + ], + "links": [], + "modified": "2019-09-25 11:55:10.646458", + "modified_by": "Administrator", + "module": "Core", + "name": "Scheduled Job Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py new file mode 100644 index 0000000000..26871c9adf --- /dev/null +++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ScheduledJobLog(Document): + pass diff --git a/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py b/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py new file mode 100644 index 0000000000..1e5290425b --- /dev/null +++ b/frappe/core/doctype/scheduled_job_log/test_scheduled_job_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestScheduledJobLog(unittest.TestCase): + pass diff --git a/frappe/core/doctype/scheduled_job_type/__init__.py b/frappe/core/doctype/scheduled_job_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.js b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.js new file mode 100644 index 0000000000..55907b17fc --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Scheduled Job Type', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json new file mode 100644 index 0000000000..e2ec921679 --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -0,0 +1,98 @@ +{ + "actions": [ + { + "action": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event", + "action_type": "Server Action", + "label": "Execute" + } + ], + "creation": "2019-09-23 14:34:09.205368", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "stopped", + "method", + "frequency", + "cron_format", + "last_execution", + "create_log" + ], + "fields": [ + { + "fieldname": "method", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Method", + "read_only": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "stopped", + "fieldtype": "Check", + "label": "Stopped" + }, + { + "default": "0", + "depends_on": "eval:doc.queue==='All'", + "fieldname": "create_log", + "fieldtype": "Check", + "label": "Create Log" + }, + { + "fieldname": "last_execution", + "fieldtype": "Datetime", + "label": "Last Execution", + "read_only": 1 + }, + { + "allow_in_quick_entry": 1, + "depends_on": "eval:doc.queue==='Cron'", + "fieldname": "cron_format", + "fieldtype": "Data", + "label": "Cron Format", + "read_only": 1 + }, + { + "fieldname": "frequency", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Frequency", + "options": "All\nHourly\nHourly Long\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual", + "read_only": 1, + "reqd": 1 + } + ], + "in_create": 1, + "links": [ + { + "link_doctype": "Scheduled Job Log", + "link_fieldname": "scheduled_job_type" + } + ], + "modified": "2019-12-09 11:10:21.259929", + "modified_by": "Administrator", + "module": "Core", + "name": "Scheduled Job Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py new file mode 100644 index 0000000000..5d5bd35eec --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals + +import frappe, json +from frappe.model.document import Document +from frappe.utils import now_datetime, get_datetime +from datetime import datetime +from croniter import croniter +from frappe.utils.background_jobs import enqueue, get_jobs + +class ScheduledJobType(Document): + def autoname(self): + self.name = '.'.join(self.method.split('.')[-2:]) + + def validate(self): + if self.frequency != 'All': + # force logging for all events other than continuous ones (ALL) + self.create_log = 1 + + def enqueue(self): + # enqueue event if last execution is done + if self.is_event_due(): + if frappe.flags.enqueued_jobs: + frappe.flags.enqueued_jobs.append(self.method) + + if frappe.flags.execute_job: + self.execute() + else: + if not self.is_job_in_queue(): + enqueue('frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job', + queue = self.get_queue_name(), job_type=self.method) + return True + + return False + + def is_event_due(self, current_time = None): + '''Return true if event is due based on time lapsed since last execution''' + # if the next scheduled event is before NOW, then its due! + return self.get_next_execution() <= (current_time or now_datetime()) + + def is_job_in_queue(self): + queued_jobs = get_jobs(site=frappe.local.site, key='job_type')[frappe.local.site] + return self.method in queued_jobs + + def get_next_execution(self): + CRON_MAP = { + "Yearly": "0 0 1 1 *", + "Annual": "0 0 1 1 *", + "Monthly": "0 0 1 * *", + "Monthly Long": "0 0 1 * *", + "Weekly": "0 0 * * 0", + "Weekly Long": "0 0 * * 0", + "Daily": "0 0 * * *", + "Daily Long": "0 0 * * *", + "Hourly": "0 * * * *", + "Hourly Long": "0 * * * *", + "All": "0/" + str((frappe.get_conf().scheduler_interval or 240) // 60) + " * * * *", + } + + if not self.cron_format: + self.cron_format = CRON_MAP[self.frequency] + + return croniter(self.cron_format, + get_datetime(self.last_execution or datetime(2000, 1, 1))).get_next(datetime) + + def execute(self): + self.scheduler_log = None + try: + self.log_status('Start') + frappe.get_attr(self.method)() + frappe.db.commit() + self.log_status('Complete') + except Exception: + frappe.db.rollback() + self.log_status('Failed') + + def log_status(self, status): + # log file + frappe.logger(__name__).info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site)) + self.update_scheduler_log(status) + + def update_scheduler_log(self, status): + if not self.create_log: + return + if not self.scheduler_log: + self.scheduler_log = frappe.get_doc(dict(doctype = 'Scheduled Job Log', scheduled_job_type=self.name)).insert(ignore_permissions=True) + self.scheduler_log.db_set('status', status) + if status == 'Failed': + self.scheduler_log.db_set('details', frappe.get_traceback()) + if status == 'Start': + self.db_set('last_execution', now_datetime(), update_modified=False) + frappe.db.commit() + + def get_queue_name(self): + return 'long' if ('Long' in self.frequency) else 'default' + + def on_trash(self): + frappe.db.sql('delete from `tabScheduled Job Log` where scheduled_job_type=%s', self.name) + +@frappe.whitelist() +def execute_event(doc): + frappe.only_for('System Manager') + doc = json.loads(doc) + frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue() + +def run_scheduled_job(job_type): + '''This is a wrapper function that runs a hooks.scheduler_events method''' + try: + frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute() + except Exception: + print(frappe.get_traceback()) + +def sync_jobs(): + frappe.reload_doc('core', 'doctype', 'scheduled_job_type') + all_events = [] + scheduler_events = frappe.get_hooks("scheduler_events") + insert_events(all_events, scheduler_events) + clear_events(all_events, scheduler_events) + +def insert_events(all_events, scheduler_events): + for event_type in scheduler_events: + events = scheduler_events.get(event_type) + if isinstance(events, dict): + insert_cron_event(events, all_events) + else: + # hourly, daily etc + insert_event_list(events, event_type, all_events) + +def insert_cron_event(events, all_events): + for cron_format in events: + for event in events.get(cron_format): + all_events.append(event) + insert_single_event('Cron', event, cron_format) + +def insert_event_list(events, event_type, all_events): + for event in events: + all_events.append(event) + frequency = event_type.replace('_', ' ').title() + insert_single_event(frequency, event) + +def insert_single_event(frequency, event, cron_format = None): + if not frappe.db.exists('Scheduled Job Type', dict(method=event)): + frappe.get_doc(dict( + doctype = 'Scheduled Job Type', + method = event, + cron_format = cron_format, + frequency = frequency + )).insert() + +def clear_events(all_events, scheduler_events): + for event in frappe.get_all('Scheduled Job Type', ('name', 'method')): + if event.method not in all_events: + frappe.delete_doc('Scheduled Job Type', event.name) diff --git a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py new file mode 100644 index 0000000000..ec1e70ad6a --- /dev/null +++ b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from frappe.utils import get_datetime + +from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs + +class TestScheduledJobType(unittest.TestCase): + def setUp(self): + if not frappe.get_all('Scheduled Job Type', limit=1): + frappe.db.rollback() + frappe.db.sql('truncate `tabScheduled Job Type`') + sync_jobs() + frappe.db.commit() + + def test_sync_jobs(self): + all_job = frappe.get_doc('Scheduled Job Type', + dict(method='frappe.email.queue.flush')) + self.assertEqual(all_job.frequency, 'All') + + daily_job = frappe.get_doc('Scheduled Job Type', + dict(method='frappe.email.queue.clear_outbox')) + self.assertEqual(daily_job.frequency, 'Daily') + + # check if cron jobs are synced + cron_job = frappe.get_doc('Scheduled Job Type', + dict(method='frappe.oauth.delete_oauth2_data')) + self.assertEqual(cron_job.frequency, 'Cron') + self.assertEqual(cron_job.cron_format, '0/15 * * * *') + + def test_daily_job(self): + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.queue.clear_outbox')) + job.db_set('last_execution', '2019-01-01 00:00:00') + self.assertTrue(job.is_event_due(get_datetime('2019-01-02 00:00:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:00:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-01 23:59:59'))) + + def test_weekly_job(self): + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.social.doctype.energy_point_log.energy_point_log.send_weekly_summary')) + job.db_set('last_execution', '2019-01-01 00:00:00') + self.assertTrue(job.is_event_due(get_datetime('2019-01-06 00:00:01'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-02 00:00:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-05 23:59:59'))) + + def test_monthly_job(self): + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.doctype.auto_email_report.auto_email_report.send_monthly')) + job.db_set('last_execution', '2019-01-01 00:00:00') + self.assertTrue(job.is_event_due(get_datetime('2019-02-01 00:00:01'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-15 00:00:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-31 23:59:59'))) + + def test_cron_job(self): + # runs every 15 mins + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.oauth.delete_oauth2_data')) + job.db_set('last_execution', '2019-01-01 00:00:00') + self.assertTrue(job.is_event_due(get_datetime('2019-01-01 00:15:01'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:05:06'))) + self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:14:59'))) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 55bc5d49db..9e8bc8c3fd 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2014-04-17 16:53:52.640856", "doctype": "DocType", "document_type": "System", @@ -21,7 +22,7 @@ "backup_limit", "background_workers", "enable_scheduler", - "scheduler_last_event", + "dormant_days", "permissions", "apply_strict_user_permissions", "column_break_21", @@ -168,13 +169,6 @@ "hidden": 1, "label": "Enable Scheduled Jobs" }, - { - "fieldname": "scheduler_last_event", - "fieldtype": "Data", - "hidden": 1, - "label": "Scheduler Last Event", - "report_hide": 1 - }, { "collapsible": 1, "fieldname": "permissions", @@ -397,11 +391,18 @@ "fieldname": "allow_guests_to_upload_files", "fieldtype": "Check", "label": "Allow Guests to Upload Files" + }, + { + "default": "4", + "description": "Will run scheduled jobs only once a day for inactive sites. Default 4 days if set to 0.", + "fieldname": "dormant_days", + "fieldtype": "Int", + "label": "Run Jobs only Daily if Inactive For (Days)" } ], "icon": "fa fa-cog", "issingle": 1, - "modified": "2019-08-16 08:26:45.936626", + "modified": "2019-09-24 10:04:28.807388", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/test_runner/_test_test_runner.js b/frappe/core/doctype/test_runner/_test_test_runner.js deleted file mode 100644 index 0b0bd9a98b..0000000000 --- a/frappe/core/doctype/test_runner/_test_test_runner.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Test Runner", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('Test Runner', [ - // insert a new Test Runner - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/frappe/core/doctype/test_runner/test_runner.js b/frappe/core/doctype/test_runner/test_runner.js deleted file mode 100644 index d08a3626a3..0000000000 --- a/frappe/core/doctype/test_runner/test_runner.js +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Test Runner', { - refresh: (frm) => { - frm.disable_save(); - frm.page.set_primary_action(__("Run Tests"), () => { - return new Promise(resolve => { - let wrapper = $(frm.fields_dict.output.wrapper).empty(); - $("

Loading...

").appendTo(wrapper); - - // all tests - frappe.call({ - method: 'frappe.core.doctype.test_runner.test_runner.get_test_js', - args: { test_path: frm.doc.module_path } - }).always((data) => { - $("
").appendTo(wrapper.empty()); - frm.events.run_tests(frm, data.message); - resolve(); - }); - }); - }); - - }, - run_tests: function(frm, files) { - frappe.flags.in_test = true; - let require_list = [ - "assets/frappe/js/lib/jquery/qunit.js", - "assets/frappe/js/lib/jquery/qunit.css" - ].concat(); - - frappe.require(require_list, () => { - files.forEach((f) => { - frappe.dom.eval(f.script); - }); - - QUnit.config.notrycatch = true; - - window.onerror = function(msg, url, lineNo, columnNo, error) { - console.log(error.stack); // eslint-disable-line - $('
').appendTo($('body')); - }; - - QUnit.testDone(function(details) { - // var result = { - // "Module name": details.module, - // "Test name": details.name, - // "Assertions": { - // "Total": details.total, - // "Passed": details.passed, - // "Failed": details.failed - // }, - // "Skipped": details.skipped, - // "Todo": details.todo, - // "Runtime": details.runtime - // }; - - // eslint-disable-next-line - // console.log(JSON.stringify(result, null, 2)); - - details.assertions.map(a => { - // eslint-disable-next-line - console.log(`${a.result ? '✔' : '✗'} ${a.message}`); - }); - - }); - QUnit.load(); - - QUnit.done(({ total, failed, passed, runtime }) => { - // flag for selenium that test is done - - console.log( `Total: ${total}, Failed: ${failed}, Passed: ${passed}, Runtime: ${runtime}` ); // eslint-disable-line - - if(failed) { - console.log('Tests Failed'); // eslint-disable-line - } else { - console.log('Tests Passed'); // eslint-disable-line - } - frappe.set_route('Form', 'Test Runner', 'Test Runner'); - - $('
').appendTo($('body')); - - }); - }); - - } -}); diff --git a/frappe/core/doctype/test_runner/test_runner.json b/frappe/core/doctype/test_runner/test_runner.json deleted file mode 100644 index ccc1361dc9..0000000000 --- a/frappe/core/doctype/test_runner/test_runner.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-06-26 10:57:19.976624", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "module_path", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Module Path", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "app", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "App", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "output", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Output", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2017-07-19 03:22:33.221169", - "modified_by": "Administrator", - "module": "Core", - "name": "Test Runner", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "Administrator", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/frappe/core/doctype/test_runner/test_runner.py b/frappe/core/doctype/test_runner/test_runner.py deleted file mode 100644 index 2961e9f38b..0000000000 --- a/frappe/core/doctype/test_runner/test_runner.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe, os -from frappe.model.document import Document - -class TestRunner(Document): - pass - -@frappe.whitelist() -def get_test_js(test_path=None): - '''Get test + data for app, example: app/tests/ui/test_name.js''' - if not test_path: - test_path = frappe.db.get_single_value('Test Runner', 'module_path') - test_js = [] - - # split - app, test_path = test_path.split(os.path.sep, 1) - - # now full path - test_path = frappe.get_app_path(app, test_path) - - def add_file(path): - with open(path, 'r') as fileobj: - test_js.append(dict( - script = fileobj.read() - )) - - # add test_lib.js - add_file(frappe.get_app_path('frappe', 'tests', 'ui', 'data', 'test_lib.js')) - add_file(test_path) - - return test_js - diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 7de2bb20e5..35495954b4 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -97,7 +97,9 @@ class User(Document): self.share_with_self() clear_notifications(user=self.name) frappe.clear_cache(user=self.name) - self.send_password_notification(self.__new_password) + if self.__new_password: + self.send_password_notification(self.__new_password) + self.reset_password_key = '' create_contact(self, ignore_mandatory=True) if self.name not in ('Administrator', 'Guest') and not self.user_image: frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name) @@ -1071,4 +1073,4 @@ def generate_keys(user): user_details.save() return {"api_secret": api_secret} - frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) \ No newline at end of file + frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) diff --git a/frappe/core/page/background_jobs/background_jobs.html b/frappe/core/page/background_jobs/background_jobs.html index c5d598ccd3..08177ecf8a 100644 --- a/frappe/core/page/background_jobs/background_jobs.html +++ b/frappe/core/page/background_jobs/background_jobs.html @@ -11,7 +11,7 @@ {% for j in jobs %} - {{ j.queue.split(".").slice(-1)[0] }} + {{ j.queue.split(".").slice(-1)[0] }}
{{ frappe.utils.encode_tags(j.job_name) }} diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py index 10cb7b97ac..c8a2352968 100644 --- a/frappe/core/page/background_jobs/background_jobs.py +++ b/frappe/core/page/background_jobs/background_jobs.py @@ -29,9 +29,9 @@ def get_info(show_failed=False): jobs.append({ 'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \ or str(j.kwargs.get('job_name')), - 'status': j.status, 'queue': name, + 'status': j.get_status(), 'queue': name, 'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)), - 'color': colors[j.status] + 'color': colors[j.get_status()] }) if j.exc_info: jobs[-1]['exc_info'] = j.exc_info diff --git a/frappe/core/page/dashboard/dashboard.py b/frappe/core/page/dashboard/dashboard.py index ebe8ce2c16..1f4eaee783 100644 --- a/frappe/core/page/dashboard/dashboard.py +++ b/frappe/core/page/dashboard/dashboard.py @@ -36,13 +36,15 @@ def generate_and_cache_results(chart, chart_name, function, cache_key): def get_from_date_from_timespan(to_date, timespan): days = months = years = 0 - if "Last Week" == timespan: + if timespan == "Last Week": days = -7 - if "Last Month" == timespan: + if timespan == "Last Month": months = -1 - elif "Last Quarter" == timespan: + elif timespan == "Last Quarter": months = -3 - elif "Last Year" == timespan: + elif timespan == "Last Year": years = -1 + elif timespan == "All Time": + years = -50 return add_to_date(to_date, years=years, months=months, days=days, as_datetime=True) diff --git a/frappe/database/database.py b/frappe/database/database.py index f5055571ff..1c08dd714e 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -15,7 +15,7 @@ import frappe.model.meta from frappe import _ from time import time -from frappe.utils import now, getdate, cast_fieldtype +from frappe.utils import now, getdate, cast_fieldtype, get_datetime from frappe.utils.background_jobs import execute_job, get_queue from frappe.model.utils.link_count import flush_local_link_count from frappe.utils import cint @@ -941,6 +941,16 @@ class Database(object): else: frappe.throw(_('No conditions provided')) + def get_last_created(self, doctype): + last_record = self.get_all(doctype, ('creation'), limit=1, order_by='creation desc') + if last_record: + return get_datetime(last_record[0].creation) + else: + return None + + def clear_table(self, doctype): + self.sql('truncate `tab{}`'.format(doctype)) + def log_touched_tables(self, query, values=None): if values: query = frappe.safe_decode(self._cursor.mogrify(query, values)) 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/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index 7058ed0325..b1a769b189 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -105,6 +105,53 @@ CREATE TABLE `tabDocPerm` ( KEY `parent` (`parent`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- +-- Table structure for table `tabDocType Action` +-- + +CREATE TABLE `tabDocType Action` ( + `name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `creation` datetime(6) DEFAULT NULL, + `modified` datetime(6) DEFAULT NULL, + `modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `docstatus` int(1) NOT NULL DEFAULT 0, + `parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `idx` int(8) NOT NULL DEFAULT 0, + `label` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `action_type` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `action` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`name`), + KEY `parent` (`parent`), + KEY `modified` (`modified`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED; + +-- +-- Table structure for table `tabDocType Action` +-- + +CREATE TABLE `tabDocType Link` ( + `name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `creation` datetime(6) DEFAULT NULL, + `modified` datetime(6) DEFAULT NULL, + `modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `docstatus` int(1) NOT NULL DEFAULT 0, + `parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `idx` int(8) NOT NULL DEFAULT 0, + `group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `link_doctype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `link_fieldname` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`name`), + KEY `parent` (`parent`), + KEY `modified` (`modified`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED; + -- -- Table structure for table `tabDocType` -- diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index df59de92df..cd2f02d8e4 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -106,6 +106,57 @@ CREATE TABLE "tabDocPerm" ( create index on "tabDocPerm" ("parent"); +-- +-- Table structure for table "tabDocType Action" +-- + +DROP TABLE IF EXISTS "tabDocType Action"; +CREATE TABLE "tabDocType Action" ( + "name" varchar(255) NOT NULL, + "creation" timestamp(6) DEFAULT NULL, + "modified" timestamp(6) DEFAULT NULL, + "modified_by" varchar(255) DEFAULT NULL, + "owner" varchar(255) DEFAULT NULL, + "docstatus" smallint NOT NULL DEFAULT 0, + "parent" varchar(255) DEFAULT NULL, + "parentfield" varchar(255) DEFAULT NULL, + "parenttype" varchar(255) DEFAULT NULL, + "idx" bigint NOT NULL DEFAULT 0, + "label" varchar(140) NOT NULL, + "group" varchar(140) DEFAULT NULL, + "action_type" varchar(140) NOT NULL, + "action" varchar(140) NOT NULL, + PRIMARY KEY ("name") +) ; + +create index on "tabDocType Action" ("parent"); + +-- +-- Table structure for table "tabDocType Link" +-- + +DROP TABLE IF EXISTS "tabDocType Link"; +CREATE TABLE "tabDocType Link" ( + "name" varchar(255) NOT NULL, + "creation" timestamp(6) DEFAULT NULL, + "modified" timestamp(6) DEFAULT NULL, + "modified_by" varchar(255) DEFAULT NULL, + "owner" varchar(255) DEFAULT NULL, + "docstatus" smallint NOT NULL DEFAULT 0, + "parent" varchar(255) DEFAULT NULL, + "parentfield" varchar(255) DEFAULT NULL, + "parenttype" varchar(255) DEFAULT NULL, + "idx" bigint NOT NULL DEFAULT 0, + "label" varchar(140) DEFAULT NULL, + "group" varchar(140) DEFAULT NULL, + "link_doctype" varchar(140) NOT NULL, + "link_fieldname" varchar(140) NOT NULL, + PRIMARY KEY ("name") +) ; + +create index on "tabDocType Link" ("parent"); + + -- -- Table structure for table "tabDocType" -- diff --git a/frappe/desk/doctype/bulk_update/bulk_update.js b/frappe/desk/doctype/bulk_update/bulk_update.js index 6f2a9c3dde..bb9cf2af51 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.js +++ b/frappe/desk/doctype/bulk_update/bulk_update.js @@ -3,6 +3,15 @@ frappe.ui.form.on('Bulk Update', { refresh: function(frm) { + frm.set_query("document_type", function() { + return { + filters: [ + ['DocType', 'issingle', '=', 0], + ['DocType', 'name', 'not in', frappe.model.core_doctypes_list] + ] + }; + }); + frm.page.set_primary_action(__('Update'), function() { if (!frm.doc.update_value) { frappe.throw(__('Field "value" is mandatory. Please specify value to be updated')); diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index 99ba49bc4f..a330f7e97e 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -41,6 +41,7 @@ frappe.ui.form.on('Dashboard Chart', { timespan: function(frm) { const time_interval_options = { "Select Date Range": ["Quarterly", "Monthly", "Weekly", "Daily"], + "All Time": ["Yearly", "Monthly"], "Last Year": ["Quarterly", "Monthly", "Weekly", "Daily"], "Last Quarter": ["Monthly", "Weekly", "Daily"], "Last Month": ["Weekly", "Daily"], diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index 7e99b86eae..a5f9cead00 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -82,14 +82,14 @@ "fieldname": "timespan", "fieldtype": "Select", "label": "Timespan", - "options": "Last Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range" + "options": "All Time\nLast Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range" }, { "depends_on": "timeseries", "fieldname": "time_interval", "fieldtype": "Select", "label": "Time Interval", - "options": "Quarterly\nMonthly\nWeekly\nDaily" + "options": "Yearly\nQuarterly\nMonthly\nWeekly\nDaily" }, { "default": "0", @@ -187,7 +187,7 @@ "label": "To Date" } ], - "modified": "2019-11-04 12:32:14.525409", + "modified": "2019-11-18 16:20:11.529496", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart", diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index e7ec3f8bab..6dec12efc5 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -74,8 +74,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): result = convert_to_dates(data, timegrain) # add missing data points for periods where there was no result - result = add_missing_values(result, timegrain, from_date, to_date) - + result = add_missing_values(result, timegrain, timespan, from_date, to_date) chart_config = { "labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result], "datasets": [{ @@ -133,7 +132,9 @@ def get_aggregate_function(chart_type): "Average": "AVG", }[chart_type] + def convert_to_dates(data, timegrain): + """ Converts individual dates within data to the end of period """ result = [] for d in data: if timegrain == 'Daily': @@ -141,10 +142,11 @@ def convert_to_dates(data, timegrain): elif timegrain == 'Weekly': result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), weeks = d[1] + 1), days = -1), d[2]]) elif timegrain == 'Monthly': - result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months = d[1]), days = -1), d[2]]) + result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1]), days = -1), d[2]]) elif timegrain == 'Quarterly': - result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months = d[1] * 3), days = -1), d[2]]) - + result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1] * 3), days = -1), d[2]]) + elif timegrain == 'Yearly': + result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=12), days = -1), d[2]]) result[-1][0] = getdate(result[-1][0]) return result @@ -164,17 +166,17 @@ def get_unit_function(datefield, timegrain): return unit_function -def add_missing_values(data, timegrain, from_date, to_date): +def add_missing_values(data, timegrain, timespan, from_date, to_date): # add missing intervals result = [] - first_expected_date = get_period_ending(from_date, timegrain) - - # fill out data before the first data point - first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1)) - while first_data_point_date > first_expected_date: - result.append([first_expected_date, 0.0]) - first_expected_date = get_next_expected_date(first_expected_date, timegrain) + if timespan != 'All Time': + first_expected_date = get_period_ending(from_date, timegrain) + # fill out data before the first data point + first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1)) + while first_data_point_date > first_expected_date: + result.append([first_expected_date, 0.0]) + first_expected_date = get_next_expected_date(first_expected_date, timegrain) # fill data points and missing points for i, d in enumerate(data): @@ -212,14 +214,16 @@ def get_next_expected_date(date, timegrain): def get_period_ending(date, timegrain): date = getdate(date) - if timegrain=='Daily': + if timegrain == 'Daily': pass - elif timegrain=='Weekly': + elif timegrain == 'Weekly': date = get_week_ending(date) - elif timegrain=='Monthly': + elif timegrain == 'Monthly': date = get_month_ending(date) - elif timegrain=='Quarterly': + elif timegrain == 'Quarterly': date = get_quarter_ending(date) + elif timegrain == 'Yearly': + date = get_year_ending(date) return getdate(date) @@ -231,7 +235,7 @@ def get_week_ending(date): # first day of next week date = add_to_date('{}-01-01'.format(date.year), weeks = week_of_the_year + 1) # last day of this week - return add_to_date(date, days = -1) + return add_to_date(date, days=-1) def get_month_ending(date): month_of_the_year = int(date.strftime('%m')) @@ -239,7 +243,7 @@ def get_month_ending(date): date = add_to_date('{}-01-01'.format(date.year), months = month_of_the_year) # last day of this month - return add_to_date(date, days = -1) + return add_to_date(date, days=-1) def get_quarter_ending(date): date = getdate(date) @@ -255,8 +259,17 @@ def get_quarter_ending(date): return date +def get_year_ending(date): + ''' returns year ending of the given date ''' + + # first day of next year (note year starts from 1) + date = add_to_date('{}-01-01'.format(date.year), months = 12) + # last day of this month + return add_to_date(date, days=-1) + class DashboardChart(Document): + def on_update(self): frappe.cache().delete_key('chart-data:{}'.format(self.name)) diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index fc74448d10..238d4f28e5 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -36,6 +36,9 @@ class TestDashboardChart(unittest.TestCase): self.assertEqual(get_period_ending('2019-10-01', 'Quarterly'), getdate('2019-12-31')) + self.assertEqual(get_period_ending('2019-10-01', 'Yearly'), + getdate('2019-12-31')) + def test_dashboard_chart(self): if frappe.db.exists('Dashboard Chart', 'Test Dashboard Chart'): frappe.delete_doc('Dashboard Chart', 'Test Dashboard Chart') 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 0729fca5cb..85c9687ab3 100644 --- a/frappe/desk/doctype/global_search_settings/global_search_settings.py +++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py @@ -29,12 +29,16 @@ class GlobalSearchSettings(Document): repeated_dts = (", ".join([frappe.bold(dt) for dt in repeated_dts])) frappe.throw(_("Document Type {0} has been repeated.").format(repeated_dts)) -def get_doctypes_for_global_search(): - doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC") - if not doctypes: - return [] + # reset cache + frappe.cache().hdel('global_search', 'search_priorities') + +def get_doctypes_for_global_search(): + def get_from_db(): + doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC") + return [d.document_type for d in doctypes] or [] + + return frappe.cache().hget("global_search", "search_priorities", get_from_db) - return [d.document_type for d in doctypes] @frappe.whitelist() def reset_global_search_settings_doctypes(): @@ -57,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/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index 87bfc1ca17..398a3de351 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -42,7 +42,6 @@ def enqueue_create_notification(users, doc): This breaks new site creation if Redis server is not running. We do not need any notifications in fresh installation ''' - if frappe.flags.in_install: return diff --git a/frappe/desk/doctype/notification_settings/notification_settings.js b/frappe/desk/doctype/notification_settings/notification_settings.js new file mode 100644 index 0000000000..d4e3b08def --- /dev/null +++ b/frappe/desk/doctype/notification_settings/notification_settings.js @@ -0,0 +1,12 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Notification Settings', { + onload: () => { + frappe.breadcrumbs.add({ + label: __('Settings'), + route: '#modules/Settings', + type: 'Custom' + }); + } +}); diff --git a/frappe/desk/doctype/onboarding_slide/__init__.py b/frappe/desk/doctype/onboarding_slide/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/onboarding_slide/onboarding_slide.js b/frappe/desk/doctype/onboarding_slide/onboarding_slide.js new file mode 100644 index 0000000000..dc91f42913 --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide/onboarding_slide.js @@ -0,0 +1,45 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Onboarding Slide', { + refresh: function(frm) { + frm.toggle_reqd('ref_doctype', (frm.doc.slide_type=='Create' || frm.doc.slide_type=='Settings')); + frm.toggle_reqd('slide_module', (frm.doc.slide_type=='Information' || frm.doc.slide_type=='Continue')); + }, + + ref_doctype: function(frm) { + frm.set_query('ref_doctype', function() { + if (frm.doc.slide_type === 'Create') { + return { + filters: { + 'issingle': 0, + 'istable': 0 + } + }; + } else if (frm.doc.slide_type === 'Settings') { + return { + filters: { + 'issingle': 1, + 'istable': 0 + } + }; + } + }); + + //fetch mandatory fields automatically + if (frm.doc.ref_doctype) { + frappe.model.clear_table(frm.doc, 'slide_fields'); + let fields = frappe.meta.get_docfields(frm.doc.ref_doctype, null, { + reqd: 1 + }); + $.each(fields, function(_i, data) { + let row = frappe.model.add_child(frm.doc, 'Onboarding Slide', 'slide_fields'); + row.label = data.label; + row.fieldtype = data.fieldtype; + row.fieldname = data.fieldname; + row.options = data.options; + }); + refresh_field('slide_fields'); + } + } +}); diff --git a/frappe/desk/doctype/onboarding_slide/onboarding_slide.json b/frappe/desk/doctype/onboarding_slide/onboarding_slide.json new file mode 100644 index 0000000000..3f6f0d719f --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide/onboarding_slide.json @@ -0,0 +1,184 @@ +{ + "autoname": "field:slide_title", + "creation": "2019-11-13 14:39:56.834658", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "slide_title", + "app", + "slide_order", + "column_break_4", + "image_src", + "slide_module", + "description_section_break", + "slide_desc", + "action_section_break", + "slide_type", + "column_break_6", + "max_count", + "add_more_button", + "section_break_18", + "ref_doctype", + "slide_fields", + "section_break_10", + "domains", + "column_break_12", + "help_links", + "is_completed" + ], + "fields": [ + { + "fieldname": "slide_title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Slide Title", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "slide_desc", + "fieldtype": "HTML Editor", + "label": "Slide Description" + }, + { + "default": "3", + "depends_on": "add_more_button", + "description": "The amount of times you want to repeat the set of fields (eg: if you want 3 customers in the slide, set this field to 3. Only the first set of fields is shown as mandatory in the slide)", + "fieldname": "max_count", + "fieldtype": "Int", + "label": "Max Count" + }, + { + "default": "0", + "depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'", + "fieldname": "add_more_button", + "fieldtype": "Check", + "label": "Add More Button" + }, + { + "depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'", + "fieldname": "slide_fields", + "fieldtype": "Table", + "label": "Slide Fields", + "options": "Onboarding Slide Field" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "description": "Specify in what all domains should the slides show up. If nothing is specified the slide is shown in all domains by default.", + "fieldname": "domains", + "fieldtype": "Table", + "label": "Domains", + "options": "Has Domain" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "description": "Add a help video link just in case user has no idea about what to fill in the slide.", + "fieldname": "help_links", + "fieldtype": "Table", + "label": "Help Links", + "options": "Onboarding Slide Help Link" + }, + { + "fieldname": "action_section_break", + "fieldtype": "Section Break", + "label": "Action Settings" + }, + { + "description": "If Slide Type is Create or Settings there should be a 'create_onboarding_docs' method in the {ref_doctype}.py file bound to be executed after the slide is completed.", + "fieldname": "slide_type", + "fieldtype": "Select", + "label": "Slide Type", + "options": "Information\nCreate\nSettings\nContinue", + "reqd": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "app", + "fieldtype": "Select", + "label": "App", + "options": "Frappe\nERPNext", + "reqd": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "image_src", + "fieldtype": "Data", + "label": "Slide Image Source" + }, + { + "fieldname": "description_section_break", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'", + "fieldname": "ref_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType" + }, + { + "default": "0", + "description": "Determines the order of the slide in the wizard. If the slide is not to be displayed, priority should be set to 0.", + "fieldname": "slide_order", + "fieldtype": "Int", + "label": "Slide Order" + }, + { + "depends_on": "eval:doc.slide_type=='Information' || doc.slide_type=='Continue'", + "fieldname": "slide_module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" + }, + { + "collapsible_depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'", + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "label": "Fields" + }, + { + "default": "0", + "fieldname": "is_completed", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Completed", + "print_hide": 1 + } + ], + "modified": "2019-12-04 10:50:43.528901", + "modified_by": "Administrator", + "module": "Desk", + "name": "Onboarding Slide", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/onboarding_slide/onboarding_slide.py b/frappe/desk/doctype/onboarding_slide/onboarding_slide.py new file mode 100644 index 0000000000..8c75d10b9a --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide/onboarding_slide.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from frappe import _ +from frappe.model.document import Document +from frappe.modules.export_file import export_to_files + +class OnboardingSlide(Document): + def validate(self): + if self.slide_type == 'Continue' and frappe.db.exists('Onboarding Slide', {'slide_type': 'Continue', 'name': ('!=', self.name)}): + frappe.throw(_('An Onboarding Slide of Slide Type Continue already exists.')) + + if self.slide_order: + same_order_slide = frappe.db.exists('Onboarding Slide', {'slide_order': self.slide_order, 'name': ('!=', self.name)}) + if same_order_slide: + frappe.throw(_('An Onboarding Slide {0} with the same slide order already exists').format(same_order_slide)) + + def on_update(self): + if self.ref_doctype: + module = frappe.db.get_value('DocType', self.ref_doctype, 'module') + else: + module = self.slide_module + export_to_files(record_list=[['Onboarding Slide', self.name]], record_module=module) + +def get_onboarding_slides_as_list(): + slides = [] + slide_docs = frappe.db.get_all('Onboarding Slide', + filters={'is_completed': 0}, + or_filters={'slide_order': ('!=', 0), 'slide_type': 'Continue'}, + order_by='slide_order') + + # to check if continue slide is required + first_slide = get_first_slide() + + for entry in slide_docs: + # using get_doc because child table fields are not fetched in get_all + slide_doc = frappe.get_doc('Onboarding Slide', entry.name) + if frappe.scrub(slide_doc.app) in frappe.get_installed_apps(): + slide = frappe._dict( + slide_type=slide_doc.slide_type, + title=slide_doc.slide_title, + help=slide_doc.slide_desc, + fields=slide_doc.slide_fields, + help_links=get_help_links(slide_doc), + add_more=slide_doc.add_more_button, + max_count=slide_doc.max_count, + image_src=get_slide_image(slide_doc), + ref_doctype=slide_doc.ref_doctype, + app=slide_doc.app + ) + if slide.slide_type == 'Continue': + if is_continue_slide_required(first_slide): + slides.insert(0, slide) + else: + slides.append(slide) + + return slides + +@frappe.whitelist() +def get_onboarding_slides(): + slides = [] + slide_list = get_onboarding_slides_as_list() + + active_domains = frappe.get_active_domains() + for slide in slide_list: + if not slide.domains or any(domain in active_domains for domain in slide.domains): + slides.append(slide) + return slides + +def get_help_links(slide_doc): + links=[] + for link in slide_doc.help_links: + links.append({ + 'label': link.label, + 'video_id': link.video_id + }) + return links + +def get_slide_image(slide_doc): + if slide_doc.image_src: + return slide_doc.image_src + return None + +def is_continue_slide_required(first_slide): + # check if first slide itself is not completed + if not first_slide.is_completed: + return False + + # check if there is any active slide which is not completed + return frappe.db.exists('Onboarding Slide', { + 'is_completed': 0, + 'slide_order': ('!=', 0), + 'slide_type': ('!=', 'Continue') + }) + +@frappe.whitelist() +def create_onboarding_docs(values, doctype=None, app=None, slide_type=None): + data = json.loads(values) + doc = frappe.new_doc(doctype) + if hasattr(doc, 'create_onboarding_docs'): + doc.create_onboarding_docs(data) + else: + create_generic_onboarding_doc(data, doctype, slide_type) + +def create_generic_onboarding_doc(data, doctype, slide_type): + if slide_type == 'Settings': + doc = frappe.get_single(doctype) + for entry in data: + doc.set(entry, data.get(entry)) + doc.save() + + elif slide_type == 'Create': + doc = frappe.new_doc(doctype) + for entry in data: + doc.set(entry, data.get(entry)) + doc.flags.ignore_mandatory = True + doc.flags.ignore_links = True + doc.insert() + +@frappe.whitelist() +def mark_slide_as_completed(slide_title): + frappe.db.set_value('Onboarding Slide', slide_title, 'is_completed', 1) + +def get_first_slide(): + slides = frappe.db.get_all('Onboarding Slide', + filters={'slide_order': ('!=', 0), 'slide_type': ('!=', 'Continue')}, + order_by='slide_order', + fields=['name', 'is_completed'] + ) + return slides[0] \ No newline at end of file diff --git a/frappe/desk/doctype/onboarding_slide/test_onboarding_slide.py b/frappe/desk/doctype/onboarding_slide/test_onboarding_slide.py new file mode 100644 index 0000000000..d78b9b6158 --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide/test_onboarding_slide.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestOnboardingSlide(unittest.TestCase): + pass diff --git a/frappe/desk/doctype/onboarding_slide_field/__init__.py b/frappe/desk/doctype/onboarding_slide_field/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.json b/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.json new file mode 100644 index 0000000000..0992aed092 --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.json @@ -0,0 +1,74 @@ +{ + "creation": "2019-11-13 13:35:08.617909", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "fieldtype", + "fieldname", + "align", + "placeholder", + "reqd", + "column_break_4", + "options" + ], + "fields": [ + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label" + }, + { + "fieldname": "fieldtype", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Fieldtype", + "options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nFloat\nHTML\nInt\nRating\nSelect\nLink\nSmall Text\nText\nText Editor\nSection Break\nColumn Break" + }, + { + "fieldname": "fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Fieldname" + }, + { + "fieldname": "align", + "fieldtype": "Select", + "label": "Align", + "options": "\ncenter\nleft\nright" + }, + { + "fieldname": "placeholder", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Placeholder" + }, + { + "default": "0", + "fieldname": "reqd", + "fieldtype": "Check", + "label": "Mandatory" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "options", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Options" + } + ], + "istable": 1, + "modified": "2019-12-02 16:43:51.930018", + "modified_by": "Administrator", + "module": "Desk", + "name": "Onboarding Slide Field", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.py b/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.py new file mode 100644 index 0000000000..74b6782ff8 --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class OnboardingSlideField(Document): + pass diff --git a/frappe/desk/doctype/onboarding_slide_help_link/__init__.py b/frappe/desk/doctype/onboarding_slide_help_link/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.json b/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.json new file mode 100644 index 0000000000..a09ba50553 --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.json @@ -0,0 +1,35 @@ +{ + "creation": "2019-11-19 12:22:42.805741", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "video_id" + ], + "fields": [ + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label" + }, + { + "fieldname": "video_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Video" + } + ], + "istable": 1, + "modified": "2019-11-19 13:39:57.716248", + "modified_by": "Administrator", + "module": "Desk", + "name": "Onboarding Slide Help Link", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.py b/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.py new file mode 100644 index 0000000000..6ee5e4f7d7 --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class OnboardingSlideHelpLink(Document): + pass 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/listview.py b/frappe/desk/listview.py index fd0833eb51..e7f56d313e 100644 --- a/frappe/desk/listview.py +++ b/frappe/desk/listview.py @@ -48,7 +48,7 @@ def get_group_by_count(doctype, current_filters, field): else: return frappe.db.get_list(doctype, filters=current_filters, - group_by=field, + group_by='`tab{0}`.{1}'.format(doctype, field), fields=['count(*) as count', '`{}` as name'.format(field)], order_by='count desc', limit=50, diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py index c5e5ea7c2b..0bad171b04 100644 --- a/frappe/desk/moduleview.py +++ b/frappe/desk/moduleview.py @@ -234,8 +234,11 @@ def get_config(app, module): for item in section["items"]: if item["type"]=="report" and item["name"] in disabled_reports: continue + # some module links might not have name + if not item.get("name"): + item["name"] = item.get("label") if not item.get("label"): - item["label"] = _(item["name"]) + item["label"] = _(item.get("name")) items.append(item) section['items'] = items @@ -297,7 +300,7 @@ def get_onboard_items(app, module): @frappe.whitelist() def get_links_for_module(app, module): - return [l.get('label') for l in get_links(app, module)] + return [{'value': l.get('name'), 'label': l.get('label')} for l in get_links(app, module)] def get_links(app, module): try: @@ -330,13 +333,13 @@ def get_desktop_settings(): def apply_user_saved_links(module): module = frappe._dict(module) all_links = get_links(module.app, module.module_name) - module_links_by_label = {} + module_links_by_name = {} for link in all_links: - module_links_by_label[link['label']] = link + module_links_by_name[link['name']] = link if module.module_name in user_saved_links_by_module: user_links = frappe.parse_json(user_saved_links_by_module[module.module_name]) - module.links = [module_links_by_label[l] for l in user_links if l in module_links_by_label] + module.links = [module_links_by_name[l] for l in user_links if l in module_links_by_name] return module diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 84d515050c..b142047059 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -11,15 +11,19 @@ import json @frappe.whitelist() @frappe.read_only() def get_notifications(): + out = { + "open_count_doctype": {}, + "targets": {}, + } if (frappe.flags.in_install or not frappe.db.get_single_value('System Settings', 'setup_complete')): - return { - "open_count_doctype": {}, - "targets": {}, - } + return out config = get_notification_config() + if not config: + return out + groups = list(config.get("for_doctype")) + list(config.get("for_module")) cache = frappe.cache() @@ -31,10 +35,10 @@ def get_notifications(): if count is not None: notification_count[name] = count - return { - "open_count_doctype": get_notifications_for_doctypes(config, notification_count), - "targets": get_notifications_for_targets(config, notification_percent), - } + out['open_count_doctype'] = get_notifications_for_doctypes(config, notification_count) + out['targets'] = get_notifications_for_targets(config, notification_percent) + + return out def get_notifications_for_doctypes(config, notification_count): """Notifications for DocTypes""" @@ -118,6 +122,10 @@ def clear_notifications(user=None): return cache = frappe.cache() config = get_notification_config() + + if not config: + return + for_doctype = list(config.get('for_doctype')) if config.get('for_doctype') else [] for_module = list(config.get('for_module')) if config.get('for_module') else [] groups = for_doctype + for_module @@ -139,6 +147,8 @@ def delete_notification_count_for(doctype): def clear_doctype_notifications(doc, method=None, *args, **kwargs): config = get_notification_config() + if not config: + return if isinstance(doc, string_types): doctype = doc # assuming doctype name was passed directly else: 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..e93b71291f 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,10 @@ 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", + }).insert(ignore_permissions=True, ignore_if_duplicate=True) \ No newline at end of file diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index d5b43807a8..5db6ae18bf 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -9,8 +9,10 @@ from six.moves import range import frappe.permissions from frappe.model.db_query import DatabaseQuery from frappe import _ -from six import text_type, string_types, StringIO +from six import string_types, StringIO from frappe.core.doctype.access_log.access_log import make_access_log +from frappe.utils import cstr + @frappe.whitelist() @frappe.read_only() @@ -170,11 +172,11 @@ def export_query(): writer = csv.writer(f) for r in data: # encode only unicode type strings and not int, floats etc. - writer.writerow([handle_html(frappe.as_unicode(v)).encode('utf-8') \ + writer.writerow([handle_html(frappe.as_unicode(v)) \ if isinstance(v, string_types) else v for v in r]) f.seek(0) - frappe.response['result'] = text_type(f.read(), 'utf-8') + frappe.response['result'] = cstr(f.read()) frappe.response['type'] = 'csv' frappe.response['doctype'] = title @@ -261,17 +263,24 @@ def delete_bulk(doctype, items): @frappe.whitelist() @frappe.read_only() def get_sidebar_stats(stats, doctype, filters=[]): + _user_tags = [] + data = frappe._dict(frappe.local.form_dict) + filters = json.loads(data["filters"]) - if not frappe.cache().hget("tags_count", doctype): - tags = [tag.name for tag in frappe.get_list("Tag")] - _user_tags = [] - for tag in tags: - count = frappe.db.count("Tag Link", filters={"document_type": doctype, "tag": tag}) - if count > 0: - _user_tags.append([tag, count]) - frappe.cache().hset("tags_count", doctype, _user_tags) + 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) - return {"stats": {"_user_tags": frappe.cache().hget("tags_count", doctype)}} + for tag in list(frappe.cache().hget("Tags", doctype)): + tag_filters = [] + tag_filters.extend(filters) + tag_filters.extend([['Tag Link', '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(*)")]) + + return {"stats": {"_user_tags": _user_tags}} @frappe.whitelist() @frappe.read_only() 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/desk/user_progress.py b/frappe/desk/user_progress.py deleted file mode 100644 index f62bb2a29d..0000000000 --- a/frappe/desk/user_progress.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals - -import frappe -from frappe.utils import cint - -@frappe.whitelist() -def get_user_progress_slides(): - ''' - Return user progress slides for the desktop (called via `get_user_progress_slides` hook) - ''' - slides = [] - if cint(frappe.db.get_single_value('System Settings', 'setup_complete')): - for fn in frappe.get_hooks('get_user_progress_slides'): - slides += frappe.get_attr(fn)() - - return slides - -@frappe.whitelist() -def update_and_get_user_progress(): - ''' - Return setup progress action states (called via `update_and_get_user_progress` hook) - ''' - states = {} - for fn in frappe.get_hooks('update_and_get_user_progress'): - states.update(frappe.get_attr(fn)()) - - return states diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index f10f08664c..c05a0f3fe4 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -21,7 +21,6 @@ from frappe.desk.form import assign_to from frappe.utils.user import get_system_managers from frappe.utils.background_jobs import enqueue, get_jobs from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts -from frappe.utils.scheduler import log from frappe.utils.html_utils import clean_email_html from frappe.email.utils import get_port @@ -284,7 +283,7 @@ class EmailAccount(Document): except Exception: frappe.db.rollback() - log('email_account.receive') + frappe.log_error('email_account.receive') if self.use_imap: self.handle_bad_emails(email_server, uid, msg, frappe.get_traceback()) exceptions.append(frappe.get_traceback()) diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 4f3e7994a5..b0d1756643 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -9,7 +9,6 @@ from frappe import throw, _ from frappe.website.website_generator import WebsiteGenerator from frappe.utils.verified_command import get_signed_params, verify_request from frappe.utils.background_jobs import enqueue -from frappe.utils.scheduler import log from frappe.email.queue import send from frappe.email.doctype.email_group.email_group import add_subscribers from frappe.utils import parse_addr @@ -213,7 +212,7 @@ def send_newsletter(newsletter): doc.db_set("email_sent", 0) frappe.db.commit() - log("send_newsletter") + frappe.log_error("send_newsletter") raise diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py index 4d3c3167a5..b9bbde172d 100644 --- a/frappe/email/doctype/notification/test_notification.py +++ b/frappe/email/doctype/notification/test_notification.py @@ -136,7 +136,7 @@ class TestNotification(unittest.TestCase): "reference_name": event.name, "status": "Not Sent"})) frappe.set_user('Administrator') - frappe.utils.scheduler.trigger(frappe.local.site, "daily", now=True) + frappe.get_doc('Scheduled Job Type', dict(method='frappe.email.doctype.notification.notification.trigger_daily_alerts')).execute() # not today, so no alert self.assertFalse(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", @@ -150,7 +150,7 @@ class TestNotification(unittest.TestCase): self.assertFalse(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", "reference_name": event.name, "status": "Not Sent"})) - frappe.utils.scheduler.trigger(frappe.local.site, "daily", now=True) + frappe.get_doc('Scheduled Job Type', dict(method='frappe.email.doctype.notification.notification.trigger_daily_alerts')).execute() # today so show alert self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", diff --git a/frappe/email/queue.py b/frappe/email/queue.py index f51451751d..792b47296a 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -12,7 +12,6 @@ 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 rq.timeouts import JobTimeoutException -from frappe.utils.scheduler import log from six import text_type, string_types class EmailLimitCrossedError(frappe.ValidationError): pass @@ -469,7 +468,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals else: # log to Error Log - log('frappe.email.queue.flush', text_type(e)) + frappe.log_error('frappe.email.queue.flush') def prepare_message(email, recipient, recipients_list): message = email.message diff --git a/frappe/email/receive.py b/frappe/email/receive.py index b8fde57a43..e5c8457b4e 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -12,7 +12,6 @@ import frappe from frappe import _, safe_decode, safe_encode from frappe.utils import (extract_email_id, convert_utc_to_user_timezone, now, cint, cstr, strip, markdown, parse_addr) -from frappe.utils.scheduler import log from frappe.core.doctype.file.file import get_random_filename, MaxFileSizeReachedError class EmailSizeExceededError(frappe.ValidationError): pass @@ -80,7 +79,7 @@ class EmailServer: except _socket.error: # log performs rollback and logs error in Error Log - log("receive.connect_pop") + frappe.log_error("receive.connect_pop") # Invalid mail server -- due to refusing connection frappe.msgprint(_('Invalid Mail Server. Please rectify and try again.')) @@ -255,7 +254,7 @@ class EmailServer: else: # log performs rollback and logs error in Error Log - log("receive.get_messages", self.make_error_msg(msg_num, incoming_mail)) + frappe.log_error("receive.get_messages", self.make_error_msg(msg_num, incoming_mail)) self.errors = True frappe.db.rollback() @@ -457,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 = [] @@ -481,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 @@ -515,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/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/hooks.py b/frappe/hooks.py index b35387efe9..5406d0fc57 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -76,8 +76,7 @@ leaderboards = "frappe.desk.leaderboard.get_leaderboards" on_session_creation = [ "frappe.core.doctype.activity_log.feed.login_feed", - "frappe.core.doctype.user.user.notify_admin_access_to_system_manager", - "frappe.utils.scheduler.reset_enabled_scheduler_events", + "frappe.core.doctype.user.user.notify_admin_access_to_system_manager" ] on_logout = "frappe.core.doctype.session_default_settings.session_default_settings.clear_session_defaults" @@ -153,14 +152,18 @@ doc_events = { } scheduler_events = { + "cron": { + "0/15 * * * *": [ + "frappe.oauth.delete_oauth2_data", + "frappe.website.doctype.web_page.web_page.check_publish_status", + "frappe.twofactor.delete_all_barcodes_for_users" + ] + }, "all": [ "frappe.email.queue.flush", "frappe.email.doctype.email_account.email_account.pull", "frappe.email.doctype.email_account.email_account.notify_unreplied", - "frappe.oauth.delete_oauth2_data", "frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment", - "frappe.twofactor.delete_all_barcodes_for_users", - "frappe.website.doctype.web_page.web_page.check_publish_status", 'frappe.utils.global_search.sync_global_search' ], "hourly": [ diff --git a/frappe/installer.py b/frappe/installer.py index 764a0b6780..4b07ab8ce8 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -18,15 +18,16 @@ from frappe.utils.fixtures import sync_fixtures from frappe.website import render from frappe.modules.utils import sync_customizations 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 @@ -91,6 +92,7 @@ def install_app(name, verbose=False, set_as_patched=True): for after_install in app_hooks.after_install or []: frappe.get_attr(after_install)() + sync_jobs() sync_fixtures(name) sync_customizations(name) @@ -189,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() @@ -207,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 8e415e6c20..290aae0e4e 100644 --- a/frappe/integrations/doctype/social_login_key/social_login_key.json +++ b/frappe/integrations/doctype/social_login_key/social_login_key.json @@ -1,700 +1,183 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "beta": 0, - "creation": "2017-11-18 15:36:09.676722", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", + "allow_import": 1, + "creation": "2017-11-18 15:36:09.676722", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enable_social_login", + "client_credentials", + "social_login_provider", + "client_id", + "column_break_0", + "provider_name", + "client_secret", + "sb_identity_details", + "icon", + "column_break_1", + "base_url", + "client_urls", + "authorize_url", + "access_token_url", + "column_break_3", + "redirect_url", + "api_endpoint", + "custom_base_url", + "client_information", + "api_endpoint_args", + "auth_url_data", + "user_id_property" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "enable_social_login", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Enable Social Login", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "enable_social_login", + "fieldtype": "Check", + "label": "Enable Social Login" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "eval:doc.enable_social_login", - "columns": 0, - "fieldname": "client_credentials", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Client Credentials", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "collapsible_depends_on": "eval:doc.enable_social_login", + "fieldname": "client_credentials", + "fieldtype": "Section Break", + "label": "Client Credentials" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Custom", - "depends_on": "eval:doc.custom!=1", - "fieldname": "social_login_provider", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Social Login Provider", - "length": 0, - "no_copy": 0, - "options": "Custom\nFacebook\nFrappe\nGitHub\nGoogle\nOffice 365\nSalesforce\nfairlogin", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 1, - "unique": 0 - }, + "default": "Custom", + "depends_on": "eval:doc.custom!=1", + "fieldname": "social_login_provider", + "fieldtype": "Select", + "label": "Social Login Provider", + "options": "Custom\nFacebook\nFrappe\nGitHub\nGoogle\nOffice 365\nSalesforce\nfairlogin", + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "client_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Client ID", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "client_id", + "fieldtype": "Data", + "label": "Client ID" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_0", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_0", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "provider_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Provider Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 1, - "unique": 0 - }, + "fieldname": "provider_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Provider Name", + "reqd": 1, + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "client_secret", - "fieldtype": "Password", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Client Secret", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "client_secret", + "fieldtype": "Password", + "label": "Client Secret" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "eval:doc.custom_base_url", - "columns": 0, - "depends_on": "", - "fieldname": "sb_identity_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Identity Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "collapsible_depends_on": "eval:doc.custom_base_url", + "fieldname": "sb_identity_details", + "fieldtype": "Section Break", + "label": "Identity Details" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "icon", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Icon", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "icon", + "fieldtype": "Data", + "label": "Icon" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_url", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Base URL", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "base_url", + "fieldtype": "Data", + "label": "Base URL" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"", - "columns": 0, - "depends_on": "", - "fieldname": "client_urls", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Client URLs", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"", + "fieldname": "client_urls", + "fieldtype": "Section Break", + "label": "Client URLs" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "authorize_url", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Authorize URL", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "authorize_url", + "fieldtype": "Data", + "label": "Authorize URL" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "access_token_url", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Access Token URL", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "access_token_url", + "fieldtype": "Data", + "label": "Access Token URL" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "redirect_url", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Redirect URL", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "redirect_url", + "fieldtype": "Data", + "label": "Redirect URL" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "api_endpoint", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "API Endpoint", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "api_endpoint", + "fieldtype": "Data", + "label": "API Endpoint" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "custom_base_url", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Custom Base URL", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "custom_base_url", + "fieldtype": "Check", + "hidden": 1, + "label": "Custom Base URL" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"", - "columns": 0, - "depends_on": "", - "fieldname": "client_information", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Client Information", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "collapsible_depends_on": "eval:doc.social_login_provider===\"Custom\"", + "fieldname": "client_information", + "fieldtype": "Section Break", + "label": "Client Information" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "api_endpoint_args", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "API Endpoint Args", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "api_endpoint_args", + "fieldtype": "Code", + "label": "API Endpoint Args" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "auth_url_data", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Auth URL Data", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "auth_url_data", + "fieldtype": "Code", + "label": "Auth URL Data" + }, + { + "depends_on": "eval:doc.social_login_provider===\"Custom\"", + "fieldname": "user_id_property", + "fieldtype": "Data", + "label": "User ID Property" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-09-15 09:00:00.000000", - "modified_by": "Administrator", - "module": "Integrations", - "name": "Social Login Key", - "name_case": "", - "owner": "Administrator", + ], + "modified": "2019-12-03 12:35:55.115260", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Social Login Key", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "provider_name", - "track_changes": 1, - "track_seen": 0 -} + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "provider_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/migrate.py b/frappe/migrate.py index 6778a3f18f..043b6817d7 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -15,6 +15,7 @@ from frappe.desk.notifications import clear_notifications from frappe.website import render from frappe.core.doctype.language.language import sync_languages from frappe.modules.utils import sync_customizations +from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.utils import global_search def migrate(verbose=True, rebuild_website=False, skip_failing=False): @@ -46,9 +47,11 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False): # run patches frappe.modules.patch_handler.run_all(skip_failing) + # sync frappe.model.sync.sync_all(verbose=verbose) frappe.translate.clear_cache() + sync_jobs() sync_fixtures() sync_customizations() sync_languages() diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 09db9bb68a..1fe92d7a67 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -45,7 +45,7 @@ default_fields = ('doctype','name','owner','creation','modified','modified_by', 'parent','parentfield','parenttype','idx','docstatus') optional_fields = ("_user_tags", "_comments", "_assign", "_liked_by", "_seen") table_fields = ('Table', 'Table MultiSelect') -core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'User', 'Role', 'Has Role', +core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link', 'User', 'Role', 'Has Role', 'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form', 'Customize Form Field', 'Property Setter', 'Custom Field', 'Custom Script') diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 6c917b8d4d..a50bf9fdaf 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -22,6 +22,8 @@ max_positive_value = { 'bigint': 2 ** 63 } +DOCTYPES_FOR_DOCTYPE = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link') + _classes = {} def get_controller(doctype): @@ -255,7 +257,7 @@ class BaseDocument(object): def get_valid_columns(self): if self.doctype not in frappe.local.valid_columns: - if self.doctype in ("DocField", "DocPerm") and self.parent in ("DocType", "DocField", "DocPerm"): + if self.doctype in DOCTYPES_FOR_DOCTYPE: from frappe.model.meta import get_table_columns valid = get_table_columns(self.doctype) else: @@ -312,7 +314,7 @@ class BaseDocument(object): self.created_by = self.modified_by = frappe.session.user # if doctype is "DocType", don't insert null values as we don't know who is valid yet - d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in ('DocType', 'DocField', 'DocPerm')) + d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE) columns = list(d) try: @@ -347,7 +349,7 @@ class BaseDocument(object): self.db_insert() return - d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in ('DocType', 'DocField', 'DocPerm')) + 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'] diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index b7feee82f4..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 @@ -513,6 +517,8 @@ class DatabaseQuery(object): or not can_be_null or (f.value and f.operator.lower() in ('=', 'like')) or 'ifnull(' in column_name.lower()): + if f.operator.lower() == 'like' and frappe.conf.get('db_type') == 'postgres': + f.operator = 'ilike' condition = '{column_name} {operator} {value}'.format( column_name=column_name, operator=f.operator, value=value) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index af67350ab6..43ffc6ac35 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -80,8 +80,8 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa if not (for_reload or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_test): try: delete_controllers(name, doc.module) - except (FileNotFoundError, OSError): - # in case a doctype doesnt have any controller code + except (FileNotFoundError, OSError, KeyError): + # in case a doctype doesnt have any controller code nor any app and module pass else: @@ -332,8 +332,8 @@ def clear_references(doctype, reference_doctype, reference_name, (reference_doctype, reference_name)) def clear_timeline_references(link_doctype, link_name): - frappe.db.sql("""delete from `tabCommunication Link` - where `tabCommunication Link`.link_doctype='{0}' and `tabCommunication Link`.link_name='{1}'""".format(link_doctype, link_name)) # nosec + frappe.db.sql("""DELETE FROM `tabCommunication Link` + WHERE `tabCommunication Link`.link_doctype=%s AND `tabCommunication Link`.link_name=%s""", (link_doctype, link_name)) def insert_feed(doc): from frappe.utils import get_fullname diff --git a/frappe/model/document.py b/frappe/model/document.py index 7f04895308..c58e09ef5a 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -150,8 +150,8 @@ class Document(BaseDocument): super(Document, self).__init__(d) if self.name=="DocType" and self.doctype=="DocType": - from frappe.model.meta import doctype_table_fields - table_fields = doctype_table_fields + from frappe.model.meta import DOCTYPE_TABLE_FIELDS + table_fields = DOCTYPE_TABLE_FIELDS else: table_fields = self.meta.get_table_fields() diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 0b1011b119..927a56b6b8 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -151,7 +151,7 @@ class Meta(Document): if self.name!="DocType": self._table_fields = self.get('fields', {"fieldtype": ['in', table_fields]}) else: - self._table_fields = doctype_table_fields + self._table_fields = DOCTYPE_TABLE_FIELDS return self._table_fields @@ -165,7 +165,7 @@ class Meta(Document): def get_valid_columns(self): if not hasattr(self, "_valid_columns"): - if self.name in ("DocType", "DocField", "DocPerm", "Property Setter"): + if self.name in ("DocType", "DocField", "DocPerm", 'DocType Action', 'DocType Link', "Property Setter"): self._valid_columns = get_table_columns(self.name) else: self._valid_columns = self.default_fields + \ @@ -174,7 +174,12 @@ class Meta(Document): return self._valid_columns def get_table_field_doctype(self, fieldname): - return { "fields": "DocField", "permissions": "DocPerm"}.get(fieldname) + return { + "fields": "DocField", + "permissions": "DocPerm", + "actions": "DocType Action", + 'links': 'DocType Link' + }.get(fieldname) def get_field(self, fieldname): '''Return docfield from meta''' @@ -419,11 +424,44 @@ class Meta(Document): except ImportError: pass + self.add_doctype_links(data) + for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []): data = frappe.get_attr(hook)(data=data) return data + def add_doctype_links(self, data): + '''add `links` child table in standard link dashboard format''' + if hasattr(self, 'links') and self.links: + if not data.transactions: + # init groups + data.transactions = [] + data.non_standard_fieldnames = {} + + for link in self.links: + link.added = False + for group in data.transactions: + # group found + if group.label == link.label: + if not link.link_doctype in group.items: + group.items.append(link.link_doctype) + link.added = True + + if not link.added: + # group not found, make a new group + data.transactions.append(dict( + label = link.group, + items = [link.link_doctype] + )) + + if link.link_fieldname != data.fieldname: + if data.fieldname: + data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname + else: + data.fieldname = link.link_fieldname + + def get_row_template(self): return self.get_web_template(suffix='_row') @@ -441,9 +479,11 @@ class Meta(Document): def is_nested_set(self): return self.has_field('lft') and self.has_field('rgt') -doctype_table_fields = [ +DOCTYPE_TABLE_FIELDS = [ frappe._dict({"fieldname": "fields", "options": "DocField"}), - frappe._dict({"fieldname": "permissions", "options": "DocPerm"}) + frappe._dict({"fieldname": "permissions", "options": "DocPerm"}), + frappe._dict({"fieldname": "actions", "options": "DocType Action"}), + frappe._dict({"fieldname": "links", "options": "DocType Link"}), ] ####### diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 18bf827c5f..b50ddb1160 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -29,6 +29,8 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe # these need to go first at time of install for d in (("core", "docfield"), ("core", "docperm"), + ("core", "doctype_action"), + ("core", "doctype_link"), ("core", "role"), ("core", "has_role"), ("core", "doctype"), @@ -41,7 +43,10 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe ("data_migration", "data_migration_mapping_detail"), ("data_migration", "data_migration_mapping"), ("data_migration", "data_migration_plan_mapping"), - ("data_migration", "data_migration_plan")): + ("data_migration", "data_migration_plan"), + ("desk", "onboarding_slide_field"), + ("desk", "onboarding_slide_help_link"), + ("desk", "onboarding_slide")): files.append(os.path.join(frappe.get_app_path("frappe"), d[0], "doctype", d[1], d[1] + ".json")) @@ -70,7 +75,7 @@ def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=F # load in sequence - warning for devs document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format', 'website_theme', 'web_form', 'notification', 'print_style', - 'data_migration_mapping', 'data_migration_plan'] + 'data_migration_mapping', 'data_migration_plan', 'onboarding_slide'] for doctype in document_types: doctype_path = os.path.join(start_path, doctype) if os.path.exists(doctype_path): diff --git a/frappe/patches.txt b/frappe/patches.txt index 9473323059..9612994168 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -6,9 +6,11 @@ frappe.patches.v8_0.update_global_search_table frappe.patches.v7_0.update_auth frappe.patches.v8_0.drop_in_dialog #2017-09-22 frappe.patches.v7_2.remove_in_filter -frappe.patches.v11_0.drop_column_apply_user_permissions +execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23 +execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2019-09-23 execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20 +frappe.patches.v11_0.drop_column_apply_user_permissions execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') execute:frappe.reload_doc('core', 'doctype', 'docperm') #2018-05-29 execute:frappe.reload_doc('core', 'doctype', 'comment') @@ -257,3 +259,7 @@ frappe.patches.v12_0.setup_tags 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 +execute:frappe.delete_doc("Test Runner") +frappe.patches.v12_0.setup_email_linking +frappe.patches.v12_0.fix_home_settings_for_all_users 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 new file mode 100644 index 0000000000..f7b9e476a9 --- /dev/null +++ b/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py @@ -0,0 +1,26 @@ +from __future__ import unicode_literals +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 + if frappe.db.exists("Prepared Report", file_dict.attached_to_name): + try: + file_doc = frappe.get_doc("File", file_dict.name) + file_doc.is_private = 1 + file_doc.save() + except Exception: + # File might not exist on the file system in that case delete both Prepared Report and File doc + frappe.delete_doc("Prepared Report", file_dict.attached_to_name) + 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..8a6bcca9a0 --- /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['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'] += 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/doctype/letter_head/letter_head.py b/frappe/printing/doctype/letter_head/letter_head.py index 1a70ebcb08..d75b253dd3 100644 --- a/frappe/printing/doctype/letter_head/letter_head.py +++ b/frappe/printing/doctype/letter_head/letter_head.py @@ -4,8 +4,7 @@ from __future__ import unicode_literals import frappe from frappe.utils import is_image - - +from frappe import _ from frappe.model.document import Document class LetterHead(Document): @@ -43,3 +42,16 @@ class LetterHead(Document): # update control panel - so it loads new letter directly frappe.db.set_default("default_letter_head_content", self.content) + + def create_onboarding_docs(self, args): + letterhead = args.get('letterhead') + if letterhead: + try: + frappe.get_doc({ + 'doctype': self.doctype, + 'image': letterhead, + 'letter_head_name': _('Standard'), + 'is_default': 1 + }).insert() + except frappe.NameError: + pass \ No newline at end of file diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index a5d41ee099..951a863776 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -1,905 +1,223 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2013-01-23 19:54:43", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 0, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fetch_if_empty": 0, - "fieldname": "doc_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "DocType", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "module", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Module", - "length": 0, - "no_copy": 0, - "options": "Module Def", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Disabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "No", - "fetch_if_empty": 0, - "fieldname": "standard", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Standard", - "length": 0, - "no_copy": 1, - "oldfieldname": "standard", - "oldfieldtype": "Select", - "options": "No\nYes", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "custom_format", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Custom Format", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "custom_format", - "fetch_if_empty": 0, - "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Jinja", - "depends_on": "custom_format", - "description": "", - "fetch_if_empty": 0, - "fieldname": "print_format_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Print Format Type", - "length": 0, - "no_copy": 0, - "options": "Jinja\nJS", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, - "fieldname": "raw_printing", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Raw Printing", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.raw_printing", - "fetch_if_empty": 0, - "fieldname": "html", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "HTML", - "length": 0, - "no_copy": 0, - "oldfieldname": "html", - "oldfieldtype": "Text Editor", - "options": "HTML", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "raw_printing", - "description": "Any string-based printer languages can be used. Writing raw commands requires knowledge of the printer's native language provided by the printer manufacturer. Please refer to the developer manual provided by the printer manufacturer on how to write their native commands. These commands are rendered on the server side using the Jinja Templating Language.", - "fetch_if_empty": 0, - "fieldname": "raw_commands", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Raw Commands", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.custom_format", - "fetch_if_empty": 0, - "fieldname": "section_break_9", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Style Settings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "align_labels_right", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Align Labels to the Right", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "show_section_headings", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Show Section Headings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "line_breaks", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Show Line Breaks after Sections", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_11", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "default_print_language", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Print Language", - "length": 0, - "no_copy": 0, - "options": "Language", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Default", - "depends_on": "eval:!doc.custom_format", - "fetch_if_empty": 0, - "fieldname": "font", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Font", - "length": 0, - "no_copy": 0, - "options": "Default\nArial\nHelvetica\nVerdana\nMonospace", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.raw_printing", - "fetch_if_empty": 0, - "fieldname": "css_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "css", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Custom CSS", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "custom_html_help", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Custom HTML Help", - "length": 0, - "no_copy": 0, - "options": "

Custom CSS Help

\n\n

Notes:

\n\n
    \n
  1. All field groups (label + value) are set attributes data-fieldtype and data-fieldname
  2. \n
  3. All values are given class value
  4. \n
  5. All Section Breaks are given class section-break
  6. \n
  7. All Column Breaks are given class column-break
  8. \n
\n\n

Examples

\n\n

1. Left align integers

\n\n
[data-fieldtype=\"Int\"] .value { text-left: left; }
\n\n

1. Add border to sections except the last section

\n\n
.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px;  }
\n", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "custom_format", - "fetch_if_empty": 0, - "fieldname": "section_break_13", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "custom_format", - "fetch_if_empty": 0, - "fieldname": "print_format_help", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Print Format Help", - "length": 0, - "no_copy": 0, - "options": "

Print Format Help

\n
\n

Introduction

\n

Print itemsFormats are rendered on the server side using the Jinja Templating Language. All forms have access to the doc object which contains information about the document that is being formatted. You can also access common utilities via the frappe module.

\n

For styling, the Boostrap CSS framework is provided and you can enjoy the full range of classes.

\n
\n

References

\n
    \n\t
  1. Jinja Tempalting Language: Reference
  2. \n\t
  3. Bootstrap CSS Framework
  4. \n
\n
\n

Example

\n
<h3>{{ doc.select_print_heading or \"Invoice\" }}</h3>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Customer Name</div>\n\t<div class=\"col-md-9\">{{ doc.customer_name }}</div>\n</div>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Date</div>\n\t<div class=\"col-md-9\">{{ doc.get_formatted(\"invoice_date\") }}</div>\n</div>\n<table class=\"table table-bordered\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<th>Sr</th>\n\t\t\t<th>Item Name</th>\n\t\t\t<th>Description</th>\n\t\t\t<th class=\"text-right\">Qty</th>\n\t\t\t<th class=\"text-right\">Rate</th>\n\t\t\t<th class=\"text-right\">Amount</th>\n\t\t</tr>\n\t\t{%- for row in doc.items -%}\n\t\t<tr>\n\t\t\t<td style=\"width: 3%;\">{{ row.idx }}</td>\n\t\t\t<td style=\"width: 20%;\">\n\t\t\t\t{{ row.item_name }}\n\t\t\t\t{% if row.item_code != row.item_name -%}\n\t\t\t\t<br>Item Code: {{ row.item_code}}\n\t\t\t\t{%- endif %}\n\t\t\t</td>\n\t\t\t<td style=\"width: 37%;\">\n\t\t\t\t<div style=\"border: 0px;\">{{ row.description }}</div></td>\n\t\t\t<td style=\"width: 10%; text-align: right;\">{{ row.qty }} {{ row.uom or row.stock_uom }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"rate\", doc) }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"amount\", doc) }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>
\n
\n

Common Functions

\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
doc.get_formatted(\"[fieldname]\", [parent_doc])Get document value formatted as Date, Currency etc. Pass parent doc for curreny type fields.
frappe.db.get_value(\"[doctype]\", \"[name]\", \"fieldname\")Get value from another document.
\n", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "format_data", - "fieldtype": "Code", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Format Data", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "print_format_builder", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Print Format Builder", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_toolbar": 0, - "icon": "fa fa-print", - "idx": 1, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-06-05 12:45:25.869180", - "modified_by": "Administrator", - "module": "Printing", - "name": "Print Format", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 - } \ No newline at end of file + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2013-01-23 19:54:43", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "doc_type", + "module", + "disabled", + "column_break_3", + "standard", + "custom_format", + "section_break_6", + "print_format_type", + "raw_printing", + "html", + "raw_commands", + "section_break_9", + "align_labels_right", + "show_section_headings", + "line_breaks", + "column_break_11", + "default_print_language", + "font", + "css_section", + "css", + "custom_html_help", + "section_break_13", + "print_format_help", + "format_data", + "print_format_builder" + ], + "fields": [ + { + "fieldname": "doc_type", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "No", + "fieldname": "standard", + "fieldtype": "Select", + "in_filter": 1, + "label": "Standard", + "no_copy": 1, + "oldfieldname": "standard", + "oldfieldtype": "Select", + "options": "No\nYes", + "reqd": 1, + "search_index": 1 + }, + { + "default": "0", + "fieldname": "custom_format", + "fieldtype": "Check", + "label": "Custom Format" + }, + { + "depends_on": "custom_format", + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "default": "Jinja", + "depends_on": "custom_format", + "fieldname": "print_format_type", + "fieldtype": "Select", + "label": "Print Format Type", + "options": "Jinja\nJS" + }, + { + "default": "0", + "fieldname": "raw_printing", + "fieldtype": "Check", + "label": "Raw Printing" + }, + { + "depends_on": "eval:!doc.raw_printing", + "fieldname": "html", + "fieldtype": "Code", + "label": "HTML", + "oldfieldname": "html", + "oldfieldtype": "Text Editor", + "options": "HTML" + }, + { + "depends_on": "raw_printing", + "description": "Any string-based printer languages can be used. Writing raw commands requires knowledge of the printer's native language provided by the printer manufacturer. Please refer to the developer manual provided by the printer manufacturer on how to write their native commands. These commands are rendered on the server side using the Jinja Templating Language.", + "fieldname": "raw_commands", + "fieldtype": "Code", + "label": "Raw Commands" + }, + { + "depends_on": "eval:!doc.custom_format", + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Style Settings" + }, + { + "default": "0", + "fieldname": "align_labels_right", + "fieldtype": "Check", + "label": "Align Labels to the Right" + }, + { + "default": "0", + "fieldname": "show_section_headings", + "fieldtype": "Check", + "label": "Show Section Headings" + }, + { + "default": "0", + "fieldname": "line_breaks", + "fieldtype": "Check", + "label": "Show Line Breaks after Sections" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_print_language", + "fieldtype": "Link", + "label": "Default Print Language", + "options": "Language" + }, + { + "default": "Default", + "depends_on": "eval:!doc.custom_format", + "fieldname": "font", + "fieldtype": "Select", + "label": "Font", + "options": "Default\nArial\nHelvetica\nVerdana\nMonospace" + }, + { + "depends_on": "eval:!doc.raw_printing", + "fieldname": "css_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "css", + "fieldtype": "Code", + "label": "Custom CSS", + "options": "CSS" + }, + { + "fieldname": "custom_html_help", + "fieldtype": "HTML", + "label": "Custom HTML Help", + "options": "

Custom CSS Help

\n\n

Notes:

\n\n
    \n
  1. All field groups (label + value) are set attributes data-fieldtype and data-fieldname
  2. \n
  3. All values are given class value
  4. \n
  5. All Section Breaks are given class section-break
  6. \n
  7. All Column Breaks are given class column-break
  8. \n
\n\n

Examples

\n\n

1. Left align integers

\n\n
[data-fieldtype=\"Int\"] .value { text-left: left; }
\n\n

1. Add border to sections except the last section

\n\n
.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px;  }
\n" + }, + { + "depends_on": "custom_format", + "fieldname": "section_break_13", + "fieldtype": "Section Break" + }, + { + "depends_on": "custom_format", + "fieldname": "print_format_help", + "fieldtype": "HTML", + "label": "Print Format Help", + "options": "

Print Format Help

\n
\n

Introduction

\n

Print itemsFormats are rendered on the server side using the Jinja Templating Language. All forms have access to the doc object which contains information about the document that is being formatted. You can also access common utilities via the frappe module.

\n

For styling, the Boostrap CSS framework is provided and you can enjoy the full range of classes.

\n
\n

References

\n
    \n\t
  1. Jinja Tempalting Language: Reference
  2. \n\t
  3. Bootstrap CSS Framework
  4. \n
\n
\n

Example

\n
<h3>{{ doc.select_print_heading or \"Invoice\" }}</h3>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Customer Name</div>\n\t<div class=\"col-md-9\">{{ doc.customer_name }}</div>\n</div>\n<div class=\"row\">\n\t<div class=\"col-md-3 text-right\">Date</div>\n\t<div class=\"col-md-9\">{{ doc.get_formatted(\"invoice_date\") }}</div>\n</div>\n<table class=\"table table-bordered\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<th>Sr</th>\n\t\t\t<th>Item Name</th>\n\t\t\t<th>Description</th>\n\t\t\t<th class=\"text-right\">Qty</th>\n\t\t\t<th class=\"text-right\">Rate</th>\n\t\t\t<th class=\"text-right\">Amount</th>\n\t\t</tr>\n\t\t{%- for row in doc.items -%}\n\t\t<tr>\n\t\t\t<td style=\"width: 3%;\">{{ row.idx }}</td>\n\t\t\t<td style=\"width: 20%;\">\n\t\t\t\t{{ row.item_name }}\n\t\t\t\t{% if row.item_code != row.item_name -%}\n\t\t\t\t<br>Item Code: {{ row.item_code}}\n\t\t\t\t{%- endif %}\n\t\t\t</td>\n\t\t\t<td style=\"width: 37%;\">\n\t\t\t\t<div style=\"border: 0px;\">{{ row.description }}</div></td>\n\t\t\t<td style=\"width: 10%; text-align: right;\">{{ row.qty }} {{ row.uom or row.stock_uom }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"rate\", doc) }}</td>\n\t\t\t<td style=\"width: 15%; text-align: right;\">{{\n\t\t\t\trow.get_formatted(\"amount\", doc) }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>
\n
\n

Common Functions

\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
doc.get_formatted(\"[fieldname]\", [parent_doc])Get document value formatted as Date, Currency etc. Pass parent doc for curreny type fields.
frappe.db.get_value(\"[doctype]\", \"[name]\", \"fieldname\")Get value from another document.
\n" + }, + { + "fieldname": "format_data", + "fieldtype": "Code", + "hidden": 1, + "label": "Format Data" + }, + { + "default": "0", + "fieldname": "print_format_builder", + "fieldtype": "Check", + "hidden": 1, + "label": "Print Format Builder" + } + ], + "icon": "fa fa-print", + "idx": 1, + "modified": "2019-11-28 12:40:40.364699", + "modified_by": "faris@erpnext.com", + "module": "Printing", + "name": "Print Format", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ 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 new file mode 100644 index 0000000000..2f51d2e18a --- /dev/null +++ b/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json @@ -0,0 +1,36 @@ +{ + "add_more_button": 0, + "app": "ERPNext", + "creation": "2019-11-22 13:25:42.892593", + "docstatus": 0, + "doctype": "Onboarding Slide", + "domains": [], + "help_links": [ + { + "label": "Need Help?", + "video_id": "cKZHcx1znMc" + } + ], + "idx": 0, + "image_src": "/assets/erpnext/images/illustrations/letterhead-onboard.png", + "max_count": 0, + "modified": "2019-12-03 22:54:57.618989", + "modified_by": "Administrator", + "name": "Company Letter Head", + "owner": "Administrator", + "ref_doctype": "Letter Head", + "slide_desc": "

The letter head will appear across all print formats and PDFs

\n

Keep it web friendly as 1024px by 128px

", + "slide_fields": [ + { + "align": "center", + "fieldname": "letterhead", + "fieldtype": "Attach Image", + "label": "Attach Letterhead", + "options": "image", + "reqd": 0 + } + ], + "slide_order": 20, + "slide_title": "Company Letter Head", + "slide_type": "Create" +} \ No newline at end of file diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js index 8605fb762e..f9bb524d54 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -695,7 +695,8 @@ frappe.PrintFormatBuilder = Class.extend({ { fieldname: "content", fieldtype: "Code", - label: label + label: label, + options: "HTML" }, { fieldname: "help", diff --git a/frappe/public/build.json b/frappe/public/build.json index 32a2cfd223..76d6d74563 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -148,6 +148,7 @@ "public/js/frappe/ui/page.html", "public/js/frappe/ui/page.js", "public/js/frappe/ui/slides.js", + "public/js/frappe/ui/onboarding_dialog.js", "public/js/frappe/ui/find.js", "public/js/frappe/ui/iconbar.js", "public/js/frappe/form/layout.js", diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index 8e43be12ea..6ddf93df6a 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -1158,16 +1158,6 @@ input[type="checkbox"]:focus { color: #fff; border-color: #b1bdca; } -.user-progress-dialog .slides-progress { +.onboarding-dialog .slides-progress { margin-top: 15px; } -.user-progress-dialog .done-state .check-container { - font-size: 64px; - margin: 40px; -} -.user-progress-dialog .done-state .title { - font-weight: normal; -} -.user-progress-dialog .done-state .help-links a { - margin: 0px 10px; -} diff --git a/frappe/public/images/ui-states/empty.png b/frappe/public/images/ui-states/empty.png new file mode 100644 index 0000000000..de2a893508 Binary files /dev/null and b/frappe/public/images/ui-states/empty.png differ diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 4fbea6684f..92194acdca 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -88,6 +88,9 @@ frappe.Application = Class.extend({ } this.show_update_available(); + if (frappe.boot.is_first_startup) { + this.setup_onboarding_wizard(); + } if(frappe.ui.startup_setup_dialog && !frappe.boot.setup_complete) { frappe.ui.startup_setup_dialog.pre_show(); @@ -482,6 +485,20 @@ frappe.Application = Class.extend({ }); }, + setup_onboarding_wizard: () => { + frappe.call('frappe.desk.doctype.onboarding_slide.onboarding_slide.get_onboarding_slides').then(res => { + if (res.message) { + let slides = res.message; + if (slides.length) { + this.progress_dialog = new frappe.setup.OnboardingDialog({ + slides: slides + }); + this.progress_dialog.show(); + } + } + }); + }, + setup_analytics: function() { if(window.mixpanel) { window.mixpanel.identify(frappe.session.user); diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index 026cdbec25..0b31776caf 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -10,13 +10,7 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({ set_options() { if (this.df.options) { let options = this.df.options || []; - if (typeof options === 'string') { - options = options.split('\n'); - } - if (typeof options[0] === 'string') { - options = options.map(o => ({ label: o, value: o })); - } - this._data = options; + this._data = this.parse_options(options); } }, @@ -100,6 +94,9 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({ }, validate(value) { + if (this.df.ignore_validation) { + return value || ''; + } let valid_values = this.awesomplete._list.map(d => d.value); if (!valid_values.length) { return value; @@ -111,11 +108,22 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({ } }, + parse_options(options) { + if (typeof options === 'string') { + options = options.split('\n'); + } + if (typeof options[0] === 'string') { + options = options.map(o => ({ label: o, value: o })); + } + return options; + }, + get_data() { return this._data || []; }, set_data(data) { + data = this.parse_options(data); if (this.awesomplete) { this.awesomplete.list = data; } diff --git a/frappe/public/js/frappe/form/controls/comment.js b/frappe/public/js/frappe/form/controls/comment.js index eba82fd7b2..a64df56bca 100644 --- a/frappe/public/js/frappe/form/controls/comment.js +++ b/frappe/public/js/frappe/form/controls/comment.js @@ -1,7 +1,7 @@ import Quill from 'quill'; import Mention from './quill-mention/quill.mention'; -Quill.register('modules/mention', Mention); +Quill.register('modules/mention', Mention, true); frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({ make_wrapper() { diff --git a/frappe/public/js/frappe/form/controls/currency.js b/frappe/public/js/frappe/form/controls/currency.js index 196ae5fd43..5a50ad9de6 100644 --- a/frappe/public/js/frappe/form/controls/currency.js +++ b/frappe/public/js/frappe/form/controls/currency.js @@ -1,29 +1,9 @@ frappe.ui.form.ControlCurrency = frappe.ui.form.ControlFloat.extend({ - eval_expression: function(value) { - if (typeof value ==='string' && value.match(/^[0-9+-/* ]+$/)) { - // Removes seperator - value = strip_number_groups(value, this.get_number_format()); - - try { - return eval(value); - } catch (e) { - return value; - } - } - // If not string - return value; - }, - format_for_input: function(value) { - var formatted_value = format_number(parseFloat(value), this.get_number_format(), this.get_precision()); + var formatted_value = format_number(value, this.get_number_format(), this.get_precision()); return isNaN(parseFloat(value)) ? "" : formatted_value; }, - get_number_format: function() { - var currency = frappe.meta.get_field_currency(this.df, this.get_doc()); - return get_number_format(currency); - }, - get_precision: function() { // always round based on field precision or currency's precision // this method is also called in this.parse() diff --git a/frappe/public/js/frappe/form/controls/float.js b/frappe/public/js/frappe/form/controls/float.js index d32f29e28a..027cfebc2a 100644 --- a/frappe/public/js/frappe/form/controls/float.js +++ b/frappe/public/js/frappe/form/controls/float.js @@ -1,10 +1,7 @@ frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({ parse: function(value) { value = this.eval_expression(value); - return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision(), - // While parsing currency, get_number_format passes currency's number_format - // In case of parsing float, it passes global number_format - this.get_number_format()); + return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision()); }, format_for_input: function(value) { @@ -12,13 +9,13 @@ frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({ if (this.df.fieldtype==="Float" && this.df.options && this.df.options.trim()) { number_format = this.get_number_format(); } - var formatted_value = format_number(parseFloat(value), number_format, this.get_precision()); + var formatted_value = format_number(value, number_format, this.get_precision()); return isNaN(parseFloat(value)) ? "" : formatted_value; }, get_number_format: function() { - // In case of 'Float' field currency's number_format shouldn't be used for formatting - return get_number_format(); + var currency = frappe.meta.get_field_currency(this.df, this.get_doc()); + return get_number_format(currency); }, get_precision: function() { diff --git a/frappe/public/js/frappe/form/controls/int.js b/frappe/public/js/frappe/form/controls/int.js index ead48a996c..5639e5f132 100644 --- a/frappe/public/js/frappe/form/controls/int.js +++ b/frappe/public/js/frappe/form/controls/int.js @@ -20,20 +20,18 @@ frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({ }); }, eval_expression: function(value) { - if (typeof value==='string' - && value.match(/^[0-9+-/* ]+$/) - // strings with commas are evaluated incorrectly - // for e.g 47,186.00 -> 186 - && !value.includes(',')) { - try { - return eval(value); - } catch (e) { - // bad expression - return value; + if (typeof value === 'string') { + if (value.match(/^[0-9\+\-\/\* ]+$/)) { + // If it is a string containing operators + try { + return eval(value); + } catch (e) { + // bad expression + return value; + } } - } else { - return value; } + return value; }, parse: function(value) { return cint(this.eval_expression(value), null); diff --git a/frappe/public/js/frappe/form/controls/multiselect_list.js b/frappe/public/js/frappe/form/controls/multiselect_list.js index 0313ee70d7..3e8dc21dca 100644 --- a/frappe/public/js/frappe/form/controls/multiselect_list.js +++ b/frappe/public/js/frappe/form/controls/multiselect_list.js @@ -17,6 +17,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ this.$list_wrapper = $(template); this.$input = $(''); + this.input = this.$input.get(0); this.$list_wrapper.prependTo(this.input_area); this.$filter_input = this.$list_wrapper.find('input'); this.$list_wrapper.on('click', '.dropdown-menu', e => { diff --git a/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js b/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js index 26485adcc0..1c5787f854 100644 --- a/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js +++ b/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js @@ -35,4 +35,4 @@ MentionBlot.blotName = 'mention'; MentionBlot.tagName = 'span'; MentionBlot.className = 'mention'; -Quill.register(MentionBlot); +Quill.register(MentionBlot, true); diff --git a/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js b/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js index 0b50ffd758..80ee5a6c6d 100644 --- a/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js +++ b/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js @@ -361,6 +361,6 @@ class Mention { } } -Quill.register('modules/mention', Mention); +Quill.register('modules/mention', Mention, true); export default Mention; diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index 1b9078ae03..b35c92c1ae 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -36,7 +36,7 @@ class MyLink extends Link { } } -Quill.register(MyLink); +Quill.register(MyLink, true); // image uploader const Uploader = Quill.import('modules/uploader'); diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 5a983986d8..2f30228aff 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -111,6 +111,7 @@ frappe.ui.form.Form = class FrappeForm { $("body").attr("data-sidebar", 1); } this.setup_file_drop(); + this.setup_doctype_actions(); this.setup_done = true; } @@ -319,6 +320,29 @@ frappe.ui.form.Form = class FrappeForm { } } + // sets up the refresh event for custom buttons + // added via configuration + setup_doctype_actions() { + if (this.meta.actions) { + for (let action of this.meta.actions) { + frappe.ui.form.on(this.doctype, 'refresh', () => { + if (!this.is_new()) { + this.add_custom_button(action.label, () => { + if (action.action_type==='Server Action') { + frappe.xcall(action.action, {doc: this.doc}).then(() => { + frappe.msgprint({ + message: __('{} Complete', [action.label]), + alert: true + }); + }); + } + }, action.group); + } + }); + } + } + } + switch_doc(docname) { // record switch if(this.docname != docname && (!this.meta.in_dialog || this.in_form) && !this.meta.istable) { @@ -328,7 +352,11 @@ frappe.ui.form.Form = class FrappeForm { } } // reset visible columns, since column headings can change in different docs - this.grids.forEach(grid_obj => grid_obj.grid.visible_columns = null); + this.grids.forEach(grid_obj => { + grid_obj.grid.visible_columns = null + // reset page number to 1 + grid_obj.grid.grid_pagination.go_to_page(1); + }); frappe.ui.form.close_grid_form(); this.docname = docname; } diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 3de62daf64..6da742638a 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -2,6 +2,7 @@ // MIT License. See license.txt import GridRow from "./grid_row"; +import GridPagination from './grid_pagination'; frappe.ui.form.get_open_grid_form = function() { return $(".grid-row-open").data("grid_row"); @@ -47,8 +48,8 @@ export default class Grid { return false; } } + make() { - var me = this; let template = `
@@ -63,25 +64,38 @@ export default class Grid {