From 717441a05eed91920f249b0cf803f9a3e72589f6 Mon Sep 17 00:00:00 2001 From: Nicolas Evrard Date: Mon, 10 Jul 2023 10:28:44 +0200 Subject: [PATCH 1/5] Don't reset dates to None when the date is invalid PCLAS-209 & PCLAS-219 --- sao/CHANGELOG | 2 + sao/src/common.js | 28 ++++++++-- sao/src/model.js | 33 +++++++++++ sao/src/sao.js | 32 ++++++++--- sao/src/screen.js | 3 + sao/src/view/form.js | 36 +++++++++++- sao/tests/sao.js | 45 ++++++++++++--- tryton/CHANGELOG | 2 + tryton/tryton/common/datetime_.py | 56 ++++++++++++++----- .../gui/window/view_form/model/field.py | 27 +++++++++ .../gui/window/view_form/screen/screen.py | 2 + 11 files changed, 228 insertions(+), 38 deletions(-) diff --git a/sao/CHANGELOG b/sao/CHANGELOG index b08151454d8..82c22c46d15 100644 --- a/sao/CHANGELOG +++ b/sao/CHANGELOG @@ -1,3 +1,5 @@ +* Don't reset date/datetime/time to None when it is invalid + Version 6.4.8 - 2022-12-06 -------------------------- * Bug fixes (see mercurial logs for details) diff --git a/sao/src/common.js b/sao/src/common.js index 8794021066a..86ccc07ec99 100644 --- a/sao/src/common.js +++ b/sao/src/common.js @@ -238,6 +238,9 @@ if (!date) { return ''; } + if (!date.isValid()) { + return date.invalid_value; + } return date.format(Sao.common.moment_format(format)); }; @@ -253,26 +256,31 @@ return number; } } - return 0; + return undefined; }; return Sao.Time(getNumber('%H'), getNumber('%M'), getNumber('%S'), - getNumber('%f')); + getNumber('%f'), value); }; Sao.common.format_date = function(date_format, date) { if (!date) { return ''; } + if (!date.isValid()) { + return date.invalid_value; + } return date.format(Sao.common.moment_format(date_format)); }; Sao.common.parse_date = function(date_format, value) { - var date = moment(value, - Sao.common.moment_format(date_format)); + if (!value) { + return null; + } + var date = moment(value, Sao.common.moment_format(date_format)); if (date.isValid()) { date = Sao.Date(date.year(), date.month(), date.date()); } else { - date = null; + date = Sao.Date(undefined, undefined, undefined, false, value); } return date; }; @@ -281,17 +289,25 @@ if (!date) { return ''; } + if (!date.isValid()) { + return date.invalid_value; + } return date.format(Sao.common.moment_format(datetime_format)); }; Sao.common.parse_datetime = function(datetime_format, value) { + if (!value) { + return null; + } var date = moment(value, Sao.common.moment_format(datetime_format)); if (date.isValid()) { date = Sao.DateTime(date.year(), date.month(), date.date(), date.hour(), date.minute(), date.second(), date.millisecond()); } else { - date = null; + date = Sao.DateTime(undefined, undefined, undefined, + undefined, undefined, undefined, undefined, + false, false, value); } return date; }; diff --git a/sao/src/model.js b/sao/src/model.js index 42a85368501..fa6eb3279fa 100644 --- a/sao/src/model.js +++ b/sao/src/model.js @@ -1810,6 +1810,17 @@ date_format: function(record) { var context = this.get_context(record); return Sao.common.date_format(context.date_format); + }, + validate: function(record, softvalidation, pre_validate) { + var valid = Sao.field.DateTime._super.validate.call( + this, record, softvalidation, pre_validate); + var state_attrs = this.get_state_attrs(record); + var value = this.get(record); + if (value && !value.isValid()) { + state_attrs.invalid = 'value'; + valid = false; + } + return valid; } }); @@ -1826,6 +1837,17 @@ date_format: function(record) { var context = this.get_context(record); return Sao.common.date_format(context.date_format); + }, + validate: function(record, softvalidation, pre_validate) { + var valid = Sao.field.DateTime._super.validate.call( + this, record, softvalidation, pre_validate); + var state_attrs = this.get_state_attrs(record); + var value = this.get(record); + if (value && !value.isValid()) { + state_attrs.invalid = 'value'; + valid = false; + } + return valid; } }); @@ -1841,6 +1863,17 @@ } Sao.field.Time._super.set_client.call(this, record, value, force_change); + }, + validate: function(record, softvalidation, pre_validate) { + var valid = Sao.field.Time._super.validate.call( + this, record, softvalidation, pre_validate); + var state_attrs = this.get_state_attrs(record); + var value = this.get(record); + if (value && !value.isValid()) { + state_attrs.invalid = 'value'; + valid = false; + } + return valid; } }); diff --git a/sao/src/sao.js b/sao/src/sao.js index f84e9e90ffb..c3512d35e67 100644 --- a/sao/src/sao.js +++ b/sao/src/sao.js @@ -239,9 +239,12 @@ var Sao = {}; } }; - Sao.Date = function(year, month, day) { + Sao.Date = function(year, month, day, valid=true, value=null) { var date; - if (month === undefined) { + if (!valid) { + date = moment.invalid(); + date.invalid_value = value; + } else if (month === undefined) { date = moment(year); year = undefined; } @@ -266,9 +269,13 @@ var Sao = {}; Sao.DateTime = function( year, month, day, - hour=0, minute=0, second=0, millisecond=0, utc=false) { + hour=0, minute=0, second=0, millisecond=0, utc=false, + valid=true, value=null) { var datetime; - if (month === undefined) { + if (!valid) { + datetime = moment.invalid(); + datetime.invalid_value = value; + } else if (month === undefined) { datetime = moment(year); year = undefined; } @@ -310,9 +317,20 @@ var Sao = {}; Sao.DateTime.max = moment(new Date(100000000 * 86400000)).local(); Sao.DateTime.max.isDateTime = true; - Sao.Time = function(hour, minute, second, millisecond) { - var time = moment({hour: hour, minute: minute, second: second, - millisecond: millisecond || 0}); + Sao.Time = function(hour, minute, second, millisecond, value=null) { + var time; + if ((hour === undefined) && (minute === undefined) && + (second === undefined) && (millisecond === undefined)) { + time = moment.invalid(); + } else { + time = moment({ + hour: hour, minute: minute, second: second, + millisecond: millisecond || 0 + }); + } + if (!time.isValid()) { + time.invalid_value = value; + } time.isTime = true; return time; }; diff --git a/sao/src/screen.js b/sao/src/screen.js index 399745d056f..d76ed341d93 100644 --- a/sao/src/screen.js +++ b/sao/src/screen.js @@ -1889,6 +1889,9 @@ } else if (invalid == 'children') { fields.push(Sao.i18n.gettext( 'The values of "%1" are not valid.', string)); + } else if (invalid == 'value') { + fields.push(Sao.i18n.gettext( + 'The value of "%1" is not valid.', string)); } else { if (domain_parser.stringable(invalid)) { fields.push(domain_parser.string(invalid)); diff --git a/sao/src/view/form.js b/sao/src/view/form.js index 26caaba0ae0..a3fc48caca6 100644 --- a/sao/src/view/form.js +++ b/sao/src/view/form.js @@ -2047,7 +2047,15 @@ function eval_pyson(value){ 'name': attributes.name, }).appendTo(group); this.date.uniqueId(); - this.date.on('keydown', this.send_modified.bind(this)); + this.date.on('keydown', () => { + var value = this.get_value(); + if (value && !value.isValid()) { + this._invalid_el().addClass('has-error'); + } else { + this._invalid_el().removeClass('has-error'); + } + this.send_modified(); + }); this.input = jQuery('', { 'type': this._input, 'role': 'button', @@ -2136,11 +2144,33 @@ function eval_pyson(value){ if (this.record && this.field) { var field_value = this.cast( this.field.get_client(this.record)); - return (JSON.stringify(field_value) != - JSON.stringify(this.get_value())); + return this._cmp(field_value, this.get_value()); } return false; }, + send_modified: function() { + window.setTimeout(() => { + var value = this.get_value(); + window.setTimeout(() => { + if (this.record && + this._cmp(this.get_value(), value) && + this.modified) { + this.view.screen.record_modified(false); + } + }, 300); + }); + }, + _cmp: function(dt1, dt2) { + if ((dt1 && !dt1.isValid()) && + (dt2 && !dt2.isValid())) { + return dt1.invalid_value != dt2.invalid_value; + } else if ((dt1 && !dt1.isValid()) || + (dt2 && !dt2.isValid())) { + return true; + } else { + return JSON.stringify(dt1) != JSON.stringify(dt2); + } + }, set_value: function() { this.field.set_client(this.record, this.get_value()); }, diff --git a/sao/tests/sao.js b/sao/tests/sao.js index 5bd0ca73b0e..a5af4c544f6 100644 --- a/sao/tests/sao.js +++ b/sao/tests/sao.js @@ -1659,6 +1659,28 @@ JSON.stringify(value) + ', ' + JSON.stringify(context) + ')'); }; + var test_moment_func = function(test) { + var value = test[0]; + var result = test[1]; + + var compare = function(d1, d2) { + if ((d1 && d1.isValid()) && (d2 && d2.isValid())) { + return d1.valueOf() === d2.valueOf(); + } else if ((d1 && !d1.isValid()) && (d2 && !d2.isValid())) { + return d1.invalid_value === d2.invalid_value; + } else if (!d1 && !d2) { + return d1 === d2; + } else { + return false; + } + }; + + QUnit.ok(compare( + parser.convert_value(this, value, context), result), + 'convert_value(' + JSON.stringify(this)+ ', ' + + JSON.stringify(value)+ ', ' + JSON.stringify(context) + ')'); + }; + var field = { 'type': 'boolean' }; @@ -1757,22 +1779,27 @@ [ ['2002-12-04', Sao.DateTime(2002, 11, 4)], ['2002-12-04 12:30:00', Sao.DateTime(2002, 11, 4, 12, 30)] - ].forEach(test_valueOf_func, field); + ].forEach(test_moment_func, field); [ - ['test', null], + ['test', Sao.DateTime( + undefined, undefined, undefined, + undefined, undefined, undefined, undefined, false, + false, 'test')], [null, null] - ].forEach(test_func, field); + ].forEach(test_moment_func, field); field = { 'type': 'date' }; [ ['2002-12-04', Sao.Date(2002, 11, 4)] - ].forEach(test_valueOf_func, field); + ].forEach(test_moment_func, field); [ - ['test', null], + ['test', Sao.Date( + undefined, undefined, undefined, + false, 'test')], [null, null] - ].forEach(test_func, field); + ].forEach(test_moment_func, field); field = { 'type': 'time', @@ -1780,11 +1807,11 @@ }; [ ['12:30:00', Sao.Time(12, 30, 0)], - ['test', Sao.Time(0, 0, 0)] - ].forEach(test_valueOf_func, field); + ['test', Sao.Time(undefined, undefined, undefined, undefined, 'test')] + ].forEach(test_moment_func, field); [ [null, null] - ].forEach(test_func, field); + ].forEach(test_moment_func, field); field = { 'type': 'timedelta' diff --git a/tryton/CHANGELOG b/tryton/CHANGELOG index 7eb3c8bef2c..cc7108fe693 100644 --- a/tryton/CHANGELOG +++ b/tryton/CHANGELOG @@ -1,3 +1,5 @@ +* Don't reset date/datetime/time to None when it is invalid + Version 6.4.6 - 2022-11-17 -------------------------- * Bug fixes (see mercurial logs for details) diff --git a/tryton/tryton/common/datetime_.py b/tryton/tryton/common/datetime_.py index 09c4e59d1d9..802d176cef5 100644 --- a/tryton/tryton/common/datetime_.py +++ b/tryton/tryton/common/datetime_.py @@ -15,6 +15,24 @@ _ = gettext.gettext +class InvalidDateTime: + _instance = None + + def __new__(cls): + if cls._instance: + return cls._instance + return super().__new__(cls) + + __gt__ = lambda self, other: False # noqa: E731 + __lt__ = lambda self, other: False # noqa: E731 + __le__ = lambda self, other: False # noqa: E731 + __ge__ = lambda self, other: False # noqa: E731 + __eq__ = lambda self, other: False # noqa: E731 + + +INVALID_DT_VALUE = InvalidDateTime() + + def _fix_format(format_): if '%Y' in format_: if (datetime.date.min.strftime('%Y') != '0001' @@ -118,16 +136,21 @@ def __init__(self): def parse(self): text = self.get_text() - date = None + style_context = self.get_style_context() if text: try: - date = date_parse(text, self.__format).date() + self.__date = date_parse(text, self.__format).date() + style_context.remove_class('invalid') except (ValueError, OverflowError): - pass - - self.__date = date + self.__date = INVALID_DT_VALUE + style_context.add_class('invalid') + else: + self.__date = None + style_context.remove_class('invalid') def update_label(self): + if self.__date is INVALID_DT_VALUE: + return if not self.__date: self.set_text('') return @@ -210,7 +233,7 @@ def do_set_property(self, prop, value): self.set_text(value) self.parse() value = self.__date - if value: + if value and value is not INVALID_DT_VALUE: if isinstance(value, datetime.datetime): value = value.date() assert isinstance(value, datetime.date), value @@ -334,16 +357,21 @@ def __init__(self): def parse(self): text = self.__entry.get_text() - time = None + style_context = self.get_style_context() if text: try: - time = date_parse(text).time() + self.__time = date_parse(text).time() + style_context.remove_class('invalid') except (ValueError, OverflowError): - pass - - self.__time = time + self.__time = INVALID_DT_VALUE + style_context.add_class('invalid') + else: + self.__time = None + style_context.remove_class('invalid') def update_label(self): + if self.__time is INVALID_DT_VALUE: + return if self.__time is None: self.__entry.set_text('') return @@ -383,7 +411,7 @@ def do_set_property(self, prop, value): self.__entry.set_text(value) self.parse() value = self.__time - if value: + if value and value is not INVALID_DT_VALUE: if isinstance(value, datetime.datetime): value = value.time() self.__time = value @@ -517,7 +545,9 @@ def do_get_property(self, prop): if prop.name == 'value': date = self.__date.props.value time = self.__time.props.value or datetime.time() - if date: + if (date is INVALID_DT_VALUE or time is INVALID_DT_VALUE): + return INVALID_DT_VALUE + elif date: return datetime.datetime.combine(date, time) else: return diff --git a/tryton/tryton/gui/window/view_form/model/field.py b/tryton/tryton/gui/window/view_form/model/field.py index 25f0c126837..4eaab989d8d 100644 --- a/tryton/tryton/gui/window/view_form/model/field.py +++ b/tryton/tryton/gui/window/view_form/model/field.py @@ -310,11 +310,29 @@ def date_format(self, record): def time_format(self, record): return record.expr_eval(self.attrs['format']) + def validate(self, record, softvalidation=False, pre_validate=None): + valid = super().validate(record, softvalidation, pre_validate) + state_attrs = self.get_state_attrs(record) + if ((v := record.value.get(self.name)) + and not isinstance(v, datetime.datetime)): + state_attrs['invalid'] = 'value' + valid = False + return valid + class DateField(Field): _default = None + def validate(self, record, softvalidation=False, pre_validate=None): + valid = super().validate(record, softvalidation, pre_validate) + state_attrs = self.get_state_attrs(record) + if ((v := record.value.get(self.name)) + and not isinstance(v, datetime.date)): + state_attrs['invalid'] = 'value' + valid = False + return valid + def set_client(self, record, value, force_change=False): if isinstance(value, datetime.datetime): assert(value.time() == datetime.time()) @@ -343,6 +361,15 @@ def set_client(self, record, value, force_change=False): def time_format(self, record): return record.expr_eval(self.attrs['format']) + def validate(self, record, softvalidation=False, pre_validate=None): + valid = super().validate(record, softvalidation, pre_validate) + state_attrs = self.get_state_attrs(record) + if ((v := record.value.get(self.name)) + and not isinstance(v, datetime.time)): + state_attrs['invalid'] = 'value' + valid = False + return valid + class TimeDeltaField(Field): diff --git a/tryton/tryton/gui/window/view_form/screen/screen.py b/tryton/tryton/gui/window/view_form/screen/screen.py index 8c97edec37b..bf74c3830a8 100644 --- a/tryton/tryton/gui/window/view_form/screen/screen.py +++ b/tryton/tryton/gui/window/view_form/screen/screen.py @@ -1238,6 +1238,8 @@ def invalid_message(self, record=None): fields.append(domain_string % string) elif invalid == 'children': fields.append(_('The values of "%s" are not valid.') % string) + elif invalid == 'value': + fields.append(_('The value of "%s" is not valid.') % string) else: if domain_parser.stringable(invalid): fields.append(domain_parser.string(invalid)) From 719066180b4428fcff9d73a9ec14039c13b82c24 Mon Sep 17 00:00:00 2001 From: Nicolas Evrard Date: Tue, 11 Jul 2023 13:04:30 +0200 Subject: [PATCH 2/5] Handle invalid dates in on_changes --- sao/src/model.js | 21 ++++++++++++++++++ tryton/tryton/common/datetime_.py | 22 ++++--------------- .../gui/window/view_form/model/field.py | 19 ++++++++++++++++ 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/sao/src/model.js b/sao/src/model.js index fa6eb3279fa..7723dd91ca6 100644 --- a/sao/src/model.js +++ b/sao/src/model.js @@ -1789,6 +1789,13 @@ time_format: function(record) { return record.expr_eval(this.description.format); }, + get_eval: function() { + var value = Sao.field.DateTime._super.get_eval.call(this); + if (!value.isValid()) { + value = null; + } + return value; + }, set_client: function(record, value, force_change) { var current_value; if (value) { @@ -1826,6 +1833,13 @@ Sao.field.Date = Sao.class_(Sao.field.Field, { _default: null, + get_eval: function() { + var value = Sao.field.DateTime._super.get_eval.call(this); + if (!value.isValid()) { + value = null; + } + return value; + }, set_client: function(record, value, force_change) { if (value && !value.isDate) { value.isDate = true; @@ -1856,6 +1870,13 @@ time_format: function(record) { return record.expr_eval(this.description.format); }, + get_eval: function() { + var value = Sao.field.DateTime._super.get_eval.call(this); + if (!value.isValid()) { + value = null; + } + return value; + }, set_client: function(record, value, force_change) { if (value && (value.isDate || value.isDateTime)) { value = Sao.Time(value.hour(), value.minute(), diff --git a/tryton/tryton/common/datetime_.py b/tryton/tryton/common/datetime_.py index 802d176cef5..bb3966748e5 100644 --- a/tryton/tryton/common/datetime_.py +++ b/tryton/tryton/common/datetime_.py @@ -2,7 +2,6 @@ # this repository contains the full copyright notices and license terms. import datetime import gettext -import re from dateutil.parser import parse from dateutil.relativedelta import relativedelta @@ -10,7 +9,9 @@ from .common import IconFactory -__all__ = ['Date', 'CellRendererDate', 'Time', 'CellRendererTime', 'DateTime'] +__all__ = [ + 'Date', 'CellRendererDate', 'InvalidDateTime', 'Time', 'CellRendererTime', + 'DateTime'] _ = gettext.gettext @@ -56,22 +57,7 @@ def date_parse(text, format_='%x'): except ValueError: monthfirst = False yearfirst = not dayfirst and not monthfirst - text = re.sub('/+', '/', text) - if len(text) == 6 and re.search('[0-9]{6}', text): - text = '%s/%s/%s' % (text[:2], text[2:4], text[4:6]) - elif len(text) == 8 and re.search('[0-9]{8}', text): - if yearfirst: - text = '%s/%s/%s' % (text[:4], text[4:6], text[6:8]) - else: - text = '%s/%s/%s' % (text[:2], text[2:4], text[4:8]) - elif text.endswith('/'): - text = text.strip('/') - # Try catch below avoid client crash when the parse method fails - try: - return parse(text, dayfirst=dayfirst, yearfirst=yearfirst, - ignoretz=True) - except Exception: - return datetime.datetime.now() + return parse(text, dayfirst=dayfirst, yearfirst=yearfirst, ignoretz=True) class Date(Gtk.Entry): diff --git a/tryton/tryton/gui/window/view_form/model/field.py b/tryton/tryton/gui/window/view_form/model/field.py index 4eaab989d8d..a5ea07d4af1 100644 --- a/tryton/tryton/gui/window/view_form/model/field.py +++ b/tryton/tryton/gui/window/view_form/model/field.py @@ -15,6 +15,7 @@ EvalEnvironment, RPCException, RPCExecute, concat, domain_inversion, eval_domain, extract_reference_models, filter_leaf, inverse_leaf, localize_domain, merge, prepare_reference_domain, simplify, unique_value) +from tryton.common.datetime_ import INVALID_DT_VALUE from tryton.common.htmltextbuffer import guess_decode from tryton.config import CONFIG from tryton.pyson import PYSONDecoder @@ -278,6 +279,12 @@ class DateTimeField(Field): _default = None + def get_eval(self, record): + value = super().get_eval(record) + if value is INVALID_DT_VALUE: + value = None + return value + def set_client(self, record, value, force_change=False): if isinstance(value, datetime.time): current_value = self.get_client(record) @@ -324,6 +331,12 @@ class DateField(Field): _default = None + def get_eval(self, record): + value = super().get_eval(record) + if value is INVALID_DT_VALUE: + value = None + return value + def validate(self, record, softvalidation=False, pre_validate=None): valid = super().validate(record, softvalidation, pre_validate) state_attrs = self.get_state_attrs(record) @@ -361,6 +374,12 @@ def set_client(self, record, value, force_change=False): def time_format(self, record): return record.expr_eval(self.attrs['format']) + def get_eval(self, record): + value = super().get_eval(record) + if value is INVALID_DT_VALUE: + value = None + return value + def validate(self, record, softvalidation=False, pre_validate=None): valid = super().validate(record, softvalidation, pre_validate) state_attrs = self.get_state_attrs(record) From 9ddace92955e42750bc45f49b5d0b8013b41ffcd Mon Sep 17 00:00:00 2001 From: Nicolas Evrard Date: Wed, 12 Jul 2023 18:00:09 +0200 Subject: [PATCH 3/5] Add DDMMYYYY date parsing --- tryton/tryton/common/datetime_.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tryton/tryton/common/datetime_.py b/tryton/tryton/common/datetime_.py index bb3966748e5..28046f3ba66 100644 --- a/tryton/tryton/common/datetime_.py +++ b/tryton/tryton/common/datetime_.py @@ -57,6 +57,8 @@ def date_parse(text, format_='%x'): except ValueError: monthfirst = False yearfirst = not dayfirst and not monthfirst + if len(text) == 8 and dayfirst: + return datetime.datetime.strptime(text, '%Y%m%d').date() return parse(text, dayfirst=dayfirst, yearfirst=yearfirst, ignoretz=True) From e7d0e6504f1b62edab6d12f441e29a4197746a20 Mon Sep 17 00:00:00 2001 From: Nicolas Evrard Date: Wed, 19 Jul 2023 14:01:42 +0200 Subject: [PATCH 4/5] Parse DDMMYYYY dates --- tryton/tryton/common/datetime_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tryton/tryton/common/datetime_.py b/tryton/tryton/common/datetime_.py index 28046f3ba66..cfe16cac384 100644 --- a/tryton/tryton/common/datetime_.py +++ b/tryton/tryton/common/datetime_.py @@ -58,7 +58,7 @@ def date_parse(text, format_='%x'): monthfirst = False yearfirst = not dayfirst and not monthfirst if len(text) == 8 and dayfirst: - return datetime.datetime.strptime(text, '%Y%m%d').date() + return datetime.datetime.strptime(text, '%d%m%Y').date() return parse(text, dayfirst=dayfirst, yearfirst=yearfirst, ignoretz=True) From 16eef231d54f6f239faa11f4e2ce29c8c4d753fd Mon Sep 17 00:00:00 2001 From: Nicolas Evrard Date: Tue, 8 Aug 2023 11:12:03 +0200 Subject: [PATCH 5/5] date_parse must return a datetime instance --- tryton/tryton/common/datetime_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tryton/tryton/common/datetime_.py b/tryton/tryton/common/datetime_.py index cfe16cac384..c56ebc56f8c 100644 --- a/tryton/tryton/common/datetime_.py +++ b/tryton/tryton/common/datetime_.py @@ -58,7 +58,7 @@ def date_parse(text, format_='%x'): monthfirst = False yearfirst = not dayfirst and not monthfirst if len(text) == 8 and dayfirst: - return datetime.datetime.strptime(text, '%d%m%Y').date() + return datetime.datetime.strptime(text, '%d%m%Y') return parse(text, dayfirst=dayfirst, yearfirst=yearfirst, ignoretz=True)