Bläddra i källkod

feat: Optionally remove seconds from datetime (#8531)

* fix: Add updated datepicker; fixed seconds formatting bug.
Seconds between 0 and 9 were not zero-padded.

* feat: Add framework for time format

* feat: datetime server-side formatters.

* tests: Added server-side datetime formatter tests

* feat: Update client-side datetime formatters

* tests: Add Cypress client-side formatting tests.

* fix: JSON errors

* fix: Update to not hard-code admin password

* fix: Change to using bulk_update rather than the REST API

* tests: Use Custom doctype for testing, not Standard

* fix: Codacy style fixes

* fix: Commonify update_datetime_picker in date.js, datetime.js, time.js
Fix order of time_format in System Settings
Restore get_user_fmt in utils/datetime.js

* feat: Drastically reduce scale of Cypress testing (to make tests faster)
Full testing is possible by setting 'fast_mode' to false in the spec file.

* fix: Fix issues with datepicker/timepicker expansion

* fix: typo

* style: Various style fixes as requested by DeppSource: Python

* fix: Timepicker not hiding on 'now' button. Force hiding on click.

* style: Codacy style fixes.

* fix: Use datepicker from node_modules

* test: Refactor Datetime UI tests

- cy.get_field
- cy.set_value
- cy.insert_doc with ignore_duplicate
- Nominal datetime tests to cover most formats
- Formatting with prettier

* test: Datetime UI tests; wait for cur_frm.doc.datetime to update

* tests: Add whitespace to typed input

- Clear input only for Time field

* test: Wait timeout 200

* test: Fix form test

Co-authored-by: Faris Ansari <netchampfaris@users.noreply.github.com>
version-14
Andrew McLeod 5 år sedan
committed by Faris Ansari
förälder
incheckning
07cedc581d
26 ändrade filer med 797 tillägg och 491 borttagningar
  1. +48
    -0
      cypress/fixtures/datetime_doctype.js
  2. +4
    -4
      cypress/integration/api.js
  3. +128
    -0
      cypress/integration/datetime.js
  4. +2
    -0
      cypress/integration/form.js
  5. +147
    -85
      cypress/support/commands.js
  6. +10
    -1
      frappe/core/doctype/system_settings/system_settings.json
  7. +1
    -0
      frappe/desk/page/setup_wizard/setup_wizard.py
  8. +3
    -1
      frappe/geo/country_info.py
  9. +7
    -1
      frappe/geo/doctype/country/country.json
  10. +17
    -5
      frappe/public/build.json
  11. +27
    -14
      frappe/public/js/frappe/form/controls/date.js
  12. +14
    -1
      frappe/public/js/frappe/form/controls/datetime.js
  13. +69
    -19
      frappe/public/js/frappe/form/controls/time.js
  14. +9
    -1
      frappe/public/js/frappe/form/formatters.js
  15. +19
    -9
      frappe/public/js/frappe/utils/datetime.js
  16. +0
    -12
      frappe/public/js/lib/datepicker/datepicker.en.js
  17. +0
    -1
      frappe/public/js/lib/datepicker/datepicker.min.css
  18. +0
    -2
      frappe/public/js/lib/datepicker/datepicker.min.js
  19. +0
    -263
      frappe/public/js/lib/datepicker/locale-all.js
  20. +1
    -1
      frappe/public/less/datepicker.less
  21. +163
    -0
      frappe/tests/test_fmt_datetime.py
  22. +62
    -22
      frappe/utils/data.py
  23. +1
    -0
      frappe/utils/install.py
  24. +50
    -46
      frappe/utils/safe_exec.py
  25. +1
    -0
      package.json
  26. +14
    -3
      yarn.lock

+ 48
- 0
cypress/fixtures/datetime_doctype.js Visa fil

@@ -0,0 +1,48 @@
export default {
name: 'DateTime Test',
custom: 1,
actions: [],
creation: '2019-03-15 06:29:07.215072',
doctype: 'DocType',
editable_grid: 1,
engine: 'InnoDB',
fields: [
{
fieldname: 'date',
fieldtype: 'Date',
label: 'Date'
},
{
fieldname: 'time',
fieldtype: 'Time',
label: 'Time'
},
{
fieldname: 'datetime',
fieldtype: 'Datetime',
label: 'Datetime'
}
],
issingle: 1,
links: [],
modified: '2019-12-09 14:40:53.127615',
modified_by: 'Administrator',
module: 'Custom',
owner: 'Administrator',
permissions: [
{
create: 1,
delete: 1,
email: 1,
print: 1,
read: 1,
role: 'System Manager',
share: 1,
write: 1
}
],
quick_entry: 1,
sort_field: 'modified',
sort_order: 'ASC',
track_changes: 1
};

+ 4
- 4
cypress/integration/api.js Visa fil

@@ -6,8 +6,8 @@ context('API Resources', () => {
});

it('Creates two Comments', () => {
cy.create_doc('Comment', {comment_type: 'Comment', content: "hello"});
cy.create_doc('Comment', {comment_type: 'Comment', content: "world"});
cy.insert_doc('Comment', {comment_type: 'Comment', content: "hello"});
cy.insert_doc('Comment', {comment_type: 'Comment', content: "world"});
});

it('Lists the Comments', () => {
@@ -25,11 +25,11 @@ context('API Resources', () => {
});

it('Gets each Comment', () => {
cy.get_list('Comment').then(body => body.data.forEach(comment => {
cy.get_list('Comment').then(body => body.data.forEach(comment => {
cy.get_doc('Comment', comment.name);
}));
});
it('Removes the Comments', () => {
cy.get_list('Comment').then(body => body.data.forEach(comment => {
cy.remove_doc('Comment', comment.name);


+ 128
- 0
cypress/integration/datetime.js Visa fil

@@ -0,0 +1,128 @@
import datetime_doctype from '../fixtures/datetime_doctype';
const doctype_name = datetime_doctype.name;

context('Control Date, Time and DateTime', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.insert_doc('DocType', datetime_doctype, true);
});

describe('Date formats', () => {
let date_formats = [
{
date_format: 'dd-mm-yyyy',
part: 2,
length: 4,
separator: '-'
},
{
date_format: 'mm/dd/yyyy',
part: 0,
length: 2,
separator: '/'
}
];

date_formats.forEach(d => {
it('test date format ' + d.date_format, () => {
cy.set_value('System Settings', 'System Settings', {
date_format: d.date_format
});
cy.window()
.its('frappe')
.then(frappe => {
// update sys_defaults value to avoid a reload
frappe.sys_defaults.date_format = d.date_format;
});

cy.new_form(doctype_name);
cy.get('.form-control[data-fieldname=date]').focus();
cy.get('.datepickers-container .datepicker.active')
.should('be.visible');
cy.get(
'.datepickers-container .datepicker.active .datepicker--cell-day.-current-'
).click();

cy.window()
.its('cur_frm')
.then(cur_frm => {
let formatted_value = cur_frm.get_field('date').input.value;
let parts = formatted_value.split(d.separator);
expect(parts[d.part].length).to.equal(d.length);
});
});
});
});

describe('Time formats', () => {
let time_formats = [
{
time_format: 'HH:mm:ss',
value: ' 11:00:12',
match_value: '11:00:12'
},
{
time_format: 'HH:mm',
value: ' 11:00:12',
match_value: '11:00'
}
];

time_formats.forEach(d => {
it('test time format ' + d.time_format, () => {
cy.set_value('System Settings', 'System Settings', {
time_format: d.time_format
});
cy.window()
.its('frappe')
.then(frappe => {
frappe.sys_defaults.time_format = d.time_format;
});
cy.new_form(doctype_name);
cy.fill_field('time', d.value, 'Time').blur();
cy.get_field('time').should('have.value', d.match_value);
});
});
});

describe('DateTime formats', () => {
let datetime_formats = [
{
date_format: 'dd.mm.yyyy',
time_format: 'HH:mm:ss',
value: ' 02.12.2019 11:00:12',
doc_value: '2019-12-02 11:00:12',
input_value: '02.12.2019 11:00:12'
},
{
date_format: 'mm-dd-yyyy',
time_format: 'HH:mm',
value: ' 12-02-2019 11:00:00',
doc_value: '2019-12-02 11:00:00',
input_value: '12-02-2019 11:00'
}
];
datetime_formats.forEach(d => {
it(`test datetime format ${d.date_format} ${d.time_format}`, () => {
cy.set_value('System Settings', 'System Settings', {
date_format: d.date_format,
time_format: d.time_format
});
cy.window()
.its('frappe')
.then(frappe => {
frappe.sys_defaults.date_format = d.date_format;
frappe.sys_defaults.time_format = d.time_format;
});
cy.new_form(doctype_name);
cy.fill_field('datetime', d.value, 'Datetime').blur();
cy.get_field('datetime').should('have.value', d.input_value);

cy.window()
.its('cur_frm.doc.datetime')
.should('eq', d.doc_value);
});
});
});
});

+ 2
- 0
cypress/integration/form.js Visa fil

@@ -16,10 +16,12 @@ context('Form', () => {
cy.get('.primary-action').click();
cy.visit('/desk#List/ToDo');
cy.location('hash').should('eq', '#List/ToDo/List');
cy.get('h1').should('be.visible').and('contain', 'To Do');
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.location('hash').should('eq', '#List/Contact/List');
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 });


+ 147
- 85
cypress/support/commands.js Visa fil

@@ -42,95 +42,156 @@ Cypress.Commands.add('login', (email, password) => {
});

Cypress.Commands.add('call', (method, args) => {
return cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
url: `/api/method/${method}`,
method: 'POST',
body: args,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
}).then(res => {
expect(res.status).eq(200);
return res.body;
return cy
.window()
.its('frappe.csrf_token')
.then(csrf_token => {
return cy
.request({
url: `/api/method/${method}`,
method: 'POST',
body: args,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
})
.then(res => {
expect(res.status).eq(200);
return res.body;
});
});
});
});

Cypress.Commands.add('get_list', (doctype, fields=[], filters=[]) => {
return cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
method: 'GET',
url: `/api/resource/${doctype}?fields=${JSON.stringify(fields)}&filters=${JSON.stringify(filters)}`,
headers: {
'Accept': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
}).then(res => {
expect(res.status).eq(200);
return res.body;
Cypress.Commands.add('get_list', (doctype, fields = [], filters = []) => {
filters = JSON.stringify(filters);
fields = JSON.stringify(fields);
let url = `/api/resource/${doctype}?fields=${fields}&filters=${filters}`;
return cy
.window()
.its('frappe.csrf_token')
.then(csrf_token => {
return cy
.request({
method: 'GET',
url,
headers: {
Accept: 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
})
.then(res => {
expect(res.status).eq(200);
return res.body;
});
});
});
});

Cypress.Commands.add('get_doc', (doctype, name) => {
return cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
method: 'GET',
url: `/api/resource/${doctype}/${name}`,
headers: {
'Accept': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
}).then(res => {
expect(res.status).eq(200);
return res.body;
return cy
.window()
.its('frappe.csrf_token')
.then(csrf_token => {
return cy
.request({
method: 'GET',
url: `/api/resource/${doctype}/${name}`,
headers: {
Accept: 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
})
.then(res => {
expect(res.status).eq(200);
return res.body;
});
});
});
});

Cypress.Commands.add('create_doc', (doctype, args) => {
return cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
method: 'POST',
url: `/api/resource/${doctype}`,
body: args,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
}).then(res => {
expect(res.status).eq(200);
return res.body;
Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
return cy
.window()
.its('frappe.csrf_token')
.then(csrf_token => {
return cy
.request({
method: 'POST',
url: `/api/resource/${doctype}`,
body: args,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
},
failOnStatusCode: !ignore_duplicate
})
.then(res => {
let status_codes = [200];
if (ignore_duplicate) {
status_codes.push(409);
}
expect(res.status).to.be.oneOf(status_codes);
return res.body;
});
});
});
});

Cypress.Commands.add('remove_doc', (doctype, name) => {
return cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
method: 'DELETE',
url: `/api/resource/${doctype}/${name}`,
headers: {
'Accept': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
}).then(res => {
expect(res.status).eq(202);
return res.body;
return cy
.window()
.its('frappe.csrf_token')
.then(csrf_token => {
return cy
.request({
method: 'DELETE',
url: `/api/resource/${doctype}/${name}`,
headers: {
Accept: 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
})
.then(res => {
expect(res.status).eq(202);
return res.body;
});
});
});
});

Cypress.Commands.add('create_records', (doc) => {
return cy.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
Cypress.Commands.add('create_records', doc => {
return cy
.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
.then(r => r.message);
});

Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => {
Cypress.Commands.add('set_value', (doctype, name, obj) => {
return cy.call('frappe.client.set_value', {
doctype,
name,
fieldname: obj
});
});

Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
cy.get_field(fieldname, fieldtype).as('input');

if (['Date', 'Time', 'Datetime'].includes(fieldtype)) {
cy.get('@input').click().wait(200);
cy.get('.datepickers-container .datepicker.active').should('exist');
}
if (fieldtype === 'Time') {
cy.get('@input').clear();
}

if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
cy.get('@input').type(value, { waitForAnimations: false });
}
return cy.get('@input');
});

Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
let selector = `.form-control[data-fieldname="${fieldname}"]`;

if (fieldtype === 'Text Editor') {
@@ -140,34 +201,33 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => {
selector = `[data-fieldname="${fieldname}"] .ace_text-input`;
}

cy.get(selector).as('input');

if (fieldtype === 'Select') {
return cy.get('@input').select(value);
} else {
return cy.get('@input').type(value, {waitForAnimations: false});
}
return cy.get(selector);
});

Cypress.Commands.add('awesomebar', (text) => {
Cypress.Commands.add('awesomebar', text => {
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 });
});

Cypress.Commands.add('new_form', (doctype) => {
cy.visit(`/desk#Form/${doctype}/New ${doctype} 1`);
Cypress.Commands.add('new_form', doctype => {
let route = `Form/${doctype}/New ${doctype} 1`;
cy.visit(`/desk#${route}`);
cy.get('body').should('have.attr', 'data-route', route);
cy.get('body').should('have.attr', 'data-ajax-state', 'complete');
});

Cypress.Commands.add('go_to_list', (doctype) => {
Cypress.Commands.add('go_to_list', doctype => {
cy.visit(`/desk#List/${doctype}/List`);
});

Cypress.Commands.add('clear_cache', () => {
cy.window().its('frappe').then(frappe => {
frappe.ui.toolbar.clear_cache();
});
cy.window()
.its('frappe')
.then(frappe => {
frappe.ui.toolbar.clear_cache();
});
});

Cypress.Commands.add('dialog', (opts) => {
Cypress.Commands.add('dialog', opts => {
return cy.window().then(win => {
var d = new win.frappe.ui.Dialog(opts);
d.show();
@@ -180,7 +240,9 @@ Cypress.Commands.add('get_open_dialog', () => {
});

Cypress.Commands.add('hide_dialog', () => {
cy.get_open_dialog().find('.btn-modal-close').click();
cy.get_open_dialog()
.find('.btn-modal-close')
.click();
cy.get('.modal:visible').should('not.exist');
});



+ 10
- 1
frappe/core/doctype/system_settings/system_settings.json Visa fil

@@ -14,6 +14,7 @@
"setup_complete",
"date_and_number_format",
"date_format",
"time_format",
"column_break_7",
"number_format",
"float_precision",
@@ -118,6 +119,14 @@
"options": "yyyy-mm-dd\ndd-mm-yyyy\ndd/mm/yyyy\ndd.mm.yyyy\nmm/dd/yyyy\nmm-dd-yyyy",
"reqd": 1
},
{
"default": "HH:mm:ss",
"fieldname": "time_format",
"fieldtype": "Select",
"label": "Time Format",
"options": "HH:mm:ss\nHH:mm",
"reqd": 1
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
@@ -420,4 +429,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
}

+ 1
- 0
frappe/desk/page/setup_wizard/setup_wizard.py Visa fil

@@ -141,6 +141,7 @@ def update_system_settings(args):
"time_zone": args.get("timezone"),
"float_precision": 3,
'date_format': frappe.db.get_value("Country", args.get("country"), "date_format"),
'time_format': frappe.db.get_value("Country", args.get("country"), "time_format"),
'number_format': number_format,
'enable_scheduler': 1 if not frappe.flags.in_test else 0,
'backup_limit': 3 # Default for downloadable backups


+ 3
- 1
frappe/geo/country_info.py Visa fil

@@ -10,8 +10,10 @@ from frappe.utils.momentjs import get_all_timezones
def get_country_info(country=None):
data = get_all()
data = frappe._dict(data.get(country, {}))
if not 'date_format' in data:
if 'date_format' not in data:
data.date_format = "dd-mm-yyyy"
if 'time_format' not in data:
data.time_format = "HH:mm:ss"

return data



+ 7
- 1
frappe/geo/doctype/country/country.json Visa fil

@@ -76,6 +76,12 @@
"translatable": 0,
"unique": 0
},
{
"fieldname": "time_format",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Time format"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
@@ -205,4 +211,4 @@
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}
}

+ 17
- 5
frappe/public/build.json Visa fil

@@ -52,6 +52,22 @@
"website/js/bootstrap-4.js"
],
"js/control.min.js": [
"node_modules/air-datepicker/dist/js/datepicker.min.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.cs.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.da.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.de.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.en.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.es.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.fi.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.fr.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.hu.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.nl.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.pl.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.pt-BR.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.pt.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.ro.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.sk.js",
"node_modules/air-datepicker/dist/js/i18n/datepicker.zh.js",
"public/js/frappe/ui/capture.js",
"public/js/frappe/form/controls/control.js"
],
@@ -114,8 +130,6 @@
"public/js/lib/socket.io.min.js",
"public/js/lib/jSignature.min.js",
"public/js/frappe/translate.js",
"public/js/lib/datepicker/datepicker.min.js",
"public/js/lib/datepicker/locale-all.js",
"public/js/lib/leaflet/leaflet.js",
"public/js/lib/leaflet/leaflet.draw.js",
"public/js/lib/leaflet/L.Control.Locate.js",
@@ -314,9 +328,7 @@
],
"js/web_form.min.js": [
"public/js/frappe/utils/datetime.js",
"public/js/frappe/web_form/webform_script.js",
"public/js/lib/datepicker/datepicker.min.js",
"public/js/lib/datepicker/datepicker.en.js"
"public/js/frappe/web_form/webform_script.js"
],
"css/web_form.css": [
"public/less/list.less",


+ 27
- 14
frappe/public/js/frappe/form/controls/date.js Visa fil

@@ -1,14 +1,17 @@


frappe.ui.form.ControlDate = frappe.ui.form.ControlData.extend({
make_input: function() {
this._super();
this.make_picker();
},
make_picker: function() {
this.set_date_options();
this.set_datepicker();
this.set_t_for_today();
},
set_formatted_input: function(value) {
this._super(value);
if (this.timepicker_only) return;
if (!this.datepicker) return;
if(!value) {
this.datepicker.clear();
@@ -71,19 +74,6 @@ frappe.ui.form.ControlDate = frappe.ui.form.ControlData.extend({
}
};
},
update_datepicker_position: function() {
if(!this.frm) return;
// show datepicker above or below the input
// based on scroll position
var window_height = $(window).height();
var window_scroll_top = $(window).scrollTop();
var el_offset_top = this.$input.offset().top + 280;
var position = 'top left';
if(window_height + window_scroll_top >= el_offset_top) {
position = 'bottom left';
}
this.datepicker.update('position', position);
},
set_datepicker: function() {
this.$input.datepicker(this.datepicker_options);
this.datepicker = this.$input.data('datepicker');
@@ -96,6 +86,29 @@ frappe.ui.form.ControlDate = frappe.ui.form.ControlData.extend({
this.datepicker.selectDate(this.get_now_date());
});
},
update_datepicker_position: function() {
if(!this.frm) return;
// show datepicker above or below the input
// based on scroll position
// We have to bodge around the timepicker getting its position
// wrong by 42px when opening upwards.
const $header = $('.page-head');
const header_bottom = $header.position().top + $header.outerHeight();
const picker_height = this.datepicker.$datepicker.outerHeight() + 12;
const picker_top = this.$input.offset().top - $(window).scrollTop() - picker_height;

var position = 'top left';
// 12 is the default datepicker.opts[offset]
if (picker_top <= header_bottom) {
position = 'bottom left';
if (this.timepicker_only) this.datepicker.opts['offset'] = 12;
} else {
// To account for 42px incorrect positioning
if (this.timepicker_only) this.datepicker.opts['offset'] = -30;
}

this.datepicker.update('position', position);
},
get_now_date: function() {
return frappe.datetime.now_date(true);
},


+ 14
- 1
frappe/public/js/frappe/form/controls/datetime.js Visa fil

@@ -2,10 +2,13 @@ frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({
set_date_options: function() {
this._super();
this.today_text = __("Now");
let sysdefaults = frappe.boot.sysdefaults;
this.date_format = frappe.defaultDatetimeFormat;
let time_format = sysdefaults && sysdefaults.time_format
? sysdefaults.time_format : 'HH:mm:ss';
$.extend(this.datepicker_options, {
timepicker: true,
timeFormat: "hh:ii:ss"
timeFormat: time_format.toLowerCase().replace("mm", "ii")
});
},
get_now_date: function() {
@@ -22,5 +25,15 @@ frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({
}
}
this._super();
},
set_datepicker: function() {
this._super();
if (this.datepicker.opts.timeFormat.indexOf('s') == -1) {
// No seconds in time format
const $tp = this.datepicker.timepicker;
$tp.$seconds.parent().css('display', 'none');
$tp.$secondsText.css('display', 'none');
$tp.$secondsText.prev().css('display', 'none');
}
}
});

+ 69
- 19
frappe/public/js/frappe/form/controls/time.js Visa fil

@@ -1,43 +1,72 @@
frappe.ui.form.ControlTime = frappe.ui.form.ControlData.extend({
frappe.ui.form.ControlTime = frappe.ui.form.ControlDate.extend({
set_formatted_input: function(value) {
this._super(value);
},
make_input: function() {
var me = this;
this.timepicker_only = true;
this._super();
this.$input.datepicker({
},
make_picker: function() {
this.set_time_options();
this.set_datepicker();
this.refresh();
},
set_time_options: function() {
let sysdefaults = frappe.boot.sysdefaults;

let time_format = sysdefaults && sysdefaults.time_format
? sysdefaults.time_format : 'HH:mm:ss';

this.time_format = frappe.defaultTimeFormat;
this.datepicker_options = {
language: "en",
timepicker: true,
onlyTimepicker: true,
timeFormat: "hh:ii:ss",
timeFormat: time_format.toLowerCase().replace("mm", "ii"),
startDate: frappe.datetime.now_time(true),
onSelect: function() {
onSelect: () => {
// ignore micro seconds
if (moment(me.get_value(), 'hh:mm:ss').format('HH:mm:ss') != moment(me.value, 'hh:mm:ss').format('HH:mm:ss')) {
me.$input.trigger('change');
}
if (moment(this.get_value(), time_format).format('HH:mm:ss') != moment(this.value, time_format).format('HH:mm:ss')) {
this.$input.trigger('change');
}
},
onShow: function() {
onShow: () => {
$('.datepicker--button:visible').text(__('Now'));

this.update_datepicker_position();
},
keyboardNav: false,
todayButton: true
});
this.datepicker = this.$input.data('datepicker');
this.datepicker.$datepicker
.find('[data-action="today"]')
.click(() => {
this.datepicker.selectDate(frappe.datetime.now_time(true));
});
this.refresh();
};
},
set_input: function(value) {
this._super(value);
if(value
if (value
&& ((this.last_value && this.last_value !== this.value)
|| (!this.datepicker.selectedDates.length))) {

var date_obj = frappe.datetime.moment_to_date_obj(moment(value, 'HH:mm:ss'));
var date_obj = frappe.datetime.moment_to_date_obj(moment(value, frappe.sys_defaults['time_format']));
this.datepicker.selectDate(date_obj);
}
},
set_datepicker: function() {
this.$input.datepicker(this.datepicker_options);
this.datepicker = this.$input.data('datepicker');

this.datepicker.$datepicker
.find('[data-action="today"]')
.click(() => {
this.datepicker.selectDate(frappe.datetime.now_time(true));
this.datepicker.hide();
});
if (this.datepicker.opts.timeFormat.indexOf('s') == -1) {
// No seconds in time format
const $tp = this.datepicker.timepicker;
$tp.$seconds.parent().css('display', 'none');
$tp.$secondsText.css('display', 'none');
$tp.$secondsText.prev().css('display', 'none');
}
},
set_description: function() {
const { description } = this.df;
const { time_zone } = frappe.sys_defaults;
@@ -49,5 +78,26 @@ frappe.ui.form.ControlTime = frappe.ui.form.ControlData.extend({
}
}
this._super();
},
parse: function(value) {
if (value) {
return frappe.datetime.user_to_str(value, true);
}
},
format_for_input: function(value) {
if (value) {
return frappe.datetime.str_to_user(value, true);
}
return "";
},
validate: function(value) {
if (value && !frappe.datetime.validate(value)) {
let sysdefaults = frappe.sys_defaults;
let time_format = sysdefaults && sysdefaults.time_format
? sysdefaults.time_format : 'HH:mm:ss';
frappe.msgprint(__("Time {0} must be in format: {1}", [value, time_format]));
return '';
}
return value;
}
});

+ 9
- 1
frappe/public/js/frappe/form/formatters.js Visa fil

@@ -156,7 +156,8 @@ frappe.form.formatters = {
if(frappe.boot.sysdefaults.time_zone) {
m = m.tz(frappe.boot.sysdefaults.time_zone);
}
return m.format(frappe.boot.sysdefaults.date_format.toUpperCase() + ', h:mm a z');
return m.format(frappe.boot.sysdefaults.date_format.toUpperCase()
+ ' ' + frappe.boot.sysdefaults.time_format);
} else {
return "";
}
@@ -180,6 +181,13 @@ frappe.form.formatters = {

return frappe.form.formatters.Data(value);
},
Time: function(value) {
if (value) {
value = frappe.datetime.str_to_user(value, true);
}

return value || "";
},
LikedBy: function(value) {
var html = "";
$.each(JSON.parse(value || "[]"), function(i, v) {


+ 19
- 9
frappe/public/js/frappe/utils/datetime.js Visa fil

@@ -51,7 +51,7 @@ $.extend(frappe.datetime, {
},

obj_to_user: function(d) {
return moment(d).format(frappe.datetime.get_user_fmt().toUpperCase());
return moment(d).format(frappe.datetime.get_user_date_fmt().toUpperCase());
},

get_diff: function(d1, d2) {
@@ -106,23 +106,32 @@ $.extend(frappe.datetime, {
return moment().endOf("year").format();
},

get_user_fmt: function() {
get_user_time_fmt: function() {
return frappe.sys_defaults && frappe.sys_defaults.time_format || "HH:mm:ss";
},

get_user_date_fmt: function() {
return frappe.sys_defaults && frappe.sys_defaults.date_format || "yyyy-mm-dd";
},

get_user_fmt: function() { // For backwards compatibility only
return frappe.sys_defaults && frappe.sys_defaults.date_format || "yyyy-mm-dd";
},

str_to_user: function(val, only_time = false) {
if(!val) return "";

var user_time_fmt = frappe.datetime.get_user_time_fmt();
if(only_time) {
return moment(val, frappe.defaultTimeFormat)
.format(frappe.defaultTimeFormat);
.format(user_time_fmt);
}

var user_fmt = frappe.datetime.get_user_fmt().toUpperCase();
var user_date_fmt = frappe.datetime.get_user_date_fmt().toUpperCase();
if(typeof val !== "string" || val.indexOf(" ")===-1) {
return moment(val).format(user_fmt);
return moment(val).format(user_date_fmt);
} else {
return moment(val, "YYYY-MM-DD HH:mm:ss").format(user_fmt + " HH:mm:ss");
return moment(val, "YYYY-MM-DD HH:mm:ss").format(user_date_fmt + " " + user_time_fmt);
}
},

@@ -132,16 +141,17 @@ $.extend(frappe.datetime, {

user_to_str: function(val, only_time = false) {

var user_time_fmt = frappe.datetime.get_user_time_fmt();
if(only_time) {
return moment(val, frappe.defaultTimeFormat)
return moment(val, user_time_fmt)
.format(frappe.defaultTimeFormat);
}

var user_fmt = frappe.datetime.get_user_fmt().toUpperCase();
var user_fmt = frappe.datetime.get_user_date_fmt().toUpperCase();
var system_fmt = "YYYY-MM-DD";

if(val.indexOf(" ")!==-1) {
user_fmt += " HH:mm:ss";
user_fmt += " " + user_time_fmt;
system_fmt += " HH:mm:ss";
}



+ 0
- 12
frappe/public/js/lib/datepicker/datepicker.en.js Visa fil

@@ -1,12 +0,0 @@
;(function ($) { $.fn.datepicker.language['en'] = {
days: [__('Sunday'), __('Monday'), __('Tuesday'), __('Wednesday'), __('Thursday'), __('Friday'), __('Saturday')],
daysShort: [__('Sun'), __('Mon'), __('Tue'), __('Wed'), __('Thu'), __('Fri'), __('Sat')],
daysMin: [__('Su'), __('Mo'), __('Tu'), __('We'), __('Th'), __('Fr'), __('Sa')],
months: [__('January'),__('February'),__('March'),__('April'),__('May'),__('June'), __('July'),__('August'),__('September'),__('October'),__('November'),__('December')],
monthsShort: [__('Jan'), __('Feb'), __('Mar'), __('Apr'), __('May'), __('Jun'), __('Jul'), __('Aug'), __('Sep'), __('Oct'), __('Nov'), __('Dec')],
today: __('Today'),
clear: __('Clear'),
dateFormat: 'mm/dd/yyyy',
timeFormat: 'hh:ii aa',
firstDay: 0
}; })(jQuery);

+ 0
- 1
frappe/public/js/lib/datepicker/datepicker.min.css
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 0
- 2
frappe/public/js/lib/datepicker/datepicker.min.js
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 0
- 263
frappe/public/js/lib/datepicker/locale-all.js Visa fil

@@ -1,263 +0,0 @@
;(function ($) { $.fn.datepicker.language['ar'] = {
days: ['الأحد', 'الأثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعه', 'السبت'],
daysShort: ['الأحد', 'الأثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعه', 'السبت'],
daysMin: ['الأحد', 'الأثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعه', 'السبت'],
months: ['يناير','فبراير','مارس','أبريل','مايو','يونيو', 'يوليو','أغسطس','سبتمبر','اكتوبر','نوفمبر','ديسمبر'],
monthsShort: ['يناير','فبراير','مارس','أبريل','مايو','يونيو', 'يوليو','أغسطس','سبتمبر','اكتوبر','نوفمبر','ديسمبر'],
today: 'اليوم',
clear: 'Clear',
dateFormat: 'dd/mm/yyyy',
timeFormat: 'hh:ii aa',
firstDay: 0
}; })(jQuery);

;(function ($) { $.fn.datepicker.language['cs'] = {
days: ['Neděle', 'Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek', 'Sobota'],
daysShort: ['Ne', 'Po', 'Út', 'St', 'Čt', 'Pá', 'So'],
daysMin: ['Ne', 'Po', 'Út', 'St', 'Čt', 'Pá', 'So'],
months: ['Leden', 'Únor', 'Březen', 'Duben', 'Květen', 'Červen', 'Červenec', 'Srpen', 'Září', 'Říjen', 'Listopad', 'Prosinec'],
monthsShort: ['Led', 'Úno', 'Bře', 'Dub', 'Kvě', 'Čvn', 'Čvc', 'Srp', 'Zář', 'Říj', 'Lis', 'Pro'],
today: 'Dnes',
clear: 'Vymazat',
dateFormat: 'dd.mm.yyyy',
timeFormat: 'hh:ii',
firstDay: 1
}; })(jQuery);

;(function ($) { $.fn.datepicker.language['da'] = {
days: ['Søndag', 'Mandag', 'Tirsdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lørdag'],
daysShort: ['Søn', 'Man', 'Tir', 'Ons', 'Tor', 'Fre', 'Lør'],
daysMin: ['Sø', 'Ma', 'Ti', 'On', 'To', 'Fr', 'Lø'],
months: ['Januar','Februar','Marts','April','Maj','Juni', 'Juli','August','September','Oktober','November','December'],
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'],
today: 'I dag',
clear: 'Nulstil',
dateFormat: 'dd/mm/yyyy',
timeFormat: 'hh:ii',
firstDay: 1
}; })(jQuery)

;(function ($) { $.fn.datepicker.language['de'] = {
days: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
daysShort: ['Son', 'Mon', 'Die', 'Mit', 'Don', 'Fre', 'Sam'],
daysMin: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
months: ['Januar','Februar','März','April','Mai','Juni', 'Juli','August','September','Oktober','November','Dezember'],
monthsShort: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
today: 'Heute',
clear: 'Aufräumen',
dateFormat: 'dd.mm.yyyy',
timeFormat: 'hh:ii',
firstDay: 1
}; })(jQuery);

;(function ($) { $.fn.datepicker.language['en'] = {
days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
daysMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
months: ['January','February','March','April','May','June', 'July','August','September','October','November','December'],
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
today: 'Today',
clear: 'Clear',
dateFormat: 'dd/mm/yyyy',
timeFormat: 'hh:ii aa',
firstDay: 0
}; })(jQuery);

;(function ($) { $.fn.datepicker.language['en-GB'] = {
days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
daysMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
months: ['January','February','March','April','May','June', 'July','August','September','October','November','December'],
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
today: 'Today',
clear: 'Clear',
dateFormat: 'dd/mm/yyyy',
timeFormat: 'hh:ii aa',
firstDay: 1
}; })(jQuery);

;(function ($) { $.fn.datepicker.language['en-US'] = {
days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
daysMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
months: ['January','February','March','April','May','June', 'July','August','September','October','November','December'],
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
today: 'Today',
clear: 'Clear',
dateFormat: 'mm/dd/yyyy',
timeFormat: 'hh:ii aa',
firstDay: 0
}; })(jQuery);


;(function ($) { $.fn.datepicker.language['es'] = {
days: ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado'],
daysShort: ['Dom', 'Lun', 'Mar', 'Mie', 'Jue', 'Vie', 'Sab'],
daysMin: ['Do', 'Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sa'],
months: ['Enero','Febrero','Marzo','Abril','Mayo','Junio', 'Julio','Augosto','Septiembre','Octubre','Noviembre','Diciembre'],
monthsShort: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'],
today: 'Hoy',
clear: 'Limpiar',
dateFormat: 'dd/mm/yyyy',
timeFormat: 'hh:ii aa',
firstDay: 1
}; })(jQuery);

;(function ($) { $.fn.datepicker.language['fi'] = {
days: ['Sunnuntai', 'Maanantai', 'Tiistai', 'Keskiviikko', 'Torstai', 'Perjantai', 'Lauantai'],
daysShort: ['Su', 'Ma', 'Ti', 'Ke', 'To', 'Pe', 'La'],
daysMin: ['Su', 'Ma', 'Ti', 'Ke', 'To', 'Pe', 'La'],
months: ['Tammikuu','Helmikuu','Maaliskuu','Huhtikuu','Toukokuu','Kesäkuu', 'Heinäkuu','Elokuu','Syyskuu','Lokakuu','Marraskuu','Joulukuu'],
monthsShort: ['Tammi', 'Helmi', 'Maalis', 'Huhti', 'Touko', 'Kesä', 'Heinä', 'Elo', 'Syys', 'Loka', 'Marras', 'Joulu'],
today: 'Tänään',
clear: 'Tyhjennä',
dateFormat: 'dd.mm.yyyy',
timeFormat: 'hh:ii',
firstDay: 1
};
})(jQuery);

;(function ($) { $.fn.datepicker.language['fr'] = {
days: ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'],
daysShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
daysMin: ['Di', 'Lu', 'Ma', 'Me', 'Je', 'Ve', 'Sa'],
months: ['Janvier','Février','Mars','Avril','Mai','Juin', 'Juillet','Août','Septembre','Octobre','Novembre','Decembre'],
monthsShort: ['Jan', 'Fév', 'Mars', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sep', 'Oct', 'Nov', 'Dec'],
today: "Aujourd'hui",
clear: 'Effacer',
dateFormat: 'dd/mm/yyyy',
timeFormat: 'hh:ii',
firstDay: 1
}; })(jQuery);

;(function ($) { $.fn.datepicker.language['gr'] = {
days: ['Κυριακή', 'Δευτέρα', 'Τρίτη', 'Τετάρτη', 'Πέμπτη', 'Παρασκευή', 'Σάββατο'],
daysShort: ['Κυρ', 'Δευ', 'Τρι', 'Τετ', 'Πεμ', 'Παρ', 'Σαβ'],
daysMin: ['Κυ', 'Δε', 'Τρ', 'Τε', 'Πε', 'Πα', 'Σα'],
months: ['Ιανουάριος', 'Φεβρουάριος', 'Μάρτιος', 'Απρίλιος', 'Μάιος', 'Ιούνιος', 'Ιούλιος', 'Αύγουστος', 'Σεπτέμβριος', 'Οκτώβριος', 'Νοέμβριος', 'Δεκέμβριος'],
monthsShort: ['Ιαν', 'Φεβ', 'Μαρ', 'Απρ', 'Μάι', 'Ι/ν', 'Ι/λ', 'Αυγ', 'Σεπ', 'Οκτ', 'Νοε', 'Δεκ'],
today: 'Σήμερα',
clear: 'Καθαρισμός',
dateFormat: 'dd/mm/yyyy',
timeFormat: 'hh:ii aa',
firstDay: 0
}; })(jQuery);

;(function ($) { ;(function ($) { $.fn.datepicker.language['hu'] = {
days: ['Vasárnap', 'Hétfő', 'Kedd', 'Szerda', 'Csütörtök', 'Péntek', 'Szombat'],
daysShort: ['Va', 'Hé', 'Ke', 'Sze', 'Cs', 'Pé', 'Szo'],
daysMin: ['V', 'H', 'K', 'Sz', 'Cs', 'P', 'Sz'],
months: ['Január', 'Február', 'Március', 'Április', 'Május', 'Június', 'Július', 'Augusztus', 'Szeptember', 'Október', 'November', 'December'],
monthsShort: ['Jan', 'Feb', 'Már', 'Ápr', 'Máj', 'Jún', 'Júl', 'Aug', 'Szep', 'Okt', 'Nov', 'Dec'],
today: 'Ma',
clear: 'Törlés',
dateFormat: 'yyyy-mm-dd',
timeFormat: 'hh:ii aa',
firstDay: 1
}; })(jQuery); })(jQuery);

;(function ($) { $.fn.datepicker.language['it'] = {
days: ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'],
daysShort: ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'],
daysMin: ['Do', 'Lu', 'Ma', 'Me', 'Gi', 'Ve', 'Sa'],
months: ['Gennaio','Febbraio','Marzo','Aprile','Maggio','Giugno','Luglio','Agosto',
'Settembre','Ottobre','Novembre','Dicembre'],
monthsShort: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'],
today: 'Oggi',
clear: 'Reset',
dateFormat: 'dd/mm/yyyy',
timeFormat: 'hh:ii',
firstDay: 1
}; })(jQuery);

;(function ($) { $.fn.datepicker.language['nl'] = {
days: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'],
daysShort: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],
daysMin: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],
months: ['Januari', 'Februari', 'Maart', 'April', 'Mei', 'Juni', 'Juli', 'Augustus', 'September', 'Oktober', 'November', 'December'],
monthsShort: ['Jan', 'Feb', 'Mrt', 'Apr', 'Mei', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'],
today: 'Vandaag',
clear: 'Legen',
dateFormat: 'dd-MM-yy',
timeFormat: 'hh:ii',
firstDay: 0
}; })(jQuery);

;(function ($) { $.fn.datepicker.language['pl'] = {
days: ['Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota'],
daysShort: ['Nie', 'Pon', 'Wto', 'Śro', 'Czw', 'Pią', 'Sob'],
daysMin: ['Nd', 'Pn', 'Wt', 'Śr', 'Czw', 'Pt', 'So'],
months: ['Styczeń','Luty','Marzec','Kwiecień','Maj','Czerwiec', 'Lipiec','Sierpień','Wrzesień','Październik','Listopad','Grudzień'],
monthsShort: ['Sty', 'Lut', 'Mar', 'Kwi', 'Maj', 'Cze', 'Lip', 'Sie', 'Wrz', 'Paź', 'Lis', 'Gru'],
today: 'Dzisiaj',
clear: 'Wyczyść',
dateFormat: 'yyyy-mm-dd',
timeFormat: 'hh:ii:aa',
firstDay: 1
};
})(jQuery);

;(function ($) { $.fn.datepicker.language['pt-BR'] = {
days: ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado'],
daysShort: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sab'],
daysMin: ['Do', 'Se', 'Te', 'Qu', 'Qu', 'Se', 'Sa'],
months: ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'],
monthsShort: ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'],
today: 'Hoje',
clear: 'Limpar',
dateFormat: 'dd/mm/yyyy',
timeFormat: 'hh:ii',
firstDay: 0
}; })(jQuery);

;(function ($) { $.fn.datepicker.language['pt'] = {
days: ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado'],
daysShort: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sab'],
daysMin: ['Do', 'Se', 'Te', 'Qa', 'Qi', 'Sx', 'Sa'],
months: ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'],
monthsShort: ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'],
today: 'Hoje',
clear: 'Limpar',
dateFormat: 'dd/mm/yyyy',
timeFormat: 'hh:ii',
firstDay: 1
}; })(jQuery);

;(function ($) { $.fn.datepicker.language['ro'] = {
days: ['Duminică', 'Luni', 'Marţi', 'Miercuri', 'Joi', 'Vineri', 'Sâmbătă'],
daysShort: ['Dum', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm'],
daysMin: ['D', 'L', 'Ma', 'Mi', 'J', 'V', 'S'],
months: ['Ianuarie','Februarie','Martie','Aprilie','Mai','Iunie','Iulie','August','Septembrie','Octombrie','Noiembrie','Decembrie'],
monthsShort: ['Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', 'Iul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'],
today: 'Azi',
clear: 'Şterge',
dateFormat: 'dd.mm.yyyy',
timeFormat: 'hh:ii',
firstDay: 1
}; })(jQuery);

;(function ($) { $.fn.datepicker.language['sk'] = {
days: ['Nedeľa', 'Pondelok', 'Utorok', 'Streda', 'Štvrtok', 'Piatok', 'Sobota'],
daysShort: ['Ned', 'Pon', 'Uto', 'Str', 'Štv', 'Pia', 'Sob'],
daysMin: ['Ne', 'Po', 'Ut', 'St', 'Št', 'Pi', 'So'],
months: ['Január','Február','Marec','Apríl','Máj','Jún', 'Júl','August','September','Október','November','December'],
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Máj', 'Jún', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'],
today: 'Dnes',
clear: 'Vymazať',
dateFormat: 'dd.mm.yyyy',
timeFormat: 'hh:ii',
firstDay: 1
}; })(jQuery);

;(function ($) { $.fn.datepicker.language['zh'] = {
days: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
daysShort: ['日', '一', '二', '三', '四', '五', '六'],
daysMin: ['日', '一', '二', '三', '四', '五', '六'],
months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
monthsShort: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
today: '今天',
clear: '清除',
dateFormat: 'yyyy-mm-dd',
timeFormat: 'hh:ii',
firstDay: 1
}; })(jQuery);

+ 1
- 1
frappe/public/less/datepicker.less Visa fil

@@ -1,5 +1,5 @@
@import "variables.less";
@import (less) "../js/lib/datepicker/datepicker.min.css";
@import (less) "../../../node_modules/air-datepicker/dist/css/datepicker.min.css";

.datepicker {
font-family: inherit;


+ 163
- 0
frappe/tests/test_fmt_datetime.py Visa fil

@@ -0,0 +1,163 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals

import datetime

import frappe
from frappe.utils import (
getdate, get_datetime, get_time,
get_user_date_format, get_user_time_format,
formatdate, format_datetime, format_time)
import unittest

test_date_obj = datetime.datetime.now()
test_date = test_date_obj.strftime('%Y-%m-%d')
test_time = test_date_obj.strftime('%H:%M:%S.%f')
test_datetime = test_date_obj.strftime('%Y-%m-%d %H:%M:%S.%f')
test_date_formats = {
'yyyy-mm-dd': test_date_obj.strftime('%Y-%m-%d'),
'dd-mm-yyyy': test_date_obj.strftime('%d-%m-%Y'),
'dd/mm/yyyy': test_date_obj.strftime('%d/%m/%Y'),
'dd.mm.yyyy': test_date_obj.strftime('%d.%m.%Y'),
'mm/dd/yyyy': test_date_obj.strftime('%m/%d/%Y'),
'mm-dd-yyyy': test_date_obj.strftime('%m-%d-%Y')}
test_time_formats = {
'HH:mm:ss': test_date_obj.strftime('%H:%M:%S'),
'HH:mm': test_date_obj.strftime('%H:%M')}


class TestFmtDatetime(unittest.TestCase):
"""Tests date, time and datetime formatters and some associated
utility functions. These rely on the system-wide date and time
formats.
"""

# Set up and tidy up routines

def setUp(self):
# create test domain
self.pre_test_date_format = frappe.db.get_default("date_format")
self.pre_test_time_format = frappe.db.get_default("time_format")

def tearDown(self):
frappe.db.set_default("date_format", self.pre_test_date_format)
frappe.db.set_default("time_format", self.pre_test_time_format)
frappe.local.user_date_format = None
frappe.local.user_time_format = None

# Test utility functions

def test_set_default_date_format(self):
frappe.db.set_default("date_format", "ZYX321")
self.assertEqual(frappe.db.get_default("date_format"), "ZYX321")

def test_set_default_time_format(self):
frappe.db.set_default("time_format", "XYZ123")
self.assertEqual(frappe.db.get_default("time_format"), "XYZ123")

def test_get_functions(self):
# Test round-trip through getdate, get_datetime and get_time
self.assertEqual(test_date_obj, get_datetime(test_datetime))
self.assertEqual(test_date_obj.date(), getdate(test_date))
self.assertEqual(test_date_obj.time(), get_time(test_time))

# Test date formatters

def test_formatdate_forced(self):
# Test with forced date formats
self.assertEqual(
formatdate(test_date, 'dd-yyyy-mm'),
test_date_obj.strftime('%d-%Y-%m'))
self.assertEqual(
formatdate(test_date, 'dd-yyyy-MM'),
test_date_obj.strftime('%d-%Y-%m'))

def test_formatdate_forced_broken_locale(self):
# Test with forced date formats
lang = frappe.local.lang
# Force fallback from Babel
try:
frappe.local.lang = 'FAKE'
self.assertEqual(
formatdate(test_date, 'dd-yyyy-mm'),
test_date_obj.strftime('%d-%Y-%m'))
self.assertEqual(
formatdate(test_date, 'dd-yyyy-MM'),
test_date_obj.strftime('%d-%Y-%m'))
finally:
frappe.local.lang = lang

def test_format_date(self):
# Test formatdate with various default date formats set
for fmt, valid_fmt in test_date_formats.items():
frappe.db.set_default("date_format", fmt)
frappe.local.user_date_format = None
self.assertEqual(get_user_date_format(), fmt)
self.assertEqual(formatdate(test_date), valid_fmt)

# Test time formatters

def test_format_time_forced(self):
# Test with forced time formats
self.assertEqual(
format_time(test_time, 'ss:mm:HH'),
test_date_obj.strftime('%S:%M:%H'))

@unittest.expectedFailure
def test_format_time_forced_broken_locale(self):
# Test with forced time formats
# Currently format_time defaults to HH:mm:ss if the locale is
# broken, so this is an expected failure.
lang = frappe.local.lang
try:
# Force fallback from Babel
frappe.local.lang = 'FAKE'
self.assertEqual(
format_time(test_time, 'ss:mm:HH'),
test_date_obj.strftime('%S:%M:%H'))
finally:
frappe.local.lang = lang

def test_format_time(self):
# Test format_time with various default time formats set
for fmt, valid_fmt in test_time_formats.items():
frappe.db.set_default("time_format", fmt)
frappe.local.user_time_format = None
self.assertEqual(get_user_time_format(), fmt)
self.assertEqual(format_time(test_time), valid_fmt)

# Test datetime formatters

def test_format_datetime_forced(self):
# Test with forced date formats
self.assertEqual(
format_datetime(test_datetime, 'dd-yyyy-MM ss:mm:HH'),
test_date_obj.strftime('%d-%Y-%m %S:%M:%H'))

@unittest.expectedFailure
def test_format_datetime_forced_broken_locale(self):
# Test with forced datetime formats
# Currently format_datetime defaults to yyyy-MM-dd HH:mm:ss
# if the locale is broken, so this is an expected failure.
lang = frappe.local.lang
# Force fallback from Babel
try:
frappe.local.lang = 'FAKE'
self.assertEqual(
format_datetime(test_datetime, 'dd-yyyy-MM ss:mm:HH'),
test_date_obj.strftime('%d-%Y-%m %S:%M:%H'))
finally:
frappe.local.lang = lang

def test_format_datetime(self):
# Test formatdate with various default date formats set
for date_fmt, valid_date in test_date_formats.items():
frappe.db.set_default("date_format", date_fmt)
frappe.local.user_date_format = None
for time_fmt, valid_time in test_time_formats.items():
frappe.db.set_default("time_format", time_fmt)
frappe.local.user_time_format = None
valid_fmt = valid_date + ' ' + valid_time
self.assertEqual(
format_datetime(test_datetime), valid_fmt)

+ 62
- 22
frappe/utils/data.py Visa fil

@@ -209,22 +209,31 @@ def get_datetime_str(datetime_obj):
datetime_obj = get_datetime(datetime_obj)
return datetime_obj.strftime(DATETIME_FORMAT)

def get_user_format():
if getattr(frappe.local, "user_format", None) is None:
frappe.local.user_format = frappe.db.get_default("date_format")
def get_user_date_format():
"""Get the current user date format. The result will be cached."""
if getattr(frappe.local, "user_date_format", None) is None:
frappe.local.user_date_format = frappe.db.get_default("date_format")

return frappe.local.user_format or "yyyy-mm-dd"
return frappe.local.user_date_format or "yyyy-mm-dd"

def formatdate(string_date=None, format_string=None):
"""
Converts the given string date to :data:`user_format`
User format specified in defaults
get_user_format = get_user_date_format # for backwards compatibility

def get_user_time_format():
"""Get the current user time format. The result will be cached."""
if getattr(frappe.local, "user_time_format", None) is None:
frappe.local.user_time_format = frappe.db.get_default("time_format")

return frappe.local.user_time_format or "HH:mm:ss"

def format_date(string_date=None, format_string=None):
"""Converts the given string date to :data:`user_date_format`
User format specified in defaults

Examples:
Examples:

* dd-mm-yyyy
* mm-dd-yyyy
* dd/mm/yyyy
* dd-mm-yyyy
* mm-dd-yyyy
* dd/mm/yyyy
"""

if not string_date:
@@ -232,29 +241,60 @@ def formatdate(string_date=None, format_string=None):

date = getdate(string_date)
if not format_string:
format_string = get_user_format()
format_string = get_user_date_format()
format_string = format_string.replace("mm", "MM")
try:
formatted_date = babel.dates.format_date(date, format_string, locale=(frappe.local.lang or "").replace("-", "_"))
formatted_date = babel.dates.format_date(
date, format_string,
locale=(frappe.local.lang or "").replace("-", "_"))
except UnknownLocaleError:
format_string = format_string.replace("MM", "%m").replace("dd", "%d").replace("yyyy", "%Y")
formatted_date = date.strftime(format_string)
return formatted_date

def format_time(txt):
formatdate = format_date # For backwards compatibility

def format_time(time_string=None, format_string=None):
"""Converts the given string time to :data:`user_time_format`
User format specified in defaults

Examples:

* HH:mm:ss
* HH:mm
"""

if not time_string:
return ''

time_ = get_time(time_string)
if not format_string:
format_string = get_user_time_format()
try:
formatted_time = babel.dates.format_time(get_time(txt), locale=(frappe.local.lang or "").replace("-", "_"))
formatted_time = babel.dates.format_time(
time_, format_string,
locale=(frappe.local.lang or "").replace("-", "_"))
except UnknownLocaleError:
formatted_time = get_time(txt).strftime("%H:%M:%S")
formatted_time = time_.strftime("%H:%M:%S")
return formatted_time

def format_datetime(datetime_string, format_string=None):
"""Converts the given string time to :data:`user_datetime_format`
User format specified in defaults

Examples:

* dd-mm-yyyy HH:mm:ss
* mm-dd-yyyy HH:mm
"""
if not datetime_string:
return

datetime = get_datetime(datetime_string)
if not format_string:
format_string = get_user_format().replace("mm", "MM") + " HH:mm:ss"
format_string = (
get_user_date_format().replace("mm", "MM")
+ ' ' + get_user_time_format())

try:
formatted_datetime = babel.dates.format_datetime(datetime, format_string, locale=(frappe.local.lang or "").replace("-", "_"))
@@ -363,14 +403,14 @@ def rounded(num, precision=0):
# avoid rounding errors
num = round(num * multiplier if precision else num, 8)

floor = math.floor(num)
decimal_part = num - floor
floor_num = math.floor(num)
decimal_part = num - floor_num

if not precision and decimal_part == 0.5:
num = floor if (floor % 2 == 0) else floor + 1
num = floor_num if (floor_num % 2 == 0) else floor_num + 1
else:
if decimal_part == 0.5:
num = floor + 1
num = floor_num + 1
else:
num = round(num)



+ 1
- 0
frappe/utils/install.py Visa fil

@@ -146,6 +146,7 @@ def add_country_and_currency(name, country):
"country_name": name,
"code": country.code,
"date_format": country.date_format or "dd-mm-yyyy",
"time_format": country.time_format or "HH:mm:ss",
"time_zones": "\n".join(country.timezones or []),
"docstatus": 0
}).db_insert()


+ 50
- 46
frappe/utils/safe_exec.py Visa fil

@@ -32,8 +32,10 @@ def get_safe_globals():
datautils = frappe._dict()
if frappe.db:
date_format = frappe.db.get_default("date_format") or "yyyy-mm-dd"
time_format = frappe.db.get_default("time_format") or "HH:mm:ss"
else:
date_format = 'yyyy-mm-dd'
date_format = "yyyy-mm-dd"
time_format = "HH:mm:ss"

add_module_properties(frappe.utils.data, datautils, lambda obj: hasattr(obj, "__call__"))

@@ -44,54 +46,55 @@ def get_safe_globals():

out = frappe._dict(
# make available limited methods of frappe
json = json,
dict = dict,
frappe = frappe._dict(
_ = frappe._,
_dict = frappe._dict,
flags = frappe.flags,

format = frappe.format_value,
format_value = frappe.format_value,
date_format = date_format,
format_date = frappe.utils.data.global_date_format,
form_dict = getattr(frappe.local, 'form_dict', {}),

get_meta = frappe.get_meta,
get_doc = frappe.get_doc,
get_cached_doc = frappe.get_cached_doc,
get_list = frappe.get_list,
get_all = frappe.get_all,
get_system_settings = frappe.get_system_settings,

utils = datautils,
get_url = frappe.utils.get_url,
render_template = frappe.render_template,
msgprint = frappe.msgprint,

user = user,
get_fullname = frappe.utils.get_fullname,
get_gravatar = frappe.utils.get_gravatar_url,
full_name = frappe.local.session.data.full_name if getattr(frappe.local, "session", None) else "Guest",
request = getattr(frappe.local, 'request', {}),
session = frappe._dict(
user = user,
csrf_token = frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else ''
json=json,
dict=dict,
frappe=frappe._dict(
_=frappe._,
_dict=frappe._dict,
flags=frappe.flags,

format=frappe.format_value,
format_value=frappe.format_value,
date_format=date_format,
time_format=time_format,
format_date=frappe.utils.data.global_date_format,
form_dict=getattr(frappe.local, 'form_dict', {}),

get_meta=frappe.get_meta,
get_doc=frappe.get_doc,
get_cached_doc=frappe.get_cached_doc,
get_list=frappe.get_list,
get_all=frappe.get_all,
get_system_settings=frappe.get_system_settings,

utils=datautils,
get_url=frappe.utils.get_url,
render_template=frappe.render_template,
msgprint=frappe.msgprint,

user=user,
get_fullname=frappe.utils.get_fullname,
get_gravatar=frappe.utils.get_gravatar_url,
full_name=frappe.local.session.data.full_name if getattr(frappe.local, "session", None) else "Guest",
request=getattr(frappe.local, 'request', {}),
session=frappe._dict(
user=user,
csrf_token=frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else ''
),
socketio_port = frappe.conf.socketio_port,
get_hooks = frappe.get_hooks,
socketio_port=frappe.conf.socketio_port,
get_hooks=frappe.get_hooks,
),
style = frappe._dict(
border_color = '#d1d8dd'
style=frappe._dict(
border_color='#d1d8dd'
),
get_toc = get_toc,
get_next_link = get_next_link,
_ = frappe._,
get_shade = get_shade,
scrub = scrub,
guess_mimetype = mimetypes.guess_type,
html2text = html2text,
dev_server = 1 if os.environ.get('DEV_SERVER', False) else 0
get_toc=get_toc,
get_next_link=get_next_link,
_=frappe._,
get_shade=get_shade,
scrub=scrub,
guess_mimetype=mimetypes.guess_type,
html2text=html2text,
dev_server=1 if os.environ.get('DEV_SERVER', False) else 0
)

add_module_properties(frappe.exceptions, out.frappe, lambda obj: inspect.isclass(obj) and issubclass(obj, Exception))
@@ -99,6 +102,7 @@ def get_safe_globals():
if not frappe.flags.in_setup_help:
out.get_visible_columns = get_visible_columns
out.frappe.date_format = date_format
out.frappe.time_format = time_format
out.frappe.db = frappe._dict(
get_list = frappe.get_list,
get_all = frappe.get_all,


+ 1
- 0
package.json Visa fil

@@ -19,6 +19,7 @@
"homepage": "https://frappe.io",
"dependencies": {
"ace-builds": "^1.4.1",
"air-datepicker": "http://github.com/frappe/air-datepicker",
"awesomplete": "^1.1.2",
"bootstrap": "^4.3.1",
"cookie": "^0.3.1",


+ 14
- 3
yarn.lock Visa fil

@@ -210,6 +210,12 @@ after@0.8.2:
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=

"air-datepicker@http://github.com/frappe/air-datepicker":
version "2.2.3"
resolved "http://github.com/frappe/air-datepicker#ed37b94d95c68d8544357e330be0c89d044a3eea"
dependencies:
jquery ">=2.0.0 <4.0.0"

ajv@^5.1.0:
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
@@ -2560,6 +2566,11 @@ jpeg-js@^0.3.2:
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.3.6.tgz#c40382aac9506e7d1f2d856eb02f6c7b2a98b37c"
integrity sha512-MUj2XlMB8kpe+8DJUGH/3UJm4XpI8XEgZQ+CiHDeyrGoKPdW/8FJv6ku+3UiYm5Fz3CWaL+iXmD8Q4Ap6aC1Jw==

"jquery@>=2.0.0 <4.0.0":
version "3.4.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2"
integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==

js-base64@^2.1.8, js-base64@^2.1.9:
version "2.5.1"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121"
@@ -5356,9 +5367,9 @@ yargs-parser@^5.0.0:
camelcase "^3.0.0"

yargs@^14.2:
version "14.2.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.0.tgz#f116a9242c4ed8668790b40759b4906c276e76c3"
integrity sha512-/is78VKbKs70bVZH7w4YaZea6xcJWOAwkhbR0CFuZBmYtfTYF0xjGJF43AYd8g2Uii1yJwmS5GR2vBmrc32sbg==
version "14.2.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.2.tgz#2769564379009ff8597cdd38fba09da9b493c4b5"
integrity sha512-/4ld+4VV5RnrynMhPZJ/ZpOCGSCeghMykZ3BhdFBDa9Wy/RH6uEGNWDJog+aUlq+9OM1CFTgtYRW5Is1Po9NOA==
dependencies:
cliui "^5.0.0"
decamelize "^1.2.0"


Laddar…
Avbryt
Spara