diff --git a/.eslintrc b/.eslintrc index cc7f555669..937f11586c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -148,6 +148,7 @@ "context": true, "before": true, "beforeEach": true, + "after": true, "qz": true, "localforage": true, "extend_cscript": true diff --git a/cypress/integration/first_day_of_the_week.js b/cypress/integration/first_day_of_the_week.js new file mode 100644 index 0000000000..1e65b78990 --- /dev/null +++ b/cypress/integration/first_day_of_the_week.js @@ -0,0 +1,45 @@ +context("First Day of the Week", () => { + before(() => { + cy.login(); + }); + + beforeEach(() => { + cy.visit('/app/system-settings'); + cy.findByText('Date and Number Format').click(); + }); + + it("Date control starts with same day as selected in System Settings", () => { + cy.intercept('POST', '/api/method/frappe.core.doctype.system_settings.system_settings.load').as("load_settings"); + cy.fill_field('first_day_of_the_week', 'Tuesday', 'Select'); + cy.findByRole('button', {name: 'Save'}).click(); + cy.wait("@load_settings"); + cy.dialog({ + title: 'Date', + fields: [ + { + label: 'Date', + fieldname: 'date', + fieldtype: 'Date' + } + ] + }); + cy.get_field('date').click(); + cy.get('.datepicker--day-name').eq(0).should('have.text', 'Tu'); + }); + + it("Calendar view starts with same day as selected in System Settings", () => { + cy.intercept('POST', '/api/method/frappe.core.doctype.system_settings.system_settings.load').as("load_settings"); + cy.fill_field('first_day_of_the_week', 'Monday', 'Select'); + cy.findByRole('button', {name: 'Save'}).click(); + cy.wait("@load_settings"); + cy.visit("app/todo/view/calendar/default"); + cy.get('.fc-day-header > span').eq(0).should('have.text', 'Mon'); + }); + + after(() => { + cy.visit('/app/system-settings'); + cy.findByText('Date and Number Format').click(); + cy.fill_field('first_day_of_the_week', 'Sunday', 'Select'); + cy.findByRole('button', {name: 'Save'}).click(); + }); +}); \ No newline at end of file diff --git a/cypress/integration/grid_keyboard_shortcut.js b/cypress/integration/grid_keyboard_shortcut.js index dee056e03e..9cf39165ad 100644 --- a/cypress/integration/grid_keyboard_shortcut.js +++ b/cypress/integration/grid_keyboard_shortcut.js @@ -1,48 +1,39 @@ context('Grid Keyboard Shortcut', () => { let total_count = 0; - beforeEach(() => { - cy.login(); - cy.visit('/app/doctype/User'); - }); before(() => { cy.login(); - cy.visit('/app/doctype/User'); - return cy.window().its('frappe').then(frappe => { - frappe.db.count('DocField', { - filters: { - 'parent': 'User', 'parentfield': 'fields', 'parenttype': 'DocType' - } - }).then((r) => { - total_count = r; - }); - }); + }); + beforeEach(() => { + cy.reload(); + cy.visit('/app/contact/new-contact-1'); + cy.get('.frappe-control[data-fieldname="email_ids"]').find(".grid-add-row").click(); }); it('Insert new row at the end', () => { cy.add_new_row_in_grid('{ctrl}{shift}{downarrow}', (cy, total_count) => { - cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', `${total_count+1}`); + cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', `${total_count+1}`); }, total_count); }); it('Insert new row at the top', () => { cy.add_new_row_in_grid('{ctrl}{shift}{uparrow}', (cy) => { - cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', '1'); + cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', '2'); }); }); it('Insert new row below', () => { cy.add_new_row_in_grid('{ctrl}{downarrow}', (cy) => { - cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', '2'); + cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', '1'); }); }); it('Insert new row above', () => { cy.add_new_row_in_grid('{ctrl}{uparrow}', (cy) => { - cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', '1'); + cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', '2'); }); }); }); Cypress.Commands.add('add_new_row_in_grid', (shortcut_keys, callbackFn, total_count) => { - cy.get('.frappe-control[data-fieldname="fields"]').as('table'); - cy.get('@table').find('.grid-body .col-xs-2').first().click(); - cy.get('@table').find('.grid-body .col-xs-2') + cy.get('.frappe-control[data-fieldname="email_ids"]').as('table'); + cy.get('@table').find('.grid-body [data-fieldname="email_id"]').first().click(); + cy.get('@table').find('.grid-body [data-fieldname="email_id"]') .first().type(shortcut_keys); callbackFn(cy, total_count); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 4fe315c372..758b3cde2b 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -193,7 +193,8 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => { }); Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => { - let selector = `[data-fieldname="${fieldname}"] input:visible`; + let field_element = fieldtype === 'Select' ? 'select': 'input'; + let selector = `[data-fieldname="${fieldname}"] ${field_element}:visible`; if (fieldtype === 'Text Editor') { selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]:visible`; diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js index 4eeab0274b..5128ae24cb 100644 --- a/frappe/core/doctype/system_settings/system_settings.js +++ b/frappe/core/doctype/system_settings/system_settings.js @@ -10,6 +10,10 @@ frappe.ui.form.on("System Settings", { frm.set_value(key, val); frappe.sys_defaults[key] = val; }); + if (frm.re_setup_moment) { + frappe.app.setup_moment(); + delete frm.re_setup_moment; + } } }); }, @@ -38,5 +42,8 @@ frappe.ui.form.on("System Settings", { // Clear cache after saving to refresh the values of boot. frappe.ui.toolbar.clear_cache(); } - } + }, + first_day_of_the_week(frm) { + frm.re_setup_moment = true; + }, }); diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 3e04643256..61410fb1a8 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -17,10 +17,11 @@ "date_and_number_format", "date_format", "time_format", - "column_break_7", "number_format", + "column_break_7", "float_precision", "currency_precision", + "first_day_of_the_week", "sec_backup_limit", "backup_limit", "encrypt_backup", @@ -477,12 +478,19 @@ "fieldname": "disable_system_update_notification", "fieldtype": "Check", "label": "Disable System Update Notification" + }, + { + "default": "Sunday", + "fieldname": "first_day_of_the_week", + "fieldtype": "Select", + "label": "First Day of the Week", + "options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2021-11-29 18:09:53.601629", + "modified": "2022-01-04 11:28:34.881192", "modified_by": "Administrator", "module": "Core", "name": "System Settings", @@ -499,5 +507,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "ASC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/database/schema.py b/frappe/database/schema.py index ce9fcb4147..10582eff8f 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -206,6 +206,12 @@ class DbColumn: if not current_def: self.fieldname = validate_column_name(self.fieldname) self.table.add_column.append(self) + + if column_type not in ('text', 'longtext'): + if self.unique: + self.table.add_unique.append(self) + if self.set_index: + self.table.add_index.append(self) return # type diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index 5562f2fc92..5c986b5b7c 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -8,6 +8,7 @@ from frappe.desk.doctype.dashboard_chart.dashboard_chart import get from datetime import datetime from dateutil.relativedelta import relativedelta +from unittest.mock import patch class TestDashboardChart(unittest.TestCase): def test_period_ending(self): @@ -15,8 +16,9 @@ class TestDashboardChart(unittest.TestCase): getdate('2019-04-10')) # week starts on monday - self.assertEqual(get_period_ending('2019-04-10', 'Weekly'), - getdate('2019-04-14')) + with patch.object(frappe.utils.data, "get_first_day_of_the_week", return_value="Monday"): + self.assertEqual(get_period_ending('2019-04-10', 'Weekly'), + getdate('2019-04-14')) self.assertEqual(get_period_ending('2019-04-10', 'Monthly'), getdate('2019-04-30')) @@ -200,13 +202,14 @@ class TestDashboardChart(unittest.TestCase): timeseries = 1 )).insert() - result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1) + with patch.object(frappe.utils.data, "get_first_day_of_the_week", return_value="Monday"): + result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1) - self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0]) - self.assertEqual( - result.get('labels'), - ['30-12-18', '06-01-19', '13-01-19', '20-01-19'] - ) + self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0]) + self.assertEqual( + result.get('labels'), + ['30-12-18', '06-01-19', '13-01-19', '20-01-19'] + ) frappe.db.rollback() @@ -231,13 +234,13 @@ class TestDashboardChart(unittest.TestCase): timeseries = 1 )).insert() - result = get(chart_name='Test Average Dashboard Chart', refresh = 1) - - self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 150.0, 266.6666666666667, 0.0]) - self.assertEqual( - result.get('labels'), - ['30-12-18', '06-01-19', '13-01-19', '20-01-19'] - ) + with patch.object(frappe.utils.data, "get_first_day_of_the_week", return_value="Monday"): + result = get(chart_name='Test Average Dashboard Chart', refresh = 1) + self.assertEqual( + result.get('labels'), + ['30-12-18', '06-01-19', '13-01-19', '20-01-19'] + ) + self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 150.0, 266.6666666666667, 0.0]) frappe.db.rollback() diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js index 0fe3932671..fc83069fd2 100644 --- a/frappe/desk/doctype/system_console/system_console.js +++ b/frappe/desk/doctype/system_console/system_console.js @@ -100,5 +100,5 @@ frappe.ui.form.on('System Console', { ${rows}`); }); - } + }, }); diff --git a/frappe/frappeclient.py b/frappe/frappeclient.py index ab58979203..59db38584c 100644 --- a/frappe/frappeclient.py +++ b/frappe/frappeclient.py @@ -1,11 +1,13 @@ -import requests -import json -import frappe -import base64 - ''' FrappeClient is a library that helps you connect with other frappe systems ''' +import base64 +import json + +import requests + +import frappe + class AuthError(Exception): pass @@ -46,7 +48,7 @@ class FrappeClient(object): def _login(self, username, password): '''Login/start a sesion. Called internally on init''' - r = self.session.post(self.url, data={ + r = self.session.post(self.url, params={ 'cmd': 'login', 'usr': username, 'pwd': password @@ -289,14 +291,14 @@ class FrappeClient(object): def get_api(self, method, params=None): if params is None: params = {} - res = self.session.get(self.url + "/api/method/" + method + "/", + res = self.session.get(f"{self.url}/api/method/{method}", params=params, verify=self.verify, headers=self.headers) return self.post_process(res) def post_api(self, method, params=None): if params is None: params = {} - res = self.session.post(self.url + "/api/method/" + method + "/", + res = self.session.post(f"{self.url}/api/method/{method}", params=params, verify=self.verify, headers=self.headers) return self.post_process(res) diff --git a/frappe/patches.txt b/frappe/patches.txt index 39d60d9496..af7e4d6e3f 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -182,6 +182,7 @@ frappe.patches.v13_0.queryreport_columns execute:frappe.reload_doc('core', 'doctype', 'doctype') frappe.patches.v13_0.jinja_hook frappe.patches.v13_0.update_notification_channel_if_empty +frappe.patches.v13_0.set_first_day_of_the_week frappe.patches.v14_0.drop_data_import_legacy frappe.patches.v14_0.rename_cancelled_documents frappe.patches.v14_0.copy_mail_data #08.03.21 diff --git a/frappe/patches/v13_0/set_first_day_of_the_week.py b/frappe/patches/v13_0/set_first_day_of_the_week.py new file mode 100644 index 0000000000..cfb694bbf1 --- /dev/null +++ b/frappe/patches/v13_0/set_first_day_of_the_week.py @@ -0,0 +1,7 @@ +import frappe + +def execute(): + frappe.reload_doctype("System Settings") + # setting first_day_of_the_week value as "Monday" to avoid breaking change + # because before the configuration was introduced, system used to consider "Monday" as start of the week + frappe.db.set_value("System Settings", "System Settings", "first_day_of_the_week", "Monday") \ No newline at end of file diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 64767e1232..202cee645a 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -275,11 +275,7 @@ frappe.Application = class Application { this.set_globals(); this.sync_pages(); frappe.router.setup(); - moment.locale("en"); - moment.user_utc_offset = moment().utcOffset(); - if(frappe.boot.timezone_info) { - moment.tz.add(frappe.boot.timezone_info); - } + this.setup_moment(); if(frappe.boot.print_css) { frappe.dom.set_style(frappe.boot.print_css, "print-style"); } @@ -628,6 +624,19 @@ frappe.Application = class Application { } }); } + + setup_moment() { + moment.updateLocale('en', { + week: { + dow: frappe.datetime.get_first_day_of_the_week_index(), + } + }); + moment.locale("en"); + moment.user_utc_offset = moment().utcOffset(); + if (frappe.boot.timezone_info) { + moment.tz.add(frappe.boot.timezone_info); + } + } } frappe.get_module = function(m, default_module) { diff --git a/frappe/public/js/frappe/form/controls/date.js b/frappe/public/js/frappe/form/controls/date.js index 28e7f2a478..78eb3832cc 100644 --- a/frappe/public/js/frappe/form/controls/date.js +++ b/frappe/public/js/frappe/form/controls/date.js @@ -62,6 +62,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat dateFormat: date_format, startDate: this.get_start_date(), keyboardNav: false, + firstDay: frappe.datetime.get_first_day_of_the_week_index(), onSelect: () => { this.$input.trigger('change'); }, diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 96e502663d..a40f428969 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -196,7 +196,7 @@ export default class GridRow { // REDESIGN-TODO: Make translation contextual, this No is Number var txt = (this.doc ? this.doc.idx : __("No.")); this.row_index = $( - `
+ `
${this.row_check_html}
`) .appendTo(this.row) diff --git a/frappe/public/js/frappe/utils/datetime.js b/frappe/public/js/frappe/utils/datetime.js index 7bb6076b72..196bdf68a3 100644 --- a/frappe/public/js/frappe/utils/datetime.js +++ b/frappe/public/js/frappe/utils/datetime.js @@ -254,6 +254,11 @@ $.extend(frappe.datetime, { ], true).isValid(); }, + get_first_day_of_the_week_index() { + const first_day_of_the_week = frappe.sys_defaults.first_day_of_the_week || "Sunday"; + return moment.weekdays().indexOf(first_day_of_the_week); + } + }); // Proxy for dateutil and get_today diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 599a638ce2..5c1541e0de 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -15,6 +15,8 @@ import io from mimetypes import guess_type from datetime import datetime, timedelta, date +from unittest.mock import patch + class TestFilters(unittest.TestCase): def test_simple_dict(self): self.assertTrue(evaluate_filters({'doctype': 'User', 'status': 'Open'}, {'status': 'Open'})) @@ -306,3 +308,24 @@ class TestDiffUtils(unittest.TestCase): diff = get_version_diff(old_version, latest_version) self.assertIn('-2;', diff) self.assertIn('+42;', diff) + +class TestDateUtils(unittest.TestCase): + def test_first_day_of_week(self): + # Monday as start of the week + with patch.object(frappe.utils.data, "get_first_day_of_the_week", return_value="Monday"): + self.assertEqual(frappe.utils.get_first_day_of_week("2020-12-25"), + frappe.utils.getdate("2020-12-21")) + self.assertEqual(frappe.utils.get_first_day_of_week("2020-12-20"), + frappe.utils.getdate("2020-12-14")) + + # Sunday as start of the week + self.assertEqual(frappe.utils.get_first_day_of_week("2020-12-25"), + frappe.utils.getdate("2020-12-20")) + self.assertEqual(frappe.utils.get_first_day_of_week("2020-12-21"), + frappe.utils.getdate("2020-12-20")) + + def test_last_day_of_week(self): + self.assertEqual(frappe.utils.get_last_day_of_week("2020-12-24"), + frappe.utils.getdate("2020-12-26")) + self.assertEqual(frappe.utils.get_last_day_of_week("2020-12-28"), + frappe.utils.getdate("2021-01-02")) \ No newline at end of file diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 206f0eac64..545d49054a 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -11,11 +11,26 @@ from code import compile_command from urllib.parse import quote, urljoin from frappe.desk.utils import slug from click import secho +from enum import Enum DATE_FORMAT = "%Y-%m-%d" TIME_FORMAT = "%H:%M:%S.%f" DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT +class Weekday(Enum): + Sunday = 0 + Monday = 1 + Tuesday = 2 + Wednesday = 3 + Thursday = 4 + Friday = 5 + Saturday = 6 + +def get_first_day_of_the_week(): + return frappe.get_system_settings('first_day_of_the_week') or "Sunday" + +def get_start_of_week_index(): + return Weekday[get_first_day_of_the_week()].value def is_invalid_date_string(date_string): # dateutil parser does not agree with dates like "0001-01-01" or "0000-00-00" @@ -246,9 +261,22 @@ def get_quarter_start(dt, as_str=False): def get_first_day_of_week(dt, as_str=False): dt = getdate(dt) - date = dt - datetime.timedelta(days=dt.weekday()) + date = dt - datetime.timedelta(days=get_week_start_offset_days(dt)) return date.strftime(DATE_FORMAT) if as_str else date +def get_week_start_offset_days(dt): + current_day_index = get_normalized_weekday_index(dt) + start_of_week_index = get_start_of_week_index() + + if current_day_index >= start_of_week_index: + return current_day_index - start_of_week_index + else: + return 7 - (start_of_week_index - current_day_index) + +def get_normalized_weekday_index(dt): + # starts Sunday with 0 + return (dt.weekday() + 1) % 7 + def get_year_start(dt, as_str=False): dt = getdate(dt) date = datetime.date(dt.year, 1, 1) diff --git a/frappe/website/doctype/web_form/templates/web_form.html b/frappe/website/doctype/web_form/templates/web_form.html index f511b3c27d..743c094314 100644 --- a/frappe/website/doctype/web_form/templates/web_form.html +++ b/frappe/website/doctype/web_form/templates/web_form.html @@ -78,6 +78,10 @@ frappe.boot = { sysdefaults: { float_precision: parseInt("{{ frappe.get_system_settings('float_precision') or 3 }}"), date_format: "{{ frappe.get_system_settings('date_format') or 'yyyy-mm-dd' }}", + }, + time_zone: { + system: "{{ frappe.utils.get_time_zone() }}", + user: "{{ frappe.db.get_value('User', frappe.session.user, 'time_zone') or frappe.utils.get_time_zone() }}" } }; // for backward compatibility of some libs diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index eb7ee90826..cef1a8d8a3 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -216,8 +216,8 @@ def get_context(context): "amount": amount, "title": title, "description": title, - "reference_doctype": "Web Form", - "reference_docname": self.name, + "reference_doctype": doc.doctype, + "reference_docname": doc.name, "payer_email": frappe.session.user, "payer_name": frappe.utils.get_fullname(frappe.session.user), "order_id": doc.name,