diff --git a/addons/account/models/account_fiscal_year.py b/addons/account/models/account_fiscal_year.py index ce031fb3f59e4..8e664c2ea7257 100644 --- a/addons/account/models/account_fiscal_year.py +++ b/addons/account/models/account_fiscal_year.py @@ -2,7 +2,6 @@ from odoo.exceptions import ValidationError from odoo import api, fields, models, _ -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT from datetime import datetime @@ -37,8 +36,8 @@ def _check_dates(self): ''' for fy in self: # Starting date must be prior to the ending date - date_from = datetime.strptime(fy.date_from, DEFAULT_SERVER_DATE_FORMAT) - date_to = datetime.strptime(fy.date_to, DEFAULT_SERVER_DATE_FORMAT) + date_from = fy.date_from + date_to = fy.date_to if date_to < date_from: raise ValidationError(_('The ending date must not be prior to the starting date.')) diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index fa235ee9d67fb..0ba6359b6af4a 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -10,7 +10,8 @@ from werkzeug.urls import url_encode from odoo import api, exceptions, fields, models, _ -from odoo.tools import email_re, email_split, email_escape_char, float_is_zero, float_compare, pycompat +from odoo.tools import email_re, email_split, email_escape_char, float_is_zero, float_compare, \ + pycompat, date_utils from odoo.tools.misc import formatLang from odoo.exceptions import AccessError, UserError, RedirectWarning, ValidationError, Warning @@ -216,7 +217,7 @@ def _get_payment_info_JSON(self): self.payments_widget = json.dumps(False) if self.payment_move_line_ids: info = {'title': _('Less Payment'), 'outstanding': False, 'content': self._get_payments_vals()} - self.payments_widget = json.dumps(info) + self.payments_widget = json.dumps(info, default=date_utils.json_default) @api.one @api.depends('move_id.line_ids.amount_residual') diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index 431be1b0aa81d..a8e229883d1c9 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -76,7 +76,7 @@ def build_graph_data(date, amount): """ self.env.cr.execute(query, (self.id, last_month, today)) for val in self.env.cr.dictfetchall(): - date = datetime.strptime(val['date'], DF) + date = val['date'] if val['date'] != today.strftime(DF): # make sure the last point in the graph is today data[:0] = [build_graph_data(date, amount)] amount -= val['amount'] @@ -92,7 +92,7 @@ def build_graph_data(date, amount): @api.multi def get_bar_graph_datas(self): data = [] - today = datetime.strptime(fields.Date.context_today(self), DF) + today = fields.Date.context_today(self) data.append({'label': _('Past'), 'value':0.0, 'type': 'past'}) day_of_week = int(format_datetime(today, 'e', locale=self._context.get('lang') or 'en_US')) first_day_of_week = today + timedelta(days=-day_of_week+1) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index bb8a29e0fb1a5..ca4e6d9d370f2 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import time +from datetime import date from collections import OrderedDict from odoo import api, fields, models, _ from odoo.osv import expression @@ -345,10 +346,10 @@ def _post_validate(self): @api.multi def _check_lock_date(self): for move in self: - lock_date = max(move.company_id.period_lock_date or '0000-00-00', move.company_id.fiscalyear_lock_date or '0000-00-00') + lock_date = max(move.company_id.period_lock_date or date.min, move.company_id.fiscalyear_lock_date or date.min) if self.user_has_groups('account.group_account_manager'): lock_date = move.company_id.fiscalyear_lock_date - if move.date <= (lock_date or '0000-00-00'): + if move.date <= (lock_date or date.min): if self.user_has_groups('account.group_account_manager'): message = _("You cannot add/modify entries prior to and inclusive of the lock date %s") % (lock_date) else: @@ -738,7 +739,7 @@ def check_full_reconcile(self): total_debit = 0 total_credit = 0 total_amount_currency = 0 - maxdate = '0000-00-00' + maxdate = date.min to_balance = {} for aml in amls: total_debit += aml.debit @@ -947,7 +948,7 @@ def compute_writeoff_counterpart_vals(values): total = 0 total_currency = 0 writeoff_lines = [] - date = time.strftime('%Y-%m-%d') + date = fields.Date.today() for vals in lines: # Check and complete vals if 'account_id' not in vals or 'journal_id' not in vals: @@ -955,7 +956,7 @@ def compute_writeoff_counterpart_vals(values): if ('debit' in vals) ^ ('credit' in vals): raise UserError(_("Either pass both debit and credit or none.")) if 'date' not in vals: - vals['date'] = self._context.get('date_p') or time.strftime('%Y-%m-%d') + vals['date'] = self._context.get('date_p') or fields.Date.today() if vals['date'] < date: date = vals['date'] if 'name' not in vals: @@ -1369,8 +1370,8 @@ class AccountPartialReconcile(models.Model): def _compute_max_date(self): for rec in self: rec.max_date = max( - fields.Datetime.from_string(rec.debit_move_id.date), - fields.Datetime.from_string(rec.credit_move_id.date) + rec.debit_move_id.date, + rec.credit_move_id.date ) @api.model @@ -1608,6 +1609,6 @@ def _prepare_exchange_diff_move(self, move_date, company): # The move date should be the maximum date between payment and invoice # (in case of payment in advance). However, we should make sure the # move date is not recorded after the end of year closing. - if move_date > (company.fiscalyear_lock_date or '0000-00-00'): + if move_date > (company.fiscalyear_lock_date or date.min): res['date'] = move_date return res diff --git a/addons/account/models/company.py b/addons/account/models/company.py index 89d9d9b4646be..0b5e879ce35f2 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -9,7 +9,7 @@ from odoo.exceptions import ValidationError, UserError, RedirectWarning from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT from odoo.tools.float_utils import float_round, float_is_zero -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, date_utils +from odoo.tools import date_utils class ResCompany(models.Model): @@ -110,16 +110,15 @@ def _check_lock_dates(self, vals): :param vals: The values passed to the write method. ''' period_lock_date = vals.get('period_lock_date') and\ - time.strptime(vals['period_lock_date'], DEFAULT_SERVER_DATE_FORMAT) + fields.Date.from_string(vals['period_lock_date']) fiscalyear_lock_date = vals.get('fiscalyear_lock_date') and\ - time.strptime(vals['fiscalyear_lock_date'], DEFAULT_SERVER_DATE_FORMAT) + fields.Date.from_string(vals['fiscalyear_lock_date']) - previous_month = datetime.strptime(fields.Date.today(), DEFAULT_SERVER_DATE_FORMAT) + relativedelta(months=-1) + previous_month = fields.Date.today() + relativedelta(months=-1) days_previous_month = calendar.monthrange(previous_month.year, previous_month.month) - previous_month = previous_month.replace(day=days_previous_month[1]).timetuple() + previous_month = previous_month.replace(day=days_previous_month[1]) for company in self: - old_fiscalyear_lock_date = company.fiscalyear_lock_date and\ - time.strptime(company.fiscalyear_lock_date, DEFAULT_SERVER_DATE_FORMAT) + old_fiscalyear_lock_date = company.fiscalyear_lock_date # The user attempts to remove the lock date for advisors if old_fiscalyear_lock_date and not fiscalyear_lock_date and 'fiscalyear_lock_date' in vals: @@ -143,7 +142,7 @@ def _check_lock_dates(self, vals): # In case of no new period lock date in vals, fallback to the one defined in the company if not period_lock_date: if company.period_lock_date: - period_lock_date = time.strptime(company.period_lock_date, DEFAULT_SERVER_DATE_FORMAT) + period_lock_date = company.period_lock_date else: continue @@ -182,8 +181,8 @@ def compute_fiscalyear_dates(self, current_date): ], limit=1) if fiscalyear: return { - 'date_from': datetime.strptime(fiscalyear.date_from, DEFAULT_SERVER_DATE_FORMAT).date(), - 'date_to': datetime.strptime(fiscalyear.date_to, DEFAULT_SERVER_DATE_FORMAT).date(), + 'date_from': fiscalyear.date_from, + 'date_to': fiscalyear.date_to, 'record': fiscalyear, } @@ -204,7 +203,7 @@ def compute_fiscalyear_dates(self, current_date): ('date_to', '>=', date_from_str), ], limit=1) if fiscalyear_from: - date_from = datetime.strptime(fiscalyear_from.date_to, DEFAULT_SERVER_DATE_FORMAT).date() + timedelta(days=1) + date_from = fiscalyear_from.date_to + timedelta(days=1) fiscalyear_to = self.env['account.fiscal.year'].search([ ('company_id', '=', self.id), @@ -212,7 +211,7 @@ def compute_fiscalyear_dates(self, current_date): ('date_to', '>=', date_to_str), ], limit=1) if fiscalyear_to: - date_to = datetime.strptime(fiscalyear_to.date_from, DEFAULT_SERVER_DATE_FORMAT).date() - timedelta(days=1) + date_to = fiscalyear_to.date_from - timedelta(days=1) return {'date_from': date_from, 'date_to': date_to} diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py index fc4a7f732ea68..88a6f4e3d88aa 100644 --- a/addons/account/report/account_aged_partner_balance.py +++ b/addons/account/report/account_aged_partner_balance.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import time -from odoo import api, models, _ +from odoo import api, fields, models, _ from odoo.exceptions import UserError from odoo.tools import float_is_zero from datetime import datetime @@ -19,7 +19,8 @@ def _get_partner_move_lines(self, account_type, date_from, target_move, period_l # The context key allow it to appear ctx = self._context periods = {} - start = datetime.strptime(date_from, "%Y-%m-%d") - relativedelta(days=1) + date_from = fields.Date.from_string(date_from) + start = date_from - relativedelta(days=1) for i in range(5)[::-1]: stop = start - relativedelta(days=period_length) periods[str(i)] = { @@ -220,7 +221,7 @@ def _get_report_values(self, docids, data=None): docs = self.env[model].browse(self.env.context.get('active_id')) target_move = data['form'].get('target_move', 'all') - date_from = data['form'].get('date_from', time.strftime('%Y-%m-%d')) + date_from = fields.Date.from_string(data['form'].get('date_from')) or fields.Date.today() if data['form']['result_selection'] == 'customer': account_type = ['receivable'] diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py index ccd2c41d64769..fa7f1206cca0e 100644 --- a/addons/account/report/account_partner_ledger.py +++ b/addons/account/report/account_partner_ledger.py @@ -4,7 +4,6 @@ import time from odoo import api, models, _ from odoo.exceptions import UserError -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT class ReportPartnerLedger(models.AbstractModel): @@ -35,7 +34,6 @@ def _lines(self, data, partner): lang_id = lang._lang_get(lang_code) date_format = lang_id.date_format for r in res: - r['date'] = datetime.strptime(r['date'], DEFAULT_SERVER_DATE_FORMAT).strftime(date_format) r['displayed_name'] = '-'.join( r[field_name] for field_name in ('move_name', 'ref', 'name') if r[field_name] not in (None, '', '/') diff --git a/addons/account/tests/test_account_fiscal_year.py b/addons/account/tests/test_account_fiscal_year.py index e1ce44c38fe07..85785473ee225 100644 --- a/addons/account/tests/test_account_fiscal_year.py +++ b/addons/account/tests/test_account_fiscal_year.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- from odoo.addons.account.tests.account_test_classes import AccountingTestCase -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT import odoo.tests +from odoo import fields from datetime import datetime @@ -19,10 +19,10 @@ def check_compute_fiscal_year(self, company, date, expected_date_from, expected_ :param expected_date_from: The expected date_from after computation. :param expected_date_to: The expected date_to after computation. ''' - current_date = datetime.strptime(date, DEFAULT_SERVER_DATE_FORMAT) + current_date = fields.Date.from_string(date) res = company.compute_fiscalyear_dates(current_date) - self.assertEqual(res['date_from'].strftime(DEFAULT_SERVER_DATE_FORMAT), expected_date_from) - self.assertEqual(res['date_to'].strftime(DEFAULT_SERVER_DATE_FORMAT), expected_date_to) + self.assertEqual(res['date_from'], fields.Date.from_string(expected_date_from)) + self.assertEqual(res['date_to'], fields.Date.from_string(expected_date_to)) def test_default_fiscal_year(self): '''Basic case with a fiscal year xxxx-01-01 - xxxx-12-31.''' diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index f16f08baac7d7..226108ffbba00 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -1,4 +1,4 @@ -from odoo import api +from odoo import api, fields from odoo.addons.account.tests.account_test_classes import AccountingTestCase from odoo.tests import tagged import time diff --git a/addons/account/wizard/account_report_aged_partner_balance.py b/addons/account/wizard/account_report_aged_partner_balance.py index c1f186b5abbca..178f1f80c380e 100644 --- a/addons/account/wizard/account_report_aged_partner_balance.py +++ b/addons/account/wizard/account_report_aged_partner_balance.py @@ -27,7 +27,7 @@ def _print_report(self, data): if not data['form']['date_from']: raise UserError(_('You must set a start date.')) - start = datetime.strptime(data['form']['date_from'], "%Y-%m-%d") + start = fields.Date.from_string(data['form']['date_from']) for i in range(5)[::-1]: stop = start - relativedelta(days=period_length - 1) diff --git a/addons/account_asset/models/account_asset.py b/addons/account_asset/models/account_asset.py index 36a4ee9ba25f1..ae0b345750854 100644 --- a/addons/account_asset/models/account_asset.py +++ b/addons/account_asset/models/account_asset.py @@ -7,7 +7,6 @@ from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF from odoo.tools import float_compare, float_is_zero @@ -164,7 +163,7 @@ def _compute_board_amount(self, sequence, residual_amount, amount_to_depr, undon if self.prorata: amount = amount_to_depr / self.method_number if sequence == 1: - date = datetime.strptime(self.date, DF) + date = self.date if self.method_period % 12 != 0: month_days = calendar.monthrange(date.year, date.month)[1] days = month_days - date.day + 1 @@ -176,7 +175,7 @@ def _compute_board_amount(self, sequence, residual_amount, amount_to_depr, undon amount = residual_amount * self.method_progress_factor if self.prorata: if sequence == 1: - date = datetime.strptime(self.date, DF) + date = self.date if self.method_period % 12 != 0: month_days = calendar.monthrange(date.year, date.month)[1] days = month_days - date.day + 1 @@ -189,7 +188,7 @@ def _compute_board_amount(self, sequence, residual_amount, amount_to_depr, undon def _compute_board_undone_dotation_nb(self, depreciation_date, total_days): undone_dotation_number = self.method_number if self.method_time == 'end': - end_date = datetime.strptime(self.method_end, DF).date() + end_date = self.method_end undone_dotation_number = 0 while depreciation_date <= end_date: depreciation_date = date(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+self.method_period) @@ -213,12 +212,11 @@ def compute_depreciation_board(self): # if we already have some previous validated entries, starting date is last entry + method period if posted_depreciation_line_ids and posted_depreciation_line_ids[-1].depreciation_date: - last_depreciation_date = datetime.strptime(posted_depreciation_line_ids[-1].depreciation_date, DF).date() + last_depreciation_date = fields.Date.from_string(posted_depreciation_line_ids[-1].depreciation_date) depreciation_date = last_depreciation_date + relativedelta(months=+self.method_period) else: # depreciation_date computed from the purchase date depreciation_date = self.date - depreciation_date = datetime.strptime(depreciation_date, DF) if self.date_first_depreciation == 'last_day_period': # depreciation_date = the last day of the month depreciation_date = depreciation_date + relativedelta(day=31) @@ -226,14 +224,12 @@ def compute_depreciation_board(self): if self.method_period == 12: depreciation_date = depreciation_date + relativedelta(month=self.company_id.fiscalyear_last_month) depreciation_date = depreciation_date + relativedelta(day=self.company_id.fiscalyear_last_day) - if datetime.strftime(depreciation_date, DF) < self.date: + if depreciation_date < self.date: depreciation_date = depreciation_date + relativedelta(years=1) elif self.first_depreciation_manual_date and self.first_depreciation_manual_date != self.date: # depreciation_date set manually from the 'first_depreciation_manual_date' field depreciation_date = self.first_depreciation_manual_date - depreciation_date = datetime.strptime(depreciation_date, DF) - depreciation_date = depreciation_date.date() total_days = (depreciation_date.year % 4) and 365 or 366 month_day = depreciation_date.day undone_dotation_number = self._compute_board_undone_dotation_nb(depreciation_date, total_days) @@ -252,7 +248,7 @@ def compute_depreciation_board(self): 'name': (self.code or '') + '/' + str(sequence), 'remaining_value': residual_amount, 'depreciated_value': self.value - (self.salvage_value + residual_amount), - 'depreciation_date': depreciation_date.strftime(DF), + 'depreciation_date': depreciation_date, } commands.append((0, False, vals)) @@ -327,7 +323,7 @@ def _get_disposal_moves(self): # Create a new depr. line with the residual amount and post it sequence = len(asset.depreciation_line_ids) - len(unposted_depreciation_line_ids) + 1 - today = datetime.today().strftime(DF) + today = fields.Datetime.today() vals = { 'amount': asset.value_residual, 'asset_id': asset.id, diff --git a/addons/account_asset/models/account_invoice.py b/addons/account_asset/models/account_invoice.py index c4b960079ec0f..c87bccf2291f6 100644 --- a/addons/account_asset/models/account_invoice.py +++ b/addons/account_asset/models/account_invoice.py @@ -5,7 +5,6 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF from odoo.addons import decimal_precision as dp @@ -65,10 +64,10 @@ def _get_asset_date(self): if self.invoice_id.type in ['out_invoice', 'out_refund']: self.asset_mrr = self.price_subtotal_signed / months if self.invoice_id.date_invoice: - start_date = datetime.strptime(self.invoice_id.date_invoice, DF).replace(day=1) + start_date = self.invoice_id.date_invoice.replace(day=1) end_date = (start_date + relativedelta(months=months, days=-1)) - self.asset_start_date = start_date.strftime(DF) - self.asset_end_date = end_date.strftime(DF) + self.asset_start_date = start_date + self.asset_end_date = end_date @api.one def asset_create(self): diff --git a/addons/account_budget/models/account_budget.py b/addons/account_budget/models/account_budget.py index 961c957c2d7aa..b2db529a2600f 100644 --- a/addons/account_budget/models/account_budget.py +++ b/addons/account_budget/models/account_budget.py @@ -205,22 +205,21 @@ def _compute_practical_amount(self): @api.multi def _compute_theoritical_amount(self): # beware: 'today' variable is mocked in the python tests and thus, its implementation matter - today = fields.Datetime.from_string(fields.Date.today()) + today = fields.Date.today() for line in self: if line.paid_date: - if fields.Datetime.to_string(today) <= line.paid_date: + if today <= line.paid_date: theo_amt = 0.00 else: theo_amt = line.planned_amount else: - line_timedelta = fields.Datetime.from_string(line.date_to) - fields.Datetime.from_string( - line.date_from) - elapsed_timedelta = today - fields.Datetime.from_string(line.date_from) + line_timedelta = line.date_to - line.date_from + elapsed_timedelta = today - line.date_from if elapsed_timedelta.days < 0: # If the budget line has not started yet, theoretical amount should be zero theo_amt = 0.00 - elif line_timedelta.days > 0 and today < fields.Datetime.from_string(line.date_to): + elif line_timedelta.days > 0 and today < line.date_to: # If today is between the budget line date_from and date_to theo_amt = (elapsed_timedelta.total_seconds() / line_timedelta.total_seconds()) * line.planned_amount else: @@ -265,14 +264,14 @@ def action_open_budget_entries(self): @api.one @api.constrains('date_from', 'date_to') def _line_dates_between_budget_dates(self): - budget_date_from = fields.Datetime.from_string(self.crossovered_budget_id.date_from) - budget_date_to = fields.Datetime.from_string(self.crossovered_budget_id.date_to) + budget_date_from = self.crossovered_budget_id.date_from + budget_date_to = self.crossovered_budget_id.date_to if self.date_from: - date_from = fields.Datetime.from_string(self.date_from) + date_from = self.date_from if date_from < budget_date_from or date_from > budget_date_to: raise ValidationError(_('"Start Date" of the budget line should be included in the Period of the budget')) if self.date_to: - date_to = fields.Datetime.from_string(self.date_to) + date_to = self.date_to if date_to < budget_date_from or date_to > budget_date_to: raise ValidationError(_('"End Date" of the budget line should be included in the Period of the budget')) diff --git a/addons/account_budget/tests/test_theoreticalamount.py b/addons/account_budget/tests/test_theoreticalamount.py index 082d39b9b14a0..c8341e92cf01d 100644 --- a/addons/account_budget/tests/test_theoreticalamount.py +++ b/addons/account_budget/tests/test_theoreticalamount.py @@ -35,29 +35,32 @@ def setUp(self): 'account_ids': [(4, account_rev.id, 0)], }) #create the budget and budget lines - first_january = datetime.now().replace(day=1, month=1) + first_january = Datetime.now().replace(day=1, month=1) self.last_day_of_budget = first_january + timedelta(days=364) # will be 30th of December or 31th in case of leap year + date_from = first_january.date() + date_to = self.last_day_of_budget.date() + crossovered_budget = self.env['crossovered.budget'].create({ 'name': 'test budget name', - 'date_from': str(first_january.date()), - 'date_to': str(self.last_day_of_budget.date()), + 'date_from': date_from, + 'date_to': date_to, }) crossovered_budget_line_obj = self.env['crossovered.budget.lines'] self.line = crossovered_budget_line_obj.create({ 'crossovered_budget_id': crossovered_budget.id, 'general_budget_id': buget_post.id, - 'date_from': str(first_january.date()), - 'date_to': str(self.last_day_of_budget.date()), + 'date_from': date_from, + 'date_to': date_to, 'planned_amount': -364, }) self.paid_date_line = crossovered_budget_line_obj.create({ 'crossovered_budget_id': crossovered_budget.id, 'general_budget_id': buget_post.id, - 'date_from': str(first_january.date()), - 'date_to': str(self.last_day_of_budget.date()), + 'date_from': date_from, + 'date_to': date_to, 'planned_amount': -364, - 'paid_date': str(datetime.now().year) + '-09-09', + 'paid_date': Date.today().replace(day=9, month=9), }) self.patcher = patch('odoo.addons.account_budget.models.account_budget.fields.Date', wraps=Date) @@ -74,7 +77,7 @@ def test_theoritical_amount_without_paid_date(self): ] for date, expected_amount in test_list: _logger.info("Checking theoritical amount for the date: " + date) - self.mock_date.today.return_value = date + self.mock_date.today.return_value = Date.from_string(date) self.assertAlmostEqual(self.line.theoritical_amount, expected_amount) #invalidate the cache of the budget lines to recompute the theoritical amount at next iteration self.line.invalidate_cache() @@ -84,13 +87,13 @@ def test_theoritical_amount_with_paid_date(self): (str(datetime.now().year) + '-01-01', 0), (str(datetime.now().year) + '-01-02', 0), (str(datetime.now().year) + '-09-08', 0), - (str(datetime.now().year) + '-09-09', -364), + (str(datetime.now().year) + '-09-09', 0), (str(datetime.now().year) + '-09-10', -364), (str(self.last_day_of_budget.date()), -364), ] for date, expected_amount in test_list: _logger.info("Checking theoritical amount for the date: " + date) - self.mock_date.today.return_value = date + self.mock_date.today.return_value = Date.from_string(date) self.assertAlmostEqual(self.paid_date_line.theoritical_amount, expected_amount) #invalidate the cache of the budget lines to recompute the theoritical amount at next iteration self.paid_date_line.invalidate_cache() diff --git a/addons/bus/models/bus.py b/addons/bus/models/bus.py index 39de41ec0a22e..cdeee00a313a5 100644 --- a/addons/bus/models/bus.py +++ b/addons/bus/models/bus.py @@ -10,6 +10,7 @@ import odoo from odoo import api, fields, models, SUPERUSER_ID from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT +from odoo.tools import date_utils _logger = logging.getLogger(__name__) @@ -20,7 +21,7 @@ # Bus #---------------------------------------------------------- def json_dump(v): - return json.dumps(v, separators=(',', ':')) + return json.dumps(v, separators=(',', ':'), default=date_utils.json_default) def hashable(key): if isinstance(key, list): diff --git a/addons/bus/models/bus_presence.py b/addons/bus/models/bus_presence.py index b9f4a3ea8c67b..c1da707b21562 100644 --- a/addons/bus/models/bus_presence.py +++ b/addons/bus/models/bus_presence.py @@ -43,11 +43,11 @@ def update(self, inactivity_period): # update the presence or a create a new one if not presence: # create a new presence for the user values['user_id'] = self._uid - values['last_presence'] = last_presence.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + values['last_presence'] = last_presence self.create(values) else: # update the last_presence if necessary, and write values - if datetime.datetime.strptime(presence.last_presence, DEFAULT_SERVER_DATETIME_FORMAT) < last_presence: - values['last_presence'] = last_presence.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + if presence.last_presence < last_presence: + values['last_presence'] = last_presence # Hide transaction serialization errors, which can be ignored, the presence update is not essential with tools.mute_logger('odoo.sql_db'): presence.write(values) diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index 49ab6f28fb696..556ea3578a2af 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -351,7 +351,7 @@ def do_check_alarm_for_one_date(self, one_date, event, event_maxdelta, in_the_ne @api.model def get_next_mail(self): - now = fields.Datetime.now() + now = fields.Datetime.to_string(fields.Datetime.now()) last_notif_mail = self.env['ir.config_parameter'].sudo().get_param('calendar.last_notif_mail', default=now) try: @@ -391,7 +391,7 @@ def get_next_mail(self): if at_least_one and not last_found: # if the precedent event had an alarm but not this one, we can stop the search for this event break else: - in_date_format = datetime.strptime(meeting.start, DEFAULT_SERVER_DATETIME_FORMAT) + in_date_format = meeting.start last_found = self.do_check_alarm_for_one_date(in_date_format, meeting, max_delta, 0, 'email', after=last_notif_mail, missing=True) for alert in last_found: self.do_mail_reminder(alert) @@ -873,9 +873,9 @@ def _compute_dates(self): """ for meeting in self: if meeting.allday: - meeting.start_date = meeting.start + meeting.start_date = meeting.start.date() meeting.start_datetime = False - meeting.stop_date = meeting.stop + meeting.stop_date = meeting.stop.date() meeting.stop_datetime = False meeting.duration = 0.0 @@ -897,13 +897,13 @@ def _inverse_dates(self): enddate = tz.localize(enddate) enddate = enddate.replace(hour=18) enddate = enddate.astimezone(pytz.utc) - meeting.stop = fields.Datetime.to_string(enddate) + meeting.stop = enddate startdate = fields.Datetime.from_string(meeting.start_date) startdate = tz.localize(startdate) # Add "+hh:mm" timezone startdate = startdate.replace(hour=8) # Set 8 AM in localtime startdate = startdate.astimezone(pytz.utc) # Convert to UTC - meeting.start = fields.Datetime.to_string(startdate) + meeting.start = startdate else: meeting.write({'start': meeting.start_datetime, 'stop': meeting.stop_datetime}) @@ -945,9 +945,9 @@ def _check_closing_date(self): @api.onchange('start_datetime', 'duration') def _onchange_duration(self): if self.start_datetime: - start = fields.Datetime.from_string(self.start_datetime) + start = self.start_datetime self.start = self.start_datetime - self.stop = fields.Datetime.to_string(start + timedelta(hours=self.duration)) + self.stop = start + timedelta(hours=self.duration) @api.onchange('start_date') def _onchange_start_date(self): @@ -971,9 +971,9 @@ def _get_ics_file(self): def ics_datetime(idate, allday=False): if idate: if allday: - return fields.Date.from_string(idate) + return idate else: - return fields.Datetime.from_string(idate).replace(tzinfo=pytz.timezone('UTC')) + return idate.replace(tzinfo=pytz.timezone('UTC')) return False try: @@ -989,7 +989,7 @@ def ics_datetime(idate, allday=False): if not meeting.start or not meeting.stop: raise UserError(_("First you have to specify the date of the invitation.")) - event.add('created').value = ics_datetime(time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)) + event.add('created').value = ics_datetime(fields.Datetime.now()) event.add('dtstart').value = ics_datetime(meeting.start, meeting.allday) event.add('dtend').value = ics_datetime(meeting.stop, meeting.allday) event.add('summary').value = meeting.name @@ -1245,7 +1245,8 @@ def get_month_string(freq): return '' def get_end_date(): - end_date_new = ''.join((re.compile('\d')).findall(self.final_date)) + 'T235959Z' if self.final_date else False + final_date = fields.Date.to_string(self.final_date) + end_date_new = ''.join((re.compile('\d')).findall(final_date)) + 'T235959Z' if final_date else False return (self.end_type == 'count' and (';COUNT=' + str(self.count)) or '') +\ ((end_date_new and self.end_type == 'end_date' and (';UNTIL=' + end_date_new)) or '') @@ -1379,6 +1380,7 @@ def detach_recurring_event(self, values=None): meeting_origin = self.browse(real_id) data = self.read(['allday', 'start', 'stop', 'rrule', 'duration'])[0] + d_class = fields.Date if data['allday'] else fields.Datetime if data.get('rrule'): data.update( values, @@ -1387,7 +1389,8 @@ def detach_recurring_event(self, values=None): rrule_type=False, rrule='', recurrency=False, - final_date=datetime.strptime(data.get('start'), DEFAULT_SERVER_DATETIME_FORMAT if data['allday'] else DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=values.get('duration', False) or data.get('duration')) + final_date=d_class.from_string(data.get('start')) \ + + timedelta(hours=values.get('duration', False) or data.get('duration')) ) # do not copy the id diff --git a/addons/calendar/tests/test_calendar.py b/addons/calendar/tests/test_calendar.py index 12547c8f695ac..fbf86ace9e1b8 100644 --- a/addons/calendar/tests/test_calendar.py +++ b/addons/calendar/tests/test_calendar.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import datetime -from datetime import datetime, timedelta +from datetime import datetime, timedelta, time from odoo import fields from odoo.tests.common import TransactionCase @@ -34,7 +34,7 @@ def test_calender_simple_event(self): }) self.assertEqual( - (m.start_datetime, m.stop_datetime), + (str(m.start_datetime), str(m.stop_datetime)), (u'2017-07-12 14:30:00', u'2017-07-12 15:00:00'), "Sanity check" ) @@ -102,8 +102,8 @@ def test_calender_event(self): # I create a recuring rule for my event calendar_event_sprint_review = self.CalendarEvent.create({ 'name': 'Begin of month meeting', - 'start': fields.Date.today() + ' 12:00:00', - 'stop': fields.Date.today() + ' 18:00:00', + 'start': datetime.combine(fields.Date.today(), time(12, 0)), + 'stop': datetime.combine(fields.Date.today(), time(18, 0)), 'recurrency': True, 'rrule': 'FREQ=MONTHLY;INTERVAL=1;COUNT=12;BYDAY=1MO' }) @@ -130,7 +130,7 @@ def test_validation_error(self): 'stop': '2017-07-12 15:00:00', }) self.assertEqual( - (m.start_datetime, m.stop_datetime), + (str(m.start_datetime), str(m.stop_datetime)), (u'2017-07-12 14:30:00', u'2017-07-12 15:00:00'), "Sanity check" ) @@ -154,11 +154,11 @@ def test_validation_error(self): records = m.detach_recurring_event(values) self.assertEqual( - (m.start_datetime, m.stop_datetime), - (u'2017-07-12 14:30:00', u'2017-07-12 15:00:00'), + (str(m.start_datetime), str(m.stop_datetime)), + ('2017-07-12 14:30:00', u'2017-07-12 15:00:00'), ) self.assertEquals( - (records.start_datetime, records.stop_datetime), + (str(records.start_datetime), str(records.stop_datetime)), (u'2017-07-10 15:30:00', u'2017-07-10 16:00:00'), ) @@ -243,7 +243,7 @@ def test_event_activity(self): self.assertEqual(test_record.activity_ids.summary, test_name) self.assertEqual(test_record.activity_ids.note, test_description) self.assertEqual(test_record.activity_ids.user_id, self.env.user) - self.assertEqual(test_record.activity_ids.date_deadline, fields.Date.to_string((now + timedelta(days=-1)).date())) + self.assertEqual(test_record.activity_ids.date_deadline, (now + timedelta(days=-1)).date()) # updating event should update activity test_event.write({ @@ -255,7 +255,7 @@ def test_event_activity(self): self.assertEqual(test_record.activity_ids.summary, '%s2' % test_name) self.assertEqual(test_record.activity_ids.note, test_description2) self.assertEqual(test_record.activity_ids.user_id, test_user) - self.assertEqual(test_record.activity_ids.date_deadline, fields.Date.to_string((now + timedelta(days=-2)).date())) + self.assertEqual(test_record.activity_ids.date_deadline, (now + timedelta(days=-2)).date()) # deleting meeting should delete its activity test_record.activity_ids.unlink() @@ -268,8 +268,8 @@ def test_event_activity(self): ).create({ 'name': test_name, 'description': test_description, - 'start': fields.Datetime.to_string(now + timedelta(days=-1)), - 'stop': fields.Datetime.to_string(now + timedelta(hours=2)), + 'start': now + timedelta(days=-1), + 'stop': now + timedelta(hours=2), 'user_id': self.env.user.id, }) self.assertEqual(test_event.res_model, test_record._name) diff --git a/addons/calendar_sms/models/calendar.py b/addons/calendar_sms/models/calendar.py index d22200b936323..986a46e521014 100644 --- a/addons/calendar_sms/models/calendar.py +++ b/addons/calendar_sms/models/calendar.py @@ -39,7 +39,7 @@ def get_next_mail(self): """ Cron method, overriden here to send SMS reminders as well """ result = super(AlarmManager, self).get_next_mail() - now = fields.Datetime.now() + now = fields.Datetime.to_string(fields.Datetime.now()) last_sms_cron = self.env['ir.config_parameter'].get_param('calendar_sms.last_sms_cron', default=now) cron = self.env['ir.model.data'].get_object('calendar', 'ir_cron_scheduler_alarm') diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index d1032e34b1d8f..35806bddb6fc9 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -1054,7 +1054,7 @@ def retrieve_sales_dashboard(self): meetings = self.env['calendar.event'].search(meetings_domain) for meeting in meetings: if meeting['start']: - start = datetime.strptime(meeting['start'], tools.DEFAULT_SERVER_DATETIME_FORMAT).date() + start = meeting['start'] if start == today: result['meeting']['today'] += 1 if today <= start <= today + timedelta(days=7): diff --git a/addons/event/models/event.py b/addons/event/models/event.py index 8902bbcaa4939..0ff913fb42b9e 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -347,7 +347,7 @@ class EventRegistration(models.Model): partner_id = fields.Many2one( 'res.partner', string='Contact', states={'done': [('readonly', True)]}) - date_open = fields.Datetime(string='Registration Date', readonly=True, default=lambda self: fields.datetime.now()) # weird crash is directly now + date_open = fields.Datetime(string='Registration Date', readonly=True, default=lambda self: fields.Datetime.now()) # weird crash is directly now date_closed = fields.Datetime(string='Attended Date', readonly=True) event_begin_date = fields.Datetime(string="Event Start Date", related='event_id.date_begin', readonly=True) event_end_date = fields.Datetime(string="Event End Date", related='event_id.date_end', readonly=True) @@ -515,8 +515,8 @@ def action_send_badge_email(self): @api.multi def get_date_range_str(self): self.ensure_one() - today = fields.Datetime.from_string(fields.Datetime.now()) - event_date = fields.Datetime.from_string(self.event_begin_date) + today = fields.Datetime.now() + event_date = self.event_begin_date diff = (event_date.date() - today.date()) if diff.days <= 0: return _('today') diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py index 680e1d4ba8a62..3c725af4dd151 100644 --- a/addons/event/models/event_mail.py +++ b/addons/event/models/event_mail.py @@ -97,7 +97,7 @@ def _compute_scheduled_date(self): else: date, sign = self.event_id.date_end, 1 - self.scheduled_date = datetime.strptime(date, tools.DEFAULT_SERVER_DATETIME_FORMAT) + _INTERVALS[self.interval_unit](sign * self.interval_nbr) + self.scheduled_date = date + _INTERVALS[self.interval_unit](sign * self.interval_nbr) @api.one def execute(self): @@ -190,7 +190,7 @@ def execute(self): def _compute_scheduled_date(self): if self.registration_id: date_open = self.registration_id.date_open - date_open_datetime = date_open and datetime.strptime(date_open, tools.DEFAULT_SERVER_DATETIME_FORMAT) or fields.datetime.now() + date_open_datetime = date_open or fields.Datetime.now() self.scheduled_date = date_open_datetime + _INTERVALS[self.scheduler_id.interval_unit](self.scheduler_id.interval_nbr) else: self.scheduled_date = False diff --git a/addons/event/tests/test_event_flow.py b/addons/event/tests/test_event_flow.py index 216a581538274..8d3d460462cab 100644 --- a/addons/event/tests/test_event_flow.py +++ b/addons/event/tests/test_event_flow.py @@ -120,7 +120,7 @@ def test_event_date_range(self): self.patcher = patch('odoo.addons.event.models.event.fields.Datetime', wraps=Datetime) self.mock_datetime = self.patcher.start() - self.mock_datetime.now.return_value = Datetime.to_string(datetime.datetime(2015, 12, 31, 12, 0)) + self.mock_datetime.now.return_value = datetime.datetime(2015, 12, 31, 12, 0) self.event_0.registration_ids.event_begin_date = datetime.datetime(2015, 12, 31, 18, 0) self.assertEqual(self.event_0.registration_ids.get_date_range_str(), u'today') @@ -131,7 +131,7 @@ def test_event_date_range(self): self.event_0.registration_ids.event_begin_date = datetime.datetime(2016, 1, 2, 6, 0) self.assertEqual(self.event_0.registration_ids.get_date_range_str(), u'in 2 days') - self.mock_datetime.now.return_value = Datetime.to_string(datetime.datetime(2015, 12, 10, 12, 0)) + self.mock_datetime.now.return_value = datetime.datetime(2015, 12, 10, 12, 0) self.event_0.registration_ids.event_begin_date = datetime.datetime(2016, 1, 25, 6, 0) self.assertEqual(self.event_0.registration_ids.get_date_range_str(), u'next month') diff --git a/addons/event/tests/test_mail_schedule.py b/addons/event/tests/test_mail_schedule.py index 010e5aa87a5d0..0e8f665f1d81a 100644 --- a/addons/event/tests/test_mail_schedule.py +++ b/addons/event/tests/test_mail_schedule.py @@ -13,7 +13,7 @@ class TestMailSchedule(TestEventCommon): @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') def test_00_event_mail_schedule(self): """ Test mail scheduling for events """ - now = fields.datetime.now() + now = fields.Datetime.now() event_date_begin = now + relativedelta(days=1) event_date_end = now + relativedelta(days=3) @@ -56,7 +56,7 @@ def test_00_event_mail_schedule(self): # verify that subscription scheduler was auto-executed after each registration self.assertEqual(len(schedulers[0].mail_registration_ids), 2, 'event: incorrect number of mail scheduled date') - mails = self.env['mail.mail'].search([('subject', 'ilike', 'registration'), ('date', '>=', datetime.datetime.strftime(now, tools.DEFAULT_SERVER_DATETIME_FORMAT))], order='date DESC', limit=3) + mails = self.env['mail.mail'].search([('subject', 'ilike', 'registration'), ('date', '>=', now)], order='date DESC', limit=3) self.assertEqual(len(mails), 2, 'event: wrong number of registration mail sent') for registration in schedulers[0].mail_registration_ids: @@ -65,7 +65,7 @@ def test_00_event_mail_schedule(self): # check before event scheduler schedulers = self.EventMail.search([('event_id', '=', test_event.id), ('interval_type', '=', 'before_event')]) self.assertEqual(len(schedulers), 1, 'event: wrong scheduler creation') - self.assertEqual(schedulers[0].scheduled_date, datetime.datetime.strftime(event_date_begin + relativedelta(days=-1), tools.DEFAULT_SERVER_DATETIME_FORMAT), 'event: incorrect scheduled date') + self.assertEqual(schedulers[0].scheduled_date, event_date_begin + relativedelta(days=-1), 'event: incorrect scheduled date') # execute event reminder scheduler explicitly schedulers[0].execute() @@ -73,7 +73,7 @@ def test_00_event_mail_schedule(self): self.assertTrue(schedulers[0].mail_sent, 'event: reminder scheduler should have sent an email') self.assertTrue(schedulers[0].done, 'event: reminder scheduler should be done') - mails = self.env['mail.mail'].search([('subject', 'ilike', 'TestEventMail'), ('date', '>=', datetime.datetime.strftime(now, tools.DEFAULT_SERVER_DATETIME_FORMAT))], order='date DESC', limit=3) + mails = self.env['mail.mail'].search([('subject', 'ilike', 'TestEventMail'), ('date', '>=', now)], order='date DESC', limit=3) self.assertEqual(len(mails), 3, 'event: wrong number of reminders in outgoing mail queue') diff --git a/addons/fleet/models/fleet_vehicle.py b/addons/fleet/models/fleet_vehicle.py index 6f19be439fc6d..609ecbe6e4f43 100644 --- a/addons/fleet/models/fleet_vehicle.py +++ b/addons/fleet/models/fleet_vehicle.py @@ -299,9 +299,9 @@ def _compute_vehicle_log_name(self): for record in self: name = record.vehicle_id.name if not name: - name = record.date + name = str(record.date) elif record.date: - name += ' / ' + record.date + name += ' / ' + str(record.date) record.name = name @api.onchange('vehicle_id') diff --git a/addons/fleet/models/fleet_vehicle_cost.py b/addons/fleet/models/fleet_vehicle_cost.py index 95d7b5fc9cadd..f066286c38840 100644 --- a/addons/fleet/models/fleet_vehicle_cost.py +++ b/addons/fleet/models/fleet_vehicle_cost.py @@ -145,7 +145,7 @@ def _compute_contract_name(self): if record.cost_subtype_id.name: name += ' / ' + record.cost_subtype_id.name if record.date: - name += ' / ' + record.date + name += ' / ' + str(record.date) record.name = name @api.depends('expiration_date', 'state') @@ -231,7 +231,7 @@ def scheduler_manage_auto_costs(self): if not contract.start_date or contract.cost_frequency == 'no': continue found = False - last_cost_date = contract.start_date + startdate = contract.start_date if contract.generated_cost_ids: last_autogenerated_cost = VehicleCost.search([ ('contract_id', '=', contract.id), @@ -239,12 +239,11 @@ def scheduler_manage_auto_costs(self): ], offset=0, limit=1, order='date desc') if last_autogenerated_cost: found = True - last_cost_date = last_autogenerated_cost.date - startdate = fields.Date.from_string(last_cost_date) + startdate = last_autogenerated_cost.date.date() if found: startdate += deltas.get(contract.cost_frequency) - today = fields.Date.from_string(fields.Date.context_today(self)) - while (startdate <= today) & (startdate <= fields.Date.from_string(contract.expiration_date)): + today = fields.Date.context_today(self) + while (startdate <= today) & (startdate <= contract.expiration_date): data = { 'amount': contract.cost_generated, 'date': fields.Date.context_today(self), diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 93cd77a22cddc..48868407f249c 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -141,11 +141,11 @@ def _get_next_report_date(self): report period. """ for challenge in self: - last = fields.Datetime.from_string(challenge.last_report_date).date() + last = challenge.last_report_date offset = self.REPORT_OFFSETS.get(challenge.report_message_frequency) if offset: - challenge.next_report_date = fields.Date.to_string(last + offset) + challenge.next_report_date = last + offset else: challenge.next_report_date = False diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py index a422438778ce5..b02f7586e75c2 100644 --- a/addons/google_calendar/models/google_calendar.py +++ b/addons/google_calendar/models/google_calendar.py @@ -193,12 +193,12 @@ class GoogleCalendar(models.AbstractModel): def generate_data(self, event, isCreating=False): if event.allday: start_date = event.start_date - final_date = (datetime.strptime(event.stop_date, tools.DEFAULT_SERVER_DATE_FORMAT) + timedelta(days=1)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT) + final_date = event.stop_date + timedelta(days=1) type = 'date' vstype = 'dateTime' else: - start_date = fields.Datetime.context_timestamp(self, fields.Datetime.from_string(event.start)).isoformat('T') - final_date = fields.Datetime.context_timestamp(self, fields.Datetime.from_string(event.stop)).isoformat('T') + start_date = fields.Datetime.context_timestamp(self, event.start).isoformat('T') + final_date = fields.Datetime.context_timestamp(self, event.stop).isoformat('T') type = 'dateTime' vstype = 'date' attendee_list = [] @@ -852,9 +852,9 @@ def update_events(self, lastSync=False): return True def check_and_sync(self, oe_event, google_event): - if datetime.strptime(oe_event.oe_update_date, "%Y-%m-%d %H:%M:%S.%f") > datetime.strptime(google_event['updated'], "%Y-%m-%dT%H:%M:%S.%fz"): + if oe_event.oe_update_date > datetime.strptime(google_event['updated'], "%Y-%m-%dT%H:%M:%S.%fz"): self.update_to_google(oe_event, google_event) - elif datetime.strptime(oe_event.oe_update_date, "%Y-%m-%d %H:%M:%S.%f") < datetime.strptime(google_event['updated'], "%Y-%m-%dT%H:%M:%S.%fz"): + elif oe_event.oe_update_date < datetime.strptime(google_event['updated'], "%Y-%m-%dT%H:%M:%S.%fz"): self.update_from_google(oe_event, google_event, 'write') def get_sequence(self, instance_id): diff --git a/addons/hr_attendance/models/hr_attendance.py b/addons/hr_attendance/models/hr_attendance.py index 7a88460c20a60..cadccfcf6d0e0 100644 --- a/addons/hr_attendance/models/hr_attendance.py +++ b/addons/hr_attendance/models/hr_attendance.py @@ -1,10 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime - from odoo import models, fields, api, exceptions, _ -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT class HrAttendance(models.Model): @@ -43,8 +40,7 @@ def name_get(self): def _compute_worked_hours(self): for attendance in self: if attendance.check_out: - delta = datetime.strptime(attendance.check_out, DEFAULT_SERVER_DATETIME_FORMAT) - datetime.strptime( - attendance.check_in, DEFAULT_SERVER_DATETIME_FORMAT) + delta = attendance.check_out - attendance.check_in attendance.worked_hours = delta.total_seconds() / 3600.0 @api.constrains('check_in', 'check_out') diff --git a/addons/hr_contract/tests/test_auto_status.py b/addons/hr_contract/tests/test_auto_status.py index fb755bdb82f63..efb7de51f9ae0 100644 --- a/addons/hr_contract/tests/test_auto_status.py +++ b/addons/hr_contract/tests/test_auto_status.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime +from datetime import date, datetime from odoo.tests.common import TransactionCase from dateutil.relativedelta import relativedelta @@ -37,13 +37,13 @@ def test_contract_enddate(self): self.assertEquals(self.contract.state, 'close') def test_contract_pending_visa_expire(self): - self.employee.visa_expire = datetime.now() + relativedelta(days=30) + self.employee.visa_expire = date.today() + relativedelta(days=30) self.test_contract.update(dict(date_end=False)) self.contract = self.contracts.create(self.test_contract) self.apply_cron() self.assertEquals(self.contract.state, 'pending') - self.employee.visa_expire = datetime.now() + relativedelta(days=-5) + self.employee.visa_expire = date.today() + relativedelta(days=-5) self.test_contract.update({ 'date_start': datetime.now() + relativedelta(days=-50), 'state': 'pending', diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 523f2b5a6952b..c2e2e2df44f07 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -376,12 +376,12 @@ def add_follower(self, employee_id): def _check_leave_type_validity(self): for leave in self: if leave.holiday_status_id.validity_start and leave.holiday_status_id.validity_stop: - vstart = fields.Datetime.from_string(leave.holiday_status_id.validity_start) - vstop = fields.Datetime.from_string(leave.holiday_status_id.validity_stop) - dfrom = fields.Datetime.from_string(leave.date_from) - dto = fields.Datetime.from_string(leave.date_to) + vstart = leave.holiday_status_id.validity_start + vstop = leave.holiday_status_id.validity_stop + dfrom = leave.date_from + dto = leave.date_to - if dfrom and dto and (dfrom < vstart or dto > vstop): + if dfrom and dto and (dfrom.date() < vstart or dto.date() > vstop): raise UserError(_('You can take %s only between %s and %s') % (leave.holiday_status_id.display_name, \ leave.holiday_status_id.validity_start, leave.holiday_status_id.validity_stop)) diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index 1f5274dda95bc..297a510030a50 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -288,9 +288,9 @@ def add_follower(self, employee_id): def _check_leave_type_validity(self): for allocation in self: if allocation.holiday_status_id.validity_start and allocation.holiday_status_id.validity_stop: - vstart = fields.Datetime.from_string(allocation.holiday_status_id.validity_start) - vstop = fields.Datetime.from_string(allocation.holiday_status_id.validity_stop) - today = fields.Datetime.from_string(fields.Datetime.now()) + vstart = allocation.holiday_status_id.validity_start + vstop = allocation.holiday_status_id.validity_stop + today = fields.Date.today() if vstart > today or vstop < today: raise UserError(_('You can allocate %s only between %s and %s') % (allocation.holiday_status_id.display_name, diff --git a/addons/hr_holidays/tests/test_accrual_allocations.py b/addons/hr_holidays/tests/test_accrual_allocations.py index 3a3d1150f3adc..8250f88f0a75b 100644 --- a/addons/hr_holidays/tests/test_accrual_allocations.py +++ b/addons/hr_holidays/tests/test_accrual_allocations.py @@ -90,7 +90,7 @@ def test_accrual_base_leaves(self): employee = self.env['hr.employee'].browse(self.employee_hruser_id) # Getting the previous work date - df = employee.resource_calendar_id.plan_days(-2, fields.Datetime.from_string(fields.Datetime.now())).date() + df = employee.resource_calendar_id.plan_days(-2, fields.Datetime.now()).date() leave_0 = Leave.create({ 'name': 'Leave for hruser', @@ -173,7 +173,7 @@ def test_accrual_new_employee(self): """ Allocation = self.env['hr.leave.allocation'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True) - self.set_employee_create_date(self.employee_emp_id, fields.Datetime.now()) + self.set_employee_create_date(self.employee_emp_id, fields.Datetime.to_string(fields.Datetime.now())) alloc_0 = Allocation.create({ 'name': 'one shot one kill', diff --git a/addons/hr_recruitment/models/hr_recruitment.py b/addons/hr_recruitment/models/hr_recruitment.py index 67ca1548bfd04..6149bc159f704 100644 --- a/addons/hr_recruitment/models/hr_recruitment.py +++ b/addons/hr_recruitment/models/hr_recruitment.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime - -from odoo import api, fields, models, tools, SUPERUSER_ID +from odoo import api, fields, models, SUPERUSER_ID from odoo.tools.translate import _ from odoo.exceptions import UserError @@ -176,13 +174,13 @@ def _default_company_id(self): @api.one def _compute_day(self): if self.date_open: - date_create = datetime.strptime(self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT) - date_open = datetime.strptime(self.date_open, tools.DEFAULT_SERVER_DATETIME_FORMAT) + date_create = self.create_date + date_open = self.date_open self.day_open = (date_open - date_create).total_seconds() / (24.0 * 3600) if self.date_closed: - date_create = datetime.strptime(self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT) - date_closed = datetime.strptime(self.date_closed, tools.DEFAULT_SERVER_DATETIME_FORMAT) + date_create = self.create_date + date_closed = self.date_closed self.day_close = (date_closed - date_create).total_seconds() / (24.0 * 3600) self.delay_close = self.day_close - self.day_open diff --git a/addons/l10n_be_hr_payroll_fleet/models/fleet.py b/addons/l10n_be_hr_payroll_fleet/models/fleet.py index 4605584cb0dc3..c72f10117baf8 100644 --- a/addons/l10n_be_hr_payroll_fleet/models/fleet.py +++ b/addons/l10n_be_hr_payroll_fleet/models/fleet.py @@ -5,7 +5,7 @@ from odoo import api, fields, models -from odoo.fields import Datetime +from odoo.fields import Datetime, Date class FleetVehicle(models.Model): @@ -80,17 +80,18 @@ def create(self, vals): def _get_acquisition_date(self): self.ensure_one() return babel.dates.format_date( - date=Datetime.from_string(self.acquisition_date), + date=self.acquisition_date, format='MMMM y', locale=self._context.get('lang') or 'en_US' ) def _get_car_atn(self, acquisition_date, car_value, fuel_type, co2): # Compute the correction coefficient from the age of the car - now = Datetime.from_string(Datetime.now()) - start = Datetime.from_string(acquisition_date) - if start: - number_of_month = (now.year - start.year) * 12.0 + now.month - start.month + int(bool(now.day - start.day + 1)) + now = Date.today() + if acquisition_date: + number_of_month = ((now.year - acquisition_date.year) * 12.0 + now.month - + acquisition_date.month + + int(bool(now.day - acquisition_date.day + 1))) if number_of_month <= 12: age_coefficient = 1.00 elif number_of_month <= 24: diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index bd8e4cc44c0e6..b18518cffe0cb 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -5,11 +5,10 @@ import base64 import io -from datetime import datetime from odoo import api, fields, models, _ from odoo.exceptions import Warning -from odoo.tools import pycompat, DEFAULT_SERVER_DATE_FORMAT +from odoo.tools import pycompat class AccountFrFec(models.TransientModel): @@ -68,7 +67,7 @@ def do_query_unaffected_earnings(self): ''' company = self.env.user.company_id formatted_date_from = self.date_from.replace('-', '') - date_from = datetime.strptime(self.date_from, DEFAULT_SERVER_DATE_FORMAT) + date_from = self.date_from formatted_date_year = date_from.year self._cr.execute( sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) @@ -174,7 +173,7 @@ def generate_fec(self): AND aat.type not in ('receivable', 'payable') ''' formatted_date_from = self.date_from.replace('-', '') - date_from = datetime.strptime(self.date_from, DEFAULT_SERVER_DATE_FORMAT) + date_from = self.date_from formatted_date_year = date_from.year self._cr.execute( sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) diff --git a/addons/l10n_in_hr_payroll/report/report_payroll_advice.py b/addons/l10n_in_hr_payroll/report/report_payroll_advice.py index 0a29469440448..51bc88994ede0 100644 --- a/addons/l10n_in_hr_payroll/report/report_payroll_advice.py +++ b/addons/l10n_in_hr_payroll/report/report_payroll_advice.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import time -from datetime import datetime from odoo import api, models @@ -16,8 +15,8 @@ def get_month(self, input_date): } slip = self.env['hr.payslip'].search([('date_from', '<=', input_date), ('date_to', '>=', input_date)], limit=1) if slip: - from_date = datetime.strptime(slip.date_from, '%Y-%m-%d') - to_date = datetime.strptime(slip.date_to, '%Y-%m-%d') + from_date = slip.date_from + to_date = slip.date_to res['from_name'] = from_date.strftime('%d') + '-' + from_date.strftime('%B') + '-' + from_date.strftime('%Y') res['to_name'] = to_date.strftime('%d') + '-' + to_date.strftime('%B') + '-' + to_date.strftime('%Y') return res diff --git a/addons/lunch/models/lunch.py b/addons/lunch/models/lunch.py index 0108165c2b040..4decde206fda6 100644 --- a/addons/lunch/models/lunch.py +++ b/addons/lunch/models/lunch.py @@ -123,9 +123,9 @@ def _check_date(self): """ Prevents the user to create an order in the past """ - date_order = datetime.datetime.strptime(self.date, '%Y-%m-%d') - date_today = datetime.datetime.strptime(fields.Date.context_today(self), '%Y-%m-%d') - if (date_order < date_today): + date_order = self.date + date_today = fields.Date.context_today(self) + if date_order < date_today: raise ValidationError(_('The date of your order is in the past.')) @api.one diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index dbb56e2d132e5..72263a134c0d5 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -172,7 +172,9 @@ def _compute_state(self): def _onchange_activity_type_id(self): if self.activity_type_id: self.summary = self.activity_type_id.summary - self.date_deadline = (datetime.now() + timedelta(days=self.activity_type_id.days)) + # Date.context_today is correct because date_deadline is a Date and is meant to be + # expressed in user TZ + self.date_deadline = fields.Date.context_today(self) + timedelta(days=self.activity_type_id.days) @api.onchange('recommended_activity_type_id') def _onchange_recommended_activity_type_id(self): diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index 97d141af0c76c..2aa38083d25b7 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -30,7 +30,7 @@ def format_date(env, date, pattern=False): def format_tz(env, dt, tz=False, format=False): record_user_timestamp = env.user.sudo().with_context(tz=tz or env.user.sudo().tz or 'UTC') - timestamp = datetime.datetime.strptime(dt, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timestamp = fields.Datetime.from_string(dt) ts = fields.Datetime.context_timestamp(record_user_timestamp, timestamp) diff --git a/addons/mail/models/mail_tracking_value.py b/addons/mail/models/mail_tracking_value.py index 5e758c45482e4..874ce4fde9a35 100644 --- a/addons/mail/models/mail_tracking_value.py +++ b/addons/mail/models/mail_tracking_value.py @@ -46,8 +46,8 @@ def create_tracking_values(self, initial_value, new_value, col_name, col_info, t }) elif col_info['type'] == 'date': values.update({ - 'old_value_datetime': initial_value and datetime.strftime(datetime.combine(datetime.strptime(initial_value, tools.DEFAULT_SERVER_DATE_FORMAT), datetime.min.time()), tools.DEFAULT_SERVER_DATETIME_FORMAT) or False, - 'new_value_datetime': new_value and datetime.strftime(datetime.combine(datetime.strptime(new_value, tools.DEFAULT_SERVER_DATE_FORMAT), datetime.min.time()), tools.DEFAULT_SERVER_DATETIME_FORMAT) or False, + 'old_value_datetime': initial_value and fields.Datetime.to_string(datetime.combine(fields.Date.from_string(initial_value), datetime.min.time())) or False, + 'new_value_datetime': new_value and fields.Datetime.to_string(datetime.combine(fields.Date.from_string(new_value), datetime.min.time())) or False, }) elif col_info['type'] == 'boolean': values.update({ @@ -88,8 +88,8 @@ def get_display_value(self, type): result.append(record['%s_value_datetime' % type]) elif record.field_type == 'date': if record['%s_value_datetime' % type]: - new_date = datetime.strptime(record['%s_value_datetime' % type], tools.DEFAULT_SERVER_DATETIME_FORMAT).date() - result.append(new_date.strftime(tools.DEFAULT_SERVER_DATE_FORMAT)) + new_date = record['%s_value_datetime' % type] + result.append(fields.Date.to_string(new_date)) else: result.append(record['%s_value_datetime' % type]) elif record.field_type == 'boolean': diff --git a/addons/membership/models/account_invoice.py b/addons/membership/models/account_invoice.py index 85b158e495944..66e4942649f90 100644 --- a/addons/membership/models/account_invoice.py +++ b/addons/membership/models/account_invoice.py @@ -3,6 +3,8 @@ from odoo import api, fields, models +from datetime import date + class Invoice(models.Model): _inherit = 'account.invoice' @@ -36,7 +38,8 @@ def write(self, vals): # Product line has changed to a membership product date_from = line.product_id.membership_date_from date_to = line.product_id.membership_date_to - if line.invoice_id.date_invoice > (date_from or '0000-00-00') and line.invoice_id.date_invoice < (date_to or '0000-00-00'): + if (line.invoice_id.date_invoice > (date_from or date.min) and + line.invoice_id.date_invoice < (date_to or date.min)): date_from = line.invoice_id.date_invoice MemberLine.create({ 'partner': line.invoice_id.partner_id.id, @@ -62,7 +65,10 @@ def create(self, vals): # Product line is a membership product date_from = invoice_line.product_id.membership_date_from date_to = invoice_line.product_id.membership_date_to - if date_from and date_from < (invoice_line.invoice_id.date_invoice or '0000-00-00') < (date_to or '0000-00-00'): + if (date_from and + date_from < + (invoice_line.invoice_id.date_invoice or date.min) < + (date_to or date.min)): date_from = invoice_line.invoice_id.date_invoice MemberLine.create({ 'partner': invoice_line.invoice_id.partner_id.id, diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 415014b6ba0fb..c3fc2a45ce3ca 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -59,7 +59,7 @@ def test_basic(self): }) inventory.action_validate() - test_date_planned = datetime.now() - timedelta(days=1) + test_date_planned = Dt.now() - timedelta(days=1) test_quantity = 2.0 self.bom_1.routing_id = False man_order = self.env['mrp.production'].sudo(self.user_mrp_user).create({ @@ -76,7 +76,7 @@ def test_basic(self): # check production move production_move = man_order.move_finished_ids - self.assertEqual(production_move.date, Dt.to_string(test_date_planned)) + self.assertEqual(production_move.date, test_date_planned) self.assertEqual(production_move.product_id, self.product_4) self.assertEqual(production_move.product_uom, man_order.product_uom_id) self.assertEqual(production_move.product_qty, man_order.product_qty) @@ -85,7 +85,7 @@ def test_basic(self): # check consumption moves for move in man_order.move_raw_ids: - self.assertEqual(move.date, Dt.to_string(test_date_planned)) + self.assertEqual(move.date, test_date_planned) first_move = man_order.move_raw_ids.filtered(lambda move: move.product_id == self.product_2) self.assertEqual(first_move.product_qty, test_quantity / self.bom_1.product_qty * self.product_4.uom_id.factor_inv * 2) first_move = man_order.move_raw_ids.filtered(lambda move: move.product_id == self.product_1) diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 7d65a44b4026e..21f70997ea947 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -6,7 +6,6 @@ from odoo import api, fields, models, _ from odoo.exceptions import ValidationError -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT class AccountCashboxLine(models.Model): @@ -209,7 +208,7 @@ def _compute_current_session_user(self): pos_config.pos_session_username = session[0].user_id.name pos_config.pos_session_state = session[0].state pos_config.pos_session_duration = ( - datetime.now() - datetime.strptime(session[0].start_at, DATETIME_FORMAT) + datetime.now() - session[0].start_at ).days if session[0].start_at else 0 else: pos_config.pos_session_username = False diff --git a/addons/point_of_sale/wizard/pos_payment.py b/addons/point_of_sale/wizard/pos_payment.py index e4b494baddea6..2b8a7eeb9a869 100644 --- a/addons/point_of_sale/wizard/pos_payment.py +++ b/addons/point_of_sale/wizard/pos_payment.py @@ -32,7 +32,7 @@ def _default_amount(self): journal_id = fields.Many2one('account.journal', string='Payment Mode', required=True, default=_default_journal) amount = fields.Float(digits=(16, 2), required=True, default=_default_amount) payment_name = fields.Char(string='Payment Reference') - payment_date = fields.Date(string='Payment Date', required=True, default=lambda *a: fields.Datetime.now()) + payment_date = fields.Date(string='Payment Date', required=True, default=lambda *a: fields.Date.today()) @api.onchange('session_id') def _on_change_session(self): diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 36b8aa3aa8ec1..30969fa29cdb7 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -171,7 +171,7 @@ def copy(self, default=None): for line in new_po.order_line: seller = line.product_id._select_seller( partner_id=line.partner_id, quantity=line.product_qty, - date=line.order_id.date_order and line.order_id.date_order[:10], uom_id=line.product_uom) + date=line.order_id.date_order and line.order_id.date_order.date(), uom_id=line.product_uom) line.date_planned = line._get_date_planned(seller) return new_po @@ -526,7 +526,7 @@ def _get_date_planned(self, seller, po=False): """ date_order = po.date_order if po else self.order_id.date_order if date_order: - return datetime.strptime(date_order, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=seller.delay if seller else 0) + return date_order + relativedelta(days=seller.delay if seller else 0) else: return datetime.today() + relativedelta(days=seller.delay if seller else 0) @@ -590,7 +590,7 @@ def _onchange_quantity(self): seller = self.product_id._select_seller( partner_id=self.partner_id, quantity=self.product_qty, - date=self.order_id.date_order and self.order_id.date_order[:10], + date=self.order_id.date_order and self.order_id.date_order.date(), uom_id=self.product_uom, params=params) diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py index 0e96a3de9f707..d9b969ec5b91f 100644 --- a/addons/purchase_requisition/models/purchase_requisition.py +++ b/addons/purchase_requisition/models/purchase_requisition.py @@ -280,7 +280,7 @@ def _prepare_purchase_order_line(self, name, product_qty=0.0, price_unit=0.0, ta 'product_qty': product_qty, 'price_unit': price_unit, 'taxes_id': [(6, 0, taxes_ids)], - 'date_planned': requisition.schedule_date or fields.Date.today(), + 'date_planned': requisition.schedule_date or fields.Datetime.now(), 'account_analytic_id': self.account_analytic_id.id, 'analytic_tag_ids': self.analytic_tag_ids.ids, 'move_dest_ids': self.move_dest_id and [(4, self.move_dest_id.id)] or [] diff --git a/addons/purchase_stock/models/stock_rule.py b/addons/purchase_stock/models/stock_rule.py index b89361f7b5d68..1419f5566f883 100644 --- a/addons/purchase_stock/models/stock_rule.py +++ b/addons/purchase_stock/models/stock_rule.py @@ -98,7 +98,7 @@ def _update_purchase_order_line(self, product_id, product_qty, product_uom, valu seller = product_id._select_seller( partner_id=partner, quantity=line.product_qty + procurement_uom_po_qty, - date=line.order_id.date_order and line.order_id.date_order[:10], + date=line.order_id.date_order and line.order_id.date_order.date(), uom_id=product_id.uom_po_id) price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, line.product_id.supplier_taxes_id, line.taxes_id, values['company_id']) if seller else 0.0 @@ -118,7 +118,7 @@ def _prepare_purchase_order_line(self, product_id, product_qty, product_uom, val seller = product_id._select_seller( partner_id=partner, quantity=procurement_uom_po_qty, - date=po.date_order and po.date_order[:10], + date=po.date_order and po.date_order.date(), uom_id=product_id.uom_po_id) taxes = product_id.supplier_taxes_id diff --git a/addons/purchase_stock/tests/test_purchase_lead_time.py b/addons/purchase_stock/tests/test_purchase_lead_time.py index f251cf4984d20..c7bad00f5abc6 100644 --- a/addons/purchase_stock/tests/test_purchase_lead_time.py +++ b/addons/purchase_stock/tests/test_purchase_lead_time.py @@ -29,19 +29,17 @@ def test_00_product_company_level_delays(self): # Check order date of purchase order order_date = fields.Datetime.from_string(date_planned) - timedelta(days=company.po_lead) - timedelta(days=self.product_1.seller_ids.delay) - po_order_date = fields.Datetime.to_string(order_date) - self.assertEqual(purchase.date_order, po_order_date, 'Order date should be equal to: Date of the procurement order - Purchase Lead Time - Delivery Lead Time.') + self.assertEqual(purchase.date_order, order_date, 'Order date should be equal to: Date of the procurement order - Purchase Lead Time - Delivery Lead Time.') # Check scheduled date of purchase order schedule_date = order_date + timedelta(days=self.product_1.seller_ids.delay) - po_schedule_date = fields.Datetime.to_string(schedule_date) - self.assertEqual(purchase.date_planned, po_schedule_date, 'Schedule date should be equal to: Order date of Purchase order + Delivery Lead Time.') + self.assertEqual(purchase.date_planned, schedule_date, 'Schedule date should be equal to: Order date of Purchase order + Delivery Lead Time.') # check the picking created or not self.assertTrue(purchase.picking_ids, "Picking should be created.") # Check scheduled date of In Type shipment - self.assertEqual(purchase.picking_ids.scheduled_date, po_schedule_date, 'Schedule date of In type shipment should be equal to: schedule date of purchase order.') + self.assertEqual(purchase.picking_ids.scheduled_date, schedule_date, 'Schedule date of In type shipment should be equal to: schedule date of purchase order.') def test_01_product_level_delay(self): """ To check schedule dates of multiple purchase order line of the same purchase order, @@ -68,21 +66,18 @@ def test_01_product_level_delay(self): order_line_pro_1 = purchase2.order_line.filtered(lambda r: r.product_id == self.product_1) order_line_pro_2 = purchase2.order_line.filtered(lambda r: r.product_id == self.product_2) order_date = fields.Datetime.from_string(date_planned1) - timedelta(days=self.product_1.seller_ids.delay) - po_order_date = fields.Datetime.to_string(order_date) - self.assertEqual(purchase2.date_order, po_order_date, 'Order date should be equal to: Date of the procurement order - Delivery Lead Time.') + self.assertEqual(purchase2.date_order, order_date, 'Order date should be equal to: Date of the procurement order - Delivery Lead Time.') # Check scheduled date of purchase order line for product_1 schedule_date_1 = order_date + timedelta(days=self.product_1.seller_ids.delay) - schedule_date_line_1 = fields.Datetime.to_string(schedule_date_1) - self.assertEqual(order_line_pro_1.date_planned, schedule_date_line_1, 'Schedule date of purchase order line for product_1 should be equal to: Order date of purchase order + Delivery Lead Time of product_1.') + self.assertEqual(order_line_pro_1.date_planned, schedule_date_1, 'Schedule date of purchase order line for product_1 should be equal to: Order date of purchase order + Delivery Lead Time of product_1.') # Check scheduled date of purchase order line for product_2 schedule_date_2 = order_date + timedelta(days=self.product_2.seller_ids.delay) - schedule_date_line_2 = fields.Datetime.to_string(schedule_date_2) - self.assertEqual(order_line_pro_2.date_planned, schedule_date_line_2, 'Schedule date of purchase order line for product_2 should be equal to: Order date of purchase order + Delivery Lead Time of product_2.') + self.assertEqual(order_line_pro_2.date_planned, schedule_date_2, 'Schedule date of purchase order line for product_2 should be equal to: Order date of purchase order + Delivery Lead Time of product_2.') # Check scheduled date of purchase order - po_schedule_date = min(schedule_date_line_1, schedule_date_line_2) + po_schedule_date = min(schedule_date_1, schedule_date_2) self.assertEqual(purchase2.date_planned, po_schedule_date, 'Schedule date of purchase order should be minimum of schedule dates of purchase order lines.') # Check the picking created or not @@ -121,13 +116,11 @@ def test_02_product_route_level_delays(self): # Check order date of purchase order order_date = fields.Datetime.from_string(date_planned) - timedelta(days=self.product_1.seller_ids.delay + rule_delay) - po_order_date = fields.Datetime.to_string(order_date) - self.assertEqual(purchase.date_order, po_order_date, 'Order date should be equal to: Date of the procurement order - Delivery Lead Time(supplier and pull rules).') + self.assertEqual(purchase.date_order, order_date, 'Order date should be equal to: Date of the procurement order - Delivery Lead Time(supplier and pull rules).') # Check scheduled date of purchase order schedule_date = order_date + timedelta(days=self.product_1.seller_ids.delay + rule_delay) - po_schedule_date = fields.Datetime.to_string(schedule_date) - self.assertEqual(date_planned, po_schedule_date, 'Schedule date should be equal to: Order date of Purchase order + Delivery Lead Time(supplier and pull rules).') + self.assertEqual(date_planned, str(schedule_date), 'Schedule date should be equal to: Order date of Purchase order + Delivery Lead Time(supplier and pull rules).') # Check the picking crated or not self.assertTrue(purchase.picking_ids, "Picking should be created.") @@ -135,10 +128,8 @@ def test_02_product_route_level_delays(self): # Check scheduled date of Internal Type shipment incoming_shipment1 = self.env['stock.picking'].search([('move_lines.product_id', 'in', (self.product_1.id, self.product_2.id)), ('picking_type_id', '=', self.warehouse_1.int_type_id.id), ('location_id', '=', self.warehouse_1.wh_input_stock_loc_id.id), ('location_dest_id', '=', self.warehouse_1.wh_qc_stock_loc_id.id)]) incoming_shipment1_date = order_date + timedelta(days=self.product_1.seller_ids.delay) - incoming_shipment1_schedule_date = fields.Datetime.to_string(incoming_shipment1_date) - self.assertEqual(incoming_shipment1.scheduled_date, incoming_shipment1_schedule_date, 'Schedule date of Internal Type shipment for input stock location should be equal to: schedule date of purchase order + push rule delay.') + self.assertEqual(incoming_shipment1.scheduled_date, incoming_shipment1_date, 'Schedule date of Internal Type shipment for input stock location should be equal to: schedule date of purchase order + push rule delay.') incoming_shipment2 = self.env['stock.picking'].search([('picking_type_id', '=', self.warehouse_1.int_type_id.id), ('location_id', '=', self.warehouse_1.wh_qc_stock_loc_id.id), ('location_dest_id', '=', self.warehouse_1.lot_stock_id.id)]) incoming_shipment2_date = schedule_date - timedelta(days=incoming_shipment2.move_lines[0].rule_id.delay) - incoming_shipment2_schedule_date = fields.Datetime.to_string(incoming_shipment2_date) - self.assertEqual(incoming_shipment2.scheduled_date, incoming_shipment2_schedule_date, 'Schedule date of Internal Type shipment for quality control stock location should be equal to: schedule date of Internal type shipment for input stock location + push rule delay..') + self.assertEqual(incoming_shipment2.scheduled_date, incoming_shipment2_date, 'Schedule date of Internal Type shipment for quality control stock location should be equal to: schedule date of Internal type shipment for input stock location + push rule delay..') diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 6ae3a5ab513f7..e1d0f7319f65e 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -163,9 +163,9 @@ def _compute_access_url(self): order.access_url = '/my/orders/%s' % (order.id) def _compute_is_expired(self): - now = datetime.now() + today = fields.Date.today() for order in self: - if order.validity_date and fields.Datetime.from_string(order.validity_date) < now: + if order.validity_date and order.validity_date < today: order.is_expired = True else: order.is_expired = False diff --git a/addons/sale_order_dates/models/sale_order.py b/addons/sale_order_dates/models/sale_order.py index f02552adfdf78..aeb9982e03946 100644 --- a/addons/sale_order_dates/models/sale_order.py +++ b/addons/sale_order_dates/models/sale_order.py @@ -39,8 +39,8 @@ def _compute_expected_date(self): def _compute_effective_date(self): for order in self: pickings = order.picking_ids.filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'customer') - dates_list = pickings.mapped('date_done') - order.effective_date = dates_list and min(dates_list) + dates_list = [date for date in pickings.mapped('date_done') if date] + order.effective_date = dates_list and min(dates_list).date() @api.onchange('commitment_date') def onchange_commitment_date(self): diff --git a/addons/sale_order_dates/tests/test_commitment_date.py b/addons/sale_order_dates/tests/test_commitment_date.py index 3b9494702ed53..d55b1d35134fc 100644 --- a/addons/sale_order_dates/tests/test_commitment_date.py +++ b/addons/sale_order_dates/tests/test_commitment_date.py @@ -17,6 +17,6 @@ def test_sale_order_commitment_date(self): # I verify that the Procurements and Stock Moves have been generated with the correct date security_delay = timedelta(days=new_order.company_id.security_lead) commitment_date = fields.Datetime.from_string(new_order.commitment_date) - right_date = fields.Datetime.to_string(commitment_date - security_delay) + right_date = commitment_date - security_delay for line in new_order.order_line: self.assertEqual(line.move_ids[0].date_expected, right_date, "The expected date for the Stock Move is wrong") diff --git a/addons/sale_order_dates/tests/test_expected_date.py b/addons/sale_order_dates/tests/test_expected_date.py index a565621cb4c7d..5286ee72dc25b 100644 --- a/addons/sale_order_dates/tests/test_expected_date.py +++ b/addons/sale_order_dates/tests/test_expected_date.py @@ -46,32 +46,36 @@ def test_sale_order_expected_date(self): # if Shipping Policy is set to `direct`(when SO is in draft state) then expected date should be # current date + shortest lead time from all of it's order lines - expected_date = fields.Datetime.to_string(fields.Datetime.from_string(fields.Datetime.now()) + timedelta(days=5)) - self.assertEquals(expected_date, sale_order.expected_date, "Wrong expected date on sale order!") + expected_date = fields.Datetime.now() + timedelta(days=5) + self.assertAlmostEqual(expected_date, sale_order.expected_date, + msg="Wrong expected date on sale order!", delta=timedelta(seconds=1)) # if Shipping Policy is set to `one`(when SO is in draft state) then expected date should be # current date + longest lead time from all of it's order lines sale_order.write({'picking_policy': 'one'}) - expected_date = fields.Datetime.to_string(fields.Datetime.from_string(fields.Datetime.now()) + timedelta(days=15)) - self.assertEquals(expected_date, sale_order.expected_date, "Wrong expected date on sale order!") + expected_date = fields.Datetime.now() + timedelta(days=15) + self.assertAlmostEquals(expected_date, sale_order.expected_date, + msg="Wrong expected date on sale order!", delta=timedelta(seconds=1)) sale_order.action_confirm() # Setting confirmation date of SO to 5 days from today so that the expected/effective date could be checked # against real confirmation date - confirm_date = fields.Datetime.from_string(fields.Datetime.now()) + timedelta(days=5) + confirm_date = fields.Datetime.now() + timedelta(days=5) sale_order.write({'confirmation_date': confirm_date}) # if Shipping Policy is set to `one`(when SO is confirmed) then expected date should be # SO confirmation date + longest lead time from all of it's order lines - expected_date = fields.Datetime.to_string(confirm_date + timedelta(days=15)) - self.assertEquals(expected_date, sale_order.expected_date, "Wrong expected date on sale order!") + expected_date = confirm_date + timedelta(days=15) + self.assertAlmostEqual(expected_date, sale_order.expected_date, + msg="Wrong expected date on sale order!", delta=timedelta(seconds=1)) # if Shipping Policy is set to `direct`(when SO is confirmed) then expected date should be # SO confirmation date + shortest lead time from all of it's order lines sale_order.write({'picking_policy': 'direct'}) - expected_date = fields.Datetime.to_string(confirm_date + timedelta(days=5)) - self.assertEquals(expected_date, sale_order.expected_date, "Wrong expected date on sale order!") + expected_date = confirm_date + timedelta(days=5) + self.assertAlmostEqual(expected_date, sale_order.expected_date, + msg="Wrong expected date on sale order!", delta=timedelta(seconds=1)) # Check effective date, it should be date on which the first shipment successfully delivered to customer picking = sale_order.picking_ids[0] diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index bb469c21b866b..43ce0fbbe1732 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -280,13 +280,13 @@ def _prepare_procurement_values(self, group_id=False): """ values = super(SaleOrderLine, self)._prepare_procurement_values(group_id) self.ensure_one() - date_planned = datetime.strptime(self.order_id.confirmation_date, DEFAULT_SERVER_DATETIME_FORMAT)\ + date_planned = self.order_id.confirmation_date\ + timedelta(days=self.customer_lead or 0.0) - timedelta(days=self.order_id.company_id.security_lead) values.update({ 'company_id': self.order_id.company_id, 'group_id': group_id, 'sale_line_id': self.id, - 'date_planned': date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'date_planned': date_planned, 'route_ids': self.route_id, 'warehouse_id': self.order_id.warehouse_id or False, 'partner_dest_id': self.order_id.partner_shipping_id diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 3a9b6f0fb0cf6..5c91fff6af294 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -93,7 +93,9 @@ def _compute_quantities_dict(self, lot_id, owner_id, package_id, from_date=False domain_quant_loc, domain_move_in_loc, domain_move_out_loc = self._get_domain_locations() domain_quant = [('product_id', 'in', self.ids)] + domain_quant_loc dates_in_the_past = False - if to_date and to_date < fields.Datetime.now(): #Only to_date as to_date will correspond to qty_available + # only to_date as to_date will correspond to qty_available + to_date = fields.Datetime.to_datetime(to_date) + if to_date and to_date < fields.Datetime.now(): dates_in_the_past = True domain_move_in = [('product_id', 'in', self.ids)] + domain_move_in_loc diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 35b18f0f0af8a..827f9c5a6ee01 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -426,11 +426,11 @@ def write(self, vals): if 'date_expected' in propagated_changes_dict: propagated_changes_dict.pop('date_expected') if propagated_date_field: - current_date = datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT) - new_date = datetime.strptime(vals.get(propagated_date_field), DEFAULT_SERVER_DATETIME_FORMAT) + current_date = move.date_expected + new_date = fields.Datetime.from_string(vals.get(propagated_date_field)) delta_days = (new_date - current_date).total_seconds() / 86400 if abs(delta_days) >= move.company_id.propagation_minimum_delta: - old_move_date = datetime.strptime(move.move_dest_ids[0].date_expected, DEFAULT_SERVER_DATETIME_FORMAT) + old_move_date = move.move_dest_ids[0].date_expected new_move_date = (old_move_date + relativedelta.relativedelta(days=delta_days or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) propagated_changes_dict['date_expected'] = new_move_date #For pushed moves as well as for pulled moves, propagate by recursive call of write(). diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py index c2ea9f222931a..11beb7338a25d 100644 --- a/addons/stock/models/stock_rule.py +++ b/addons/stock/models/stock_rule.py @@ -136,7 +136,7 @@ def _run_push(self, move): Care this function is not call by method run. It is called explicitely in stock_move.py inside the method _push_apply """ - new_date = (datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=self.delay)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) + new_date = fields.Datetime.to_string(move.date_expected + relativedelta(days=self.delay)) if self.auto == 'transparent': move.write({ 'date': new_date, @@ -193,7 +193,9 @@ def _get_stock_move_values(self, product_id, product_qty, product_uom, location_ :param procurement: browse record :rtype: dictionary ''' - date_expected = (datetime.strptime(values['date_planned'], DEFAULT_SERVER_DATETIME_FORMAT) - relativedelta(days=self.delay or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) + date_expected = fields.Datetime.to_string( + fields.Datetime.from_string(values['date_planned']) - relativedelta(days=self.delay or 0) + ) # it is possible that we've already got some move done, so check for the done qty and create # a new move with the correct qty qty_left = product_qty diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index a65365cc801ef..f3fee888e0de5 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -3602,8 +3602,9 @@ def test_in_date_2(self): ('product_id', '=', self.product3.id), ('lot_id', '=', lot1.id), ]) - from datetime import datetime, timedelta - initial_in_date_lot1 = datetime.now() - timedelta(days=5) + from odoo.fields import Datetime + from datetime import timedelta + initial_in_date_lot1 = Datetime.now() - timedelta(days=5) quant_lot1.in_date = initial_in_date_lot1 # Move one quant to pack location @@ -3626,8 +3627,7 @@ def test_in_date_2(self): # As lot1 has an older date and FIFO is set by default, it's the one that should be # in pack. self.assertEqual(len(quant_in_pack), 1) - from odoo.fields import Datetime - self.assertEqual(quant_in_pack.in_date, Datetime.to_string(initial_in_date_lot1)) + self.assertAlmostEqual(quant_in_pack.in_date, initial_in_date_lot1, delta=timedelta(seconds=1)) self.assertEqual(quant_in_pack.lot_id, lot1) # Now, edit the move line and actually move the other lot @@ -3640,7 +3640,7 @@ def test_in_date_2(self): ('lot_id', '=', lot1.id), ]) self.assertEqual(quant_lot1.location_id, self.stock_location) - self.assertEqual(quant_lot1.in_date, Datetime.to_string(initial_in_date_lot1)) + self.assertAlmostEqual(quant_lot1.in_date, initial_in_date_lot1, delta=timedelta(seconds=1)) # Check that lo2 is in pack with is right in_date quant_lot2 = self.env['stock.quant'].search([ @@ -3649,7 +3649,7 @@ def test_in_date_2(self): ('lot_id', '=', lot2.id), ]) self.assertEqual(quant_lot2.location_id, self.pack_location) - self.assertEqual(quant_lot2.in_date, initial_in_date_lot2) + self.assertAlmostEqual(quant_lot2.in_date, initial_in_date_lot2, delta=timedelta(seconds=1)) def test_in_date_3(self): """ Check that, when creating a move line on a done stock move, the lot and its incoming @@ -3707,8 +3707,9 @@ def test_in_date_3(self): ('product_id', '=', self.product3.id), ('lot_id', '=', lot1.id), ]) - from datetime import datetime, timedelta - initial_in_date_lot1 = datetime.now() - timedelta(days=5) + from odoo.fields import Datetime + from datetime import timedelta + initial_in_date_lot1 = Datetime.now() - timedelta(days=5) quant_lot1.in_date = initial_in_date_lot1 # Move one quant to pack location @@ -3741,12 +3742,11 @@ def test_in_date_3(self): ('product_id', '=', self.product3.id), ]) self.assertEqual(len(quants), 2) - from odoo.fields import Datetime for quant in quants: if quant.lot_id == lot1: - self.assertEqual(quant.in_date, Datetime.to_string(initial_in_date_lot1)) + self.assertAlmostEqual(quant.in_date, initial_in_date_lot1, delta=timedelta(seconds=1)) elif quant.lot_id == lot2: - self.assertEqual(quant.in_date, initial_in_date_lot2) + self.assertAlmostEqual(quant.in_date, initial_in_date_lot2, delta=timedelta(seconds=1)) def test_transit_1(self): """ Receive some products, send some to transit, check the product's `available_qty` diff --git a/addons/stock/tests/test_quant.py b/addons/stock/tests/test_quant.py index 9fb150341669d..749c10e57e3f1 100644 --- a/addons/stock/tests/test_quant.py +++ b/addons/stock/tests/test_quant.py @@ -763,7 +763,8 @@ def test_in_date_5(self): 'product_id': product1.id, }) - in_date1 = datetime.now() + from odoo.fields import Datetime + in_date1 = Datetime.now() self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0, lot_id=lot1, in_date=in_date1) quant = self.env['stock.quant'].search([ @@ -773,10 +774,9 @@ def test_in_date_5(self): self.assertEqual(len(quant), 1) self.assertEqual(quant.quantity, 1) self.assertEqual(quant.lot_id.id, lot1.id) - from odoo.fields import Datetime - self.assertEqual(quant.in_date, Datetime.to_string(in_date1)) + self.assertEqual(quant.in_date, in_date1) - in_date2 = datetime.now() - timedelta(days=5) + in_date2 = Datetime.now() - timedelta(days=5) self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0, lot_id=lot1, in_date=in_date2) quant = self.env['stock.quant'].search([ @@ -786,4 +786,4 @@ def test_in_date_5(self): self.assertEqual(len(quant), 1) self.assertEqual(quant.quantity, 2) self.assertEqual(quant.lot_id.id, lot1.id) - self.assertEqual(quant.in_date, Datetime.to_string(in_date2)) + self.assertEqual(quant.in_date, in_date2) diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py index 200e91af00b5f..df22e8a382f48 100644 --- a/addons/stock_account/tests/test_stockvaluation.py +++ b/addons/stock_account/tests/test_stockvaluation.py @@ -4,7 +4,7 @@ from datetime import timedelta from odoo.exceptions import UserError -from odoo.fields import Date +from odoo.fields import Date, Datetime from odoo.tests.common import TransactionCase @@ -3260,7 +3260,7 @@ def test_move_in_or_out(self): def test_at_date_standard_1(self): self.product1.product_tmpl_id.cost_method = 'standard' - now = Date.from_string(Date.today()) + now = Datetime.now() date1 = now - timedelta(days=8) date2 = now - timedelta(days=7) date3 = now - timedelta(days=6) @@ -3387,42 +3387,42 @@ def test_at_date_standard_1(self): self.assertEqual(self.product1.stock_value, 712.5) # Quantity available at date - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_available, 0) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_available, 10) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_available, 30) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_available, 15) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_available, 15) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_available, -5) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date7)).qty_available, -5) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date8)).qty_available, 95) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).qty_available, 0) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).qty_available, 10) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).qty_available, 30) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).qty_available, 15) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).qty_available, 15) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).qty_available, -5) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date7)).qty_available, -5) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date8)).qty_available, 95) # Valuation at date - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).stock_value, 100) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).stock_value, 300) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).stock_value, 150) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).stock_value, 75) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, -25) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date8)).stock_value, 712.5) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).stock_value, 100) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).stock_value, 300) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).stock_value, 150) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).stock_value, 75) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).stock_value, -25) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date8)).stock_value, 712.5) # Quantity at date - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_at_date, 0.0) - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 10.0) - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_at_date, 30.0) - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_at_date, 15.0) - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_at_date, 15.0) - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, -5.0) - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date7)).qty_at_date, -5.0) - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date8)).qty_at_date, 95.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).qty_at_date, 0.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).qty_at_date, 10.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).qty_at_date, 30.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).qty_at_date, 15.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).qty_at_date, 15.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).qty_at_date, -5.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date7)).qty_at_date, -5.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date8)).qty_at_date, 95.0) # edit the done quantity of move1, decrease it - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_available, 10) - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 10.0) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).qty_available, 10) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).qty_at_date, 10.0) move1.quantity_done = 5 move1.account_move_ids.write({'date': date2}) # the quantity at date will reflect the change directly - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_available, 5) - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 5.0) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).qty_available, 5) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).qty_at_date, 5.0) # as when we decrease a quantity on a recreipt, we consider it as a out move with the price # of today, the value will be decrease of 100 - (5*7.5) @@ -3430,22 +3430,22 @@ def test_at_date_standard_1(self): # the valuatin at date will take the qty at date * the standard price at date, that's why # it is different. - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).stock_value, 50) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).stock_value, 50) # edit move 4, send 15 instead of 20 # we now have +5 + 20 - 15 -20 = -10 * a standard price of 5 - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_available, -10.0) - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, -10.0) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, -50) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).qty_available, -10.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).qty_at_date, -10.0) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).stock_value, -50) move4.quantity_done = 15 move4.account_move_ids.write({'date': date6}) # -(20*5) + (5*7.5) self.assertEqual(move4.value, -62.5) # we now have +5 + 20 - 15 -15 = -5 * a standard price of 5 - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_available, -5.0) - self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, -5.0) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, -25) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).qty_available, -5.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).qty_at_date, -5.0) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).stock_value, -25) def test_at_date_fifo_1(self): """ Make some operations at different dates, check that the results of the valuation at @@ -3454,7 +3454,7 @@ def test_at_date_fifo_1(self): """ self.product1.product_tmpl_id.cost_method = 'fifo' - now = Date.from_string(Date.today()) + now = Datetime.now() date1 = now - timedelta(days=8) date2 = now - timedelta(days=7) date3 = now - timedelta(days=6) @@ -3580,18 +3580,18 @@ def test_at_date_fifo_1(self): # ending: manual valuation # --------------------------------------------------------------------- self.product1.product_tmpl_id.valuation = 'manual_periodic' - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_at_date, 20) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).stock_value, 200) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 30) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).stock_value, 320) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_at_date, 15) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).stock_value, 160) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_at_date, -5) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).stock_value, -125) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_at_date, 95) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).stock_value, 1375) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, 95) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, 1375) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).qty_at_date, 20) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).stock_value, 200) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).qty_at_date, 30) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).stock_value, 320) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).qty_at_date, 15) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).stock_value, 160) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).qty_at_date, -5) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).stock_value, -125) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).qty_at_date, 95) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).stock_value, 1375) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).qty_at_date, 95) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).stock_value, 1375) self.assertEqual(self.product1.qty_at_date, 95) self.assertEqual(self.product1.stock_value, 1375) @@ -3599,25 +3599,25 @@ def test_at_date_fifo_1(self): # ending: perpetual valuation # --------------------------------------------------------------------- self.product1.product_tmpl_id.valuation = 'real_time' - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_at_date, 10) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).stock_value, 100) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 20) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).stock_value, 220) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_at_date, 5) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).stock_value, 60) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_at_date, -15) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).stock_value, -180) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_at_date, 85) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).stock_value, 1320) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, 85) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, 1275) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).stock_value, 100) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).qty_at_date, 20) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).stock_value, 220) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).qty_at_date, 5) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).stock_value, 60) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).qty_at_date, -15) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).stock_value, -180) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).qty_at_date, 85) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).stock_value, 1320) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).qty_at_date, 85) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).stock_value, 1275) self.assertEqual(self.product1.qty_at_date, 95) self.assertEqual(self.product1.stock_value, 1375) def test_at_date_fifo_2(self): self.product1.product_tmpl_id.cost_method = 'fifo' - now = Date.from_string(Date.today()) + now = Datetime.now() date1 = now - timedelta(days=8) date2 = now - timedelta(days=7) date3 = now - timedelta(days=6) @@ -3741,18 +3741,18 @@ def test_at_date_fifo_2(self): # ending: manual valuation # --------------------------------------------------------------------- self.product1.product_tmpl_id.valuation = 'manual_periodic' - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_at_date, 10) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).stock_value, 100) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 20) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).stock_value, 250) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_at_date, -10) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).stock_value, -200) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_at_date, 0) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).stock_value, 0) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_at_date, 10) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).stock_value, 100) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, 10) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, 100) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).stock_value, 100) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).qty_at_date, 20) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).stock_value, 250) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).qty_at_date, -10) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).stock_value, -200) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).qty_at_date, 0) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).stock_value, 0) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).stock_value, 100) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).stock_value, 100) self.assertEqual(self.product1.qty_at_date, 10) self.assertEqual(self.product1.stock_value, 100) @@ -3760,17 +3760,17 @@ def test_at_date_fifo_2(self): # ending: perpetual valuation # --------------------------------------------------------------------- self.product1.product_tmpl_id.valuation = 'real_time' - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_at_date, 10) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).stock_value, 100) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 20) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).stock_value, 250) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_at_date, -10) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).stock_value, -150) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_at_date, 0) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).stock_value, 50) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_at_date, 10) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).stock_value, 150) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, 10) - self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, 100) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).stock_value, 100) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).qty_at_date, 20) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).stock_value, 250) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).qty_at_date, -10) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).stock_value, -150) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).qty_at_date, 0) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).stock_value, 50) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).stock_value, 150) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).stock_value, 100) self.assertEqual(self.product1.qty_at_date, 10) self.assertEqual(self.product1.stock_value, 100) diff --git a/addons/stock_dropshipping/tests/test_lifo_price.py b/addons/stock_dropshipping/tests/test_lifo_price.py index ea9c424435423..a93c1c1a4e003 100644 --- a/addons/stock_dropshipping/tests/test_lifo_price.py +++ b/addons/stock_dropshipping/tests/test_lifo_price.py @@ -86,7 +86,7 @@ def test_lifoprice(self): with out_form.move_ids_without_package.new() as move: move.product_id = product_lifo_icecream move.quantity_done = 20.0 - move.date_expected = fields.Date.context_today(self.env['stock.move.line']) + move.date_expected = fields.Datetime.now() outgoing_lifo_shipment = out_form.save() # I assign this outgoing shipment diff --git a/addons/web/models/ir_qweb.py b/addons/web/models/ir_qweb.py index 757f30dc1e27e..34d2ecb0d5dd5 100644 --- a/addons/web/models/ir_qweb.py +++ b/addons/web/models/ir_qweb.py @@ -41,7 +41,7 @@ def record_to_html(self, record, field_name, options): if max_width or max_height: max_size = '%sx%s' % (max_width, max_height) - sha = hashlib.sha1(getattr(record, '__last_update').encode('utf-8')).hexdigest()[0:7] + sha = hashlib.sha1(str(getattr(record, '__last_update')).encode('utf-8')).hexdigest()[0:7] max_size = '' if max_size is None else '/%s' % max_size avoid_if_small = '&avoid_if_small=true' if options.get('avoid_if_small') else '' src = '/web/image/%s/%s/%s%s?unique=%s%s' % (record._name, record.id, field_name, max_size, sha, avoid_if_small) diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 40c1ce0ae6eba..819d4777cdd91 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -540,7 +540,7 @@ def enumerate_pages(self, query_string=None, force=False): if page.view_id and page.view_id.priority != 16: record['__priority'] = min(round(page.view_id.priority / 32.0, 1), 1) if page['write_date']: - record['__lastmod'] = page['write_date'][:10] + record['__lastmod'] = page['write_date'].date() yield record @api.multi @@ -563,7 +563,7 @@ def search_pages(self, needle=None, limit=None): def image_url(self, record, field, size=None): """ Returns a local url that points to the image field of a given browse record. """ sudo_record = record.sudo() - sha = hashlib.sha1(getattr(sudo_record, '__last_update').encode('utf-8')).hexdigest()[0:7] + sha = hashlib.sha1(str(getattr(sudo_record, '__last_update')).encode('utf-8')).hexdigest()[0:7] size = '' if size is None else '/%s' % size return '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field, size, sha) diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py index 3741ee16035f7..6ff422579fd60 100644 --- a/addons/website_blog/models/website_blog.py +++ b/addons/website_blog/models/website_blog.py @@ -221,7 +221,8 @@ def write(self, vals): result = True for post in self: copy_vals = dict(vals) - if 'website_published' in vals and 'published_date' not in vals and (post.published_date or '') <= fields.Datetime.now(): + if ('website_published' in vals and 'published_date' not in vals and + (not post.published_date or post.published_date <= fields.Datetime.now())): copy_vals['published_date'] = vals['website_published'] and fields.Datetime.now() or False result &= super(BlogPost, self).write(copy_vals) self._check_for_publication(vals) diff --git a/addons/website_blog/views/website_blog_templates.xml b/addons/website_blog/views/website_blog_templates.xml index 248cd5dec155d..44d1d4a9ffa85 100644 --- a/addons/website_blog/views/website_blog_templates.xml +++ b/addons/website_blog/views/website_blog_templates.xml @@ -562,14 +562,14 @@ <link t-att-href="'%s/blog/%s' % (base_url ,blog.id)"/> <id t-esc="'%s/blog/%s' % (base_url, blog.id)"/> - <updated t-esc="(posts[0] if posts else blog).post_date.replace(' ', 'T') + 'Z'"/> + <updated t-esc="str((posts[0] if posts else blog).post_date).replace(' ', 'T') + 'Z'"/> <entry t-foreach="posts" t-as="post"> <title t-esc="post.name"/> <link t-att-href="'%s%s' % (base_url, post.website_url)"/> <id t-esc="'%s%s' % (base_url, post.website_url)"/> <author><name t-esc="post.sudo().author_id.name"/></author> <summary t-esc="html2plaintext(post.teaser)"/> - <updated t-esc="post.post_date.replace(' ', 'T') + 'Z'"/> + <updated t-esc="str(post.post_date).replace(' ', 'T') + 'Z'"/> </entry> </feed> </template> diff --git a/addons/website_event/models/event.py b/addons/website_event/models/event.py index ac3eb999efe3c..f82baa954560a 100644 --- a/addons/website_event/models/event.py +++ b/addons/website_event/models/event.py @@ -150,7 +150,7 @@ def _get_ics_file(self): if not event.date_begin or not event.date_end: raise UserError(_("No date has been specified for the event, no file will be generated.")) - cal_event.add('created').value = fields.Datetime.from_string(fields.Datetime.now()).replace(tzinfo=pytz.timezone('UTC')) + cal_event.add('created').value = fields.Datetime.now().replace(tzinfo=pytz.timezone('UTC')) cal_event.add('dtstart').value = fields.Datetime.from_string(event.date_begin).replace(tzinfo=pytz.timezone('UTC')) cal_event.add('dtend').value = fields.Datetime.from_string(event.date_end).replace(tzinfo=pytz.timezone('UTC')) cal_event.add('summary').value = event.name diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py index 4e493e5017ea4..c45e7a2e30a1a 100644 --- a/addons/website_forum/controllers/main.py +++ b/addons/website_forum/controllers/main.py @@ -256,7 +256,7 @@ def question(self, forum, question, **post): values.update({ 'main_object': question, 'question': question, - 'can_bump': (question.forum_id.allow_bump and not question.child_ids and (datetime.today() - datetime.strptime(question.write_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days > 9), + 'can_bump': (question.forum_id.allow_bump and not question.child_ids and (datetime.today() - question.write_date).days > 9), 'header': {'question_data': True}, 'filters': filters, 'reversed': reversed, diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 8b5ed94091a60..74af33b462cc3 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -312,7 +312,7 @@ def _get_plain_content(self): @api.depends('vote_count', 'forum_id.relevancy_post_vote', 'forum_id.relevancy_time_decay') def _compute_relevancy(self): if self.create_date: - days = (datetime.today() - datetime.strptime(self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days + days = (datetime.today() - self.create_date).days self.relevancy = math.copysign(1, self.vote_count) * (abs(self.vote_count - 1) ** self.forum_id.relevancy_post_vote / (days + 2) ** self.forum_id.relevancy_time_decay) else: self.relevancy = 0 diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 45f36c11dbde0..098edad13783e 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -47,7 +47,7 @@ def _compute_cart_info(self): @api.depends('team_id.team_type', 'date_order', 'order_line', 'state', 'partner_id') def _compute_abandoned_cart(self): abandoned_delay = float(self.env['ir.config_parameter'].sudo().get_param('website_sale.cart_abandoned_delay', '1.0')) - abandoned_datetime = fields.Datetime.to_string(datetime.utcnow() - relativedelta(hours=abandoned_delay)) + abandoned_datetime = datetime.utcnow() - relativedelta(hours=abandoned_delay) for order in self: domain = order.date_order <= abandoned_datetime and order.team_id.team_type == 'website' and order.state == 'draft' and order.partner_id.id != self.env.ref('base.public_partner').id and order.order_line order.is_abandoned_cart = bool(domain) diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 78322a9805d78..9c42b6698d797 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -429,7 +429,7 @@ <div id="o-carousel-product" class="carousel slide" data-ride="carousel" data-interval="0"> <div class="carousel-outer"> <div class="carousel-inner"> - <div t-if="variant_img" class="carousel-item active" itemprop="image" t-field="product[:1].product_variant_id.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update'] + (product.product_variant_id['__last_update'] or '')}"/> + <div t-if="variant_img" class="carousel-item active" itemprop="image" t-field="product[:1].product_variant_id.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': str(product['__last_update']) + (str(product.product_variant_id['__last_update']) or '')}"/> <div t-attf-class="carousel-item#{'' if variant_img else ' active'}" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update']}"/> <t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg"> <div class="carousel-item" t-field="pimg.image" t-options='{"widget": "image", "class": "product_detail_img", "alt-field": "name", "zoom": "image" }'/> diff --git a/doc/reference/orm.rst b/doc/reference/orm.rst index 9ce03c8709a2e..1a0dba9edf1e3 100644 --- a/doc/reference/orm.rst +++ b/doc/reference/orm.rst @@ -866,13 +866,57 @@ Basic fields .. autoclass:: odoo.fields.Html :show-inheritance: +.. _reference/orm/fields/date_datetime: + +Date and Datetime fields +------------------------ + +Dates and Datetimes are very important fields in any kind of business +application, they are heavily used in many popular Odoo applications such as +logistics or accounting and their misuse can create invisible yet painful +bugs, this excerpt aims to provide Odoo developers with the knowledge required +to avoid misusing these fields. + +When assigning a value to a Date/Datetime field, the following options are valid: + * A string in the proper server format **(YYYY-MM-DD)** for Date fields, + **(YYYY-MM-DD HH:MM:SS)** for Datetime fields. + * A `date` or `datetime` object. + * `False` or `None`. + +If not sure of the type of the value being assigned to a Date/Datetime object, +the best course of action is to pass the value to +:func:`~odoo.fields.Date.to_date` or :func:`~odoo.fields.Datetime.to_datetime` +which will attempt to convert the value to a date or datetime object +respectively, which can then be assigned to the field in question. + +.. admonition:: Example + + To parse date/datetimes coming from external sources:: + + fields.Date.to_date(self._context.get('date_from')) + +Date / Datetime comparison best practices: + * Date fields can **only** be compared to date objects. + * Datetime fields can **only** be compared to datetime objects. + + .. warning:: Strings representing dates and datetimes can be compared + between each other, however the result may not be the expected + result, as a datetime string will always be greater than a + date string, therefore this practice is **heavily** + discouraged. + +Common operations with dates and datetimes such as addition, substraction or +fetching the start/end of a period are exposed through both +:class:`~odoo.fields.Date` and :class:`~odoo.fields.Datetime`. +These helpers are also available by importing `odoo.tools.date_utils`. + .. autoclass:: odoo.fields.Date :show-inheritance: - :members: today, context_today, from_string, to_string + :members: today, context_today, to_date, to_string, start_of, end_of, add, subtract .. autoclass:: odoo.fields.Datetime :show-inheritance: - :members: now, context_timestamp, from_string, to_string + :members: now, today, context_timestamp, to_datetime, to_string, start_of, end_of, add, subtract .. _reference/orm/fields/relational: diff --git a/odoo/addons/base/models/assetsbundle.py b/odoo/addons/base/models/assetsbundle.py index d60cfe3475394..345ab279e32fc 100644 --- a/odoo/addons/base/models/assetsbundle.py +++ b/odoo/addons/base/models/assetsbundle.py @@ -358,7 +358,7 @@ def is_css_preprocessed(self): attachments = self.env['ir.attachment'].sudo().search(assets_domain) for attachment in attachments: asset = assets[attachment.url] - if asset.last_modified > fields.Datetime.from_string(attachment['__last_update']): + if asset.last_modified > attachment['__last_update']: outdated = True break if asset._content is None: @@ -539,12 +539,7 @@ def last_modified(self): if self._filename: return datetime.fromtimestamp(os.path.getmtime(self._filename)) elif self._ir_attach: - server_format = tools.DEFAULT_SERVER_DATETIME_FORMAT - last_update = self._ir_attach['__last_update'] - try: - return datetime.strptime(last_update, server_format + '.%f') - except ValueError: - return datetime.strptime(last_update, server_format) + return self._ir_attach['__last_update'] except Exception: pass return datetime(1970, 1, 1) diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py index 6fab91e749812..eea8c78927a31 100644 --- a/odoo/addons/base/models/ir_http.py +++ b/odoo/addons/base/models/ir_http.py @@ -139,12 +139,7 @@ def _serve_attachment(cls): return werkzeug.utils.redirect(name, 301) response = werkzeug.wrappers.Response() - server_format = tools.DEFAULT_SERVER_DATETIME_FORMAT - try: - response.last_modified = datetime.datetime.strptime(wdate, server_format + '.%f') - except ValueError: - # just in case we have a timestamp without microseconds - response.last_modified = datetime.datetime.strptime(wdate, server_format) + response.last_modified = wdate response.set_etag(checksum) response.make_conditional(request.httprequest) diff --git a/odoo/addons/base/models/ir_sequence.py b/odoo/addons/base/models/ir_sequence.py index 6a23b0e3f0003..b096e65e591cc 100644 --- a/odoo/addons/base/models/ir_sequence.py +++ b/odoo/addons/base/models/ir_sequence.py @@ -196,9 +196,9 @@ def _interpolate(s, d): def _interpolation_dict(): now = range_date = effective_date = datetime.now(pytz.timezone(self._context.get('tz') or 'UTC')) if self._context.get('ir_sequence_date'): - effective_date = datetime.strptime(self._context.get('ir_sequence_date'), '%Y-%m-%d') + effective_date = fields.Datetime.from_string(self._context.get('ir_sequence_date')) if self._context.get('ir_sequence_date_range'): - range_date = datetime.strptime(self._context.get('ir_sequence_date_range'), '%Y-%m-%d') + range_date = fields.Datetime.from_string(self._context.get('ir_sequence_date_range')) sequences = { 'year': '%Y', 'month': '%m', 'day': '%d', 'y': '%y', 'doy': '%j', 'woy': '%W', @@ -230,12 +230,10 @@ def _create_date_range_seq(self, date): date_to = '{}-12-31'.format(year) date_range = self.env['ir.sequence.date_range'].search([('sequence_id', '=', self.id), ('date_from', '>=', date), ('date_from', '<=', date_to)], order='date_from desc', limit=1) if date_range: - date_to = datetime.strptime(date_range.date_from, '%Y-%m-%d') + timedelta(days=-1) - date_to = date_to.strftime('%Y-%m-%d') + date_to = date_range.date_from + timedelta(days=-1) date_range = self.env['ir.sequence.date_range'].search([('sequence_id', '=', self.id), ('date_to', '>=', date_from), ('date_to', '<=', date)], order='date_to desc', limit=1) if date_range: - date_from = datetime.strptime(date_range.date_to, '%Y-%m-%d') + timedelta(days=1) - date_from = date_from.strftime('%Y-%m-%d') + date_from = date_range.date_to + timedelta(days=1) seq_date_range = self.env['ir.sequence.date_range'].sudo().create({ 'date_from': date_from, 'date_to': date_to, diff --git a/odoo/addons/base/tests/test_ir_sequence_date_range.py b/odoo/addons/base/tests/test_ir_sequence_date_range.py index 7bd43eb4ea99f..08729f3a07254 100644 --- a/odoo/addons/base/tests/test_ir_sequence_date_range.py +++ b/odoo/addons/base/tests/test_ir_sequence_date_range.py @@ -22,7 +22,7 @@ def test_ir_sequence_date_range_1_create(self): def test_ir_sequence_date_range_2_change_dates(self): """ Draw numbers to create a first subsequence then change its date range. Then, try to draw a new number adn check a new subsequence was correctly created. """ year = date.today().year - 1 - january = lambda d: date(year, 1, d).strftime(DATE_FORMAT) + january = lambda d: date(year, 1, d) seq16 = self.env['ir.sequence'].with_context({'ir_sequence_date': january(16)}) n = seq16.next_by_code('test_sequence_date_range') @@ -63,7 +63,7 @@ def test_ir_sequence_date_range_1_create_no_gap(self): def test_ir_sequence_date_range_2_change_dates(self): """ Draw numbers to create a first subsequence then change its date range. Then, try to draw a new number adn check a new subsequence was correctly created. """ year = date.today().year - 1 - january = lambda d: date(year, 1, d).strftime(DATE_FORMAT) + january = lambda d: date(year, 1, d) seq16 = self.env['ir.sequence'].with_context({'ir_sequence_date': january(16)}) n = seq16.next_by_code('test_sequence_date_range_2') @@ -111,7 +111,7 @@ def test_ir_sequence_date_range_1_create(self): def test_ir_sequence_date_range_2_use(self): """ Make some use of the sequences to create some subsequences """ year = date.today().year - 1 - january = lambda d: date(year, 1, d).strftime(DATE_FORMAT) + january = lambda d: date(year, 1, d) seq = self.env['ir.sequence'] seq16 = self.env['ir.sequence'].with_context({'ir_sequence_date': january(16)}) diff --git a/odoo/addons/test_impex/tests/test_load.py b/odoo/addons/test_impex/tests/test_load.py index 628e879374d70..5a29ec9e1c5b8 100644 --- a/odoo/addons/test_impex/tests/test_load.py +++ b/odoo/addons/test_impex/tests/test_load.py @@ -5,6 +5,7 @@ import pkgutil import re +from odoo import fields from odoo.tests import common from odoo.tools.misc import mute_logger @@ -1065,7 +1066,7 @@ def test_checktz1(self): ['value'], [['2012-02-03 11:11:11']], {'tz': 'Pacific/Kiritimati'}) self.assertFalse(result['messages']) self.assertEqual( - values(self.read(domain=[('id', 'in', result['ids'])])), + [fields.Datetime.to_string(value['value']) for value in self.read(domain=[('id', 'in', result['ids'])])], ['2012-02-02 21:11:11']) # UTC-0930 @@ -1073,7 +1074,7 @@ def test_checktz1(self): ['value'], [['2012-02-03 11:11:11']], {'tz': 'Pacific/Marquesas'}) self.assertFalse(result['messages']) self.assertEqual( - values(self.read(domain=[('id', 'in', result['ids'])])), + [fields.Datetime.to_string(value['value']) for value in self.read(domain=[('id', 'in', result['ids'])])], ['2012-02-03 20:41:11']) def test_usertz(self): @@ -1087,7 +1088,7 @@ def test_usertz(self): ['value'], [['2012-02-03 11:11:11']]) self.assertFalse(result['messages']) self.assertEqual( - values(self.read(domain=[('id', 'in', result['ids'])])), + [fields.Datetime.to_string(value['value']) for value in self.read(domain=[('id', 'in', result['ids'])])], ['2012-02-03 01:11:11']) def test_notz(self): @@ -1099,7 +1100,7 @@ def test_notz(self): result = self.import_(['value'], [['2012-02-03 11:11:11']]) self.assertFalse(result['messages']) self.assertEqual( - values(self.read(domain=[('id', 'in', result['ids'])])), + [fields.Datetime.to_string(value['value']) for value in self.read(domain=[('id', 'in', result['ids'])])], ['2012-02-03 11:11:11']) diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 1b458fa604d91..b4459f298ff89 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -1,11 +1,13 @@ # # test cases for new-style fields # -from datetime import date, datetime +from datetime import date, datetime, time -from odoo.exceptions import AccessError, UserError, except_orm +from odoo import fields +from odoo.exceptions import AccessError, UserError from odoo.tests import common from odoo.tools import mute_logger, float_repr, pycompat +from odoo.tools.date_utils import add, subtract, start_of, end_of class TestFields(common.TransactionCase): @@ -484,20 +486,117 @@ def test_21_date(self): record.date = None self.assertFalse(record.date) - # one may assign date and datetime objects + # one may assign date but not datetime objects record.date = date(2012, 5, 1) - self.assertEqual(record.date, '2012-05-01') + self.assertEqual(record.date, date(2012, 5, 1)) - record.date = datetime(2012, 5, 1, 10, 45, 00) - self.assertEqual(record.date, '2012-05-01') + with self.assertRaises(TypeError): + record.date = datetime(2012, 5, 1, 10, 45, 0) - # one may assign dates in the default format, and it must be checked + # one may assign dates and datetime in the default format, and it must be checked record.date = '2012-05-01' - self.assertEqual(record.date, '2012-05-01') + self.assertEqual(record.date, date(2012, 5, 1)) + + record.date = "2012-05-01 10:45:00" + self.assertEqual(record.date, date(2012, 5, 1)) with self.assertRaises(ValueError): record.date = '12-5-1' + for i in range(0, 10): + self.assertEqual(fields.Datetime.now().microsecond, 0) + + def test_21_date_datetime_helpers(self): + """ test date/datetime fields helpers """ + _date = fields.Date.from_string("2077-10-23") + _datetime = fields.Datetime.from_string("2077-10-23 09:42:00") + + # addition + self.assertEqual(add(_date, days=5), date(2077, 10, 28)) + self.assertEqual(add(_datetime, seconds=10), datetime(2077, 10, 23, 9, 42, 10)) + + # subtraction + self.assertEqual(subtract(_date, months=1), date(2077, 9, 23)) + self.assertEqual(subtract(_datetime, hours=2), datetime(2077, 10, 23, 7, 42, 0)) + + # start_of + # year + self.assertEqual(start_of(_date, 'year'), date(2077, 1, 1)) + self.assertEqual(start_of(_datetime, 'year'), datetime(2077, 1, 1)) + + # quarter + q1 = date(2077, 1, 1) + q2 = date(2077, 4, 1) + q3 = date(2077, 7, 1) + q4 = date(2077, 10, 1) + self.assertEqual(start_of(_date.replace(month=3), 'quarter'), q1) + self.assertEqual(start_of(_date.replace(month=5), 'quarter'), q2) + self.assertEqual(start_of(_date.replace(month=7), 'quarter'), q3) + self.assertEqual(start_of(_date, 'quarter'), q4) + self.assertEqual(start_of(_datetime, 'quarter'), datetime.combine(q4, time.min)) + + # month + self.assertEqual(start_of(_date, 'month'), date(2077, 10, 1)) + self.assertEqual(start_of(_datetime, 'month'), datetime(2077, 10, 1)) + + # week + self.assertEqual(start_of(_date, 'week'), date(2077, 10, 18)) + self.assertEqual(start_of(_datetime, 'week'), datetime(2077, 10, 18)) + + # day + self.assertEqual(start_of(_date, 'day'), _date) + self.assertEqual(start_of(_datetime, 'day'), _datetime.replace(hour=0, minute=0, second=0)) + + # hour + with self.assertRaises(ValueError): + start_of(_date, 'hour') + self.assertEqual(start_of(_datetime, 'hour'), _datetime.replace(minute=0, second=0)) + + # invalid + with self.assertRaises(ValueError): + start_of(_datetime, 'poop') + + # end_of + # year + self.assertEqual(end_of(_date, 'year'), _date.replace(month=12, day=31)) + self.assertEqual(end_of(_datetime, 'year'), + datetime.combine(_date.replace(month=12, day=31), time.max)) + + # quarter + q1 = date(2077, 3, 31) + q2 = date(2077, 6, 30) + q3 = date(2077, 9, 30) + q4 = date(2077, 12, 31) + self.assertEqual(end_of(_date.replace(month=2), 'quarter'), q1) + self.assertEqual(end_of(_date.replace(month=4), 'quarter'), q2) + self.assertEqual(end_of(_date.replace(month=9), 'quarter'), q3) + self.assertEqual(end_of(_date, 'quarter'), q4) + self.assertEqual(end_of(_datetime, 'quarter'), datetime.combine(q4, time.max)) + + # month + self.assertEqual(end_of(_date, 'month'), _date.replace(day=31)) + self.assertEqual(end_of(_datetime, 'month'), + datetime.combine(date(2077, 10, 31), time.max)) + + # week + self.assertEqual(end_of(_date, 'week'), date(2077, 10, 24)) + self.assertEqual(end_of(_datetime, 'week'), + datetime.combine(datetime(2077, 10, 24), time.max)) + + # day + self.assertEqual(end_of(_date, 'day'), _date) + self.assertEqual(end_of(_datetime, 'day'), datetime.combine(_datetime, time.max)) + + # hour + with self.assertRaises(ValueError): + end_of(_date, 'hour') + self.assertEqual(end_of(_datetime, 'hour'), + datetime.combine(_datetime, time.max).replace(hour=_datetime.hour)) + + # invalid + with self.assertRaises(ValueError): + end_of(_datetime, 'crap') + def test_22_selection(self): """ test selection fields """ record = self.env['test_new_api.mixed'].create({}) diff --git a/odoo/fields.py b/odoo/fields.py index 0e7f53fe04cb3..ec3a16b0a7161 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -4,7 +4,8 @@ """ High-level objects for fields. """ from collections import OrderedDict, defaultdict -from datetime import date, datetime +from datetime import date, datetime, time +from dateutil.relativedelta import relativedelta from functools import partial from operator import attrgetter import itertools @@ -21,7 +22,8 @@ import psycopg2 from .sql_db import LazyCursor -from .tools import float_repr, float_round, frozendict, html_sanitize, human_size, pg_varchar, ustr, OrderedSet, pycompat, sql +from .tools import float_repr, float_round, frozendict, html_sanitize, human_size, pg_varchar,\ + ustr, OrderedSet, pycompat, sql, date_utils from .tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT from .tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT from .tools.translate import html_translate, _ @@ -1516,23 +1518,30 @@ class Date(Field): column_type = ('date', 'date') column_cast_from = ('timestamp',) + start_of = staticmethod(date_utils.start_of) + end_of = staticmethod(date_utils.end_of) + add = staticmethod(date_utils.add) + subtract = staticmethod(date_utils.subtract) + @staticmethod def today(*args): """ Return the current day in the format expected by the ORM. This function may be used to compute default values. """ - return date.today().strftime(DATE_FORMAT) + return date.today() @staticmethod def context_today(record, timestamp=None): - """ Return the current date as seen in the client's timezone in a format - fit for date fields. This method may be used to compute default - values. - - :param datetime timestamp: optional datetime value to use instead of - the current date and time (must be a datetime, regular dates - can't be converted between timezones.) - :rtype: str + """ + Return the current date as seen in the client's timezone in a format + fit for date fields. This method may be used to compute default + values. + + :param record: recordset from which the timezone will be obtained. + :param datetime timestamp: optional datetime value to use instead of + the current date and time (must be a datetime, regular dates + can't be converted between timezones). + :rtype: date """ today = timestamp or datetime.now() context_today = None @@ -1544,33 +1553,55 @@ def context_today(record, timestamp=None): except Exception: _logger.debug("failed to compute context/client-specific today date, using UTC value for `today`", exc_info=True) - return (context_today or today).strftime(DATE_FORMAT) + return (context_today or today).date() @staticmethod - def from_string(value): - """ Convert an ORM ``value`` into a :class:`date` value. """ + def to_date(value): + """ + Attempt to convert ``value`` to a :class:`date` object. + + This function can take as input different kinds of types: + * A falsy object, in which case None will be returned. + * A string representing a date or datetime. + * A date object, in which case the object will be returned as-is. + * A datetime object, in which case it will be converted to a date object and all\ + datetime-specific information will be lost (HMS, TZ, ...). + + :param value: value to convert. + :return: an object representing ``value``. + :rtype: date + """ if not value: return None + if isinstance(value, date): + if isinstance(value, datetime): + return value.date() + return value value = value[:DATE_LENGTH] return datetime.strptime(value, DATE_FORMAT).date() + # kept for backwards compatibility, but consider `from_string` as deprecated, will probably + # be removed after V12 + from_string = to_date + @staticmethod def to_string(value): - """ Convert a :class:`date` value into the format expected by the ORM. """ - return value.strftime(DATE_FORMAT) if value else False + """ + Convert a :class:`date` or :class:`datetime` object to a string. - def convert_to_column(self, value, record, values=None, validate=True): - return super(Date, self).convert_to_column(value or None, record, values, validate) + :param value: value to convert. + :return: a string representing ``value`` in the server's date format, if ``value`` is of + type :class:`datetime`, the hours, minute, seconds, tzinfo will be truncated. + :rtype: str + """ + return value.strftime(DATE_FORMAT) if value else False def convert_to_cache(self, value, record, validate=True): if not value: return False - if isinstance(value, pycompat.string_types): - if validate: - # force parsing for validation - self.from_string(value) - return value[:DATE_LENGTH] - return self.to_string(value) + if isinstance(value, datetime): + raise TypeError("%s (field %s) must be string or date, not datetime." % (value, self)) + return self.from_string(value) def convert_to_export(self, value, record): if not value: @@ -1583,26 +1614,40 @@ class Datetime(Field): column_type = ('timestamp', 'timestamp') column_cast_from = ('date',) + start_of = staticmethod(date_utils.start_of) + end_of = staticmethod(date_utils.end_of) + add = staticmethod(date_utils.add) + subtract = staticmethod(date_utils.subtract) + @staticmethod def now(*args): """ Return the current day and time in the format expected by the ORM. This function may be used to compute default values. """ - return datetime.now().strftime(DATETIME_FORMAT) + # microseconds must be annihilated as they don't comply with the server datetime format + return datetime.now().replace(microsecond=0) + + @staticmethod + def today(*args): + """ + Return the current day, at midnight (00:00:00). + """ + return Datetime.now().replace(hour=0, minute=0, second=0) @staticmethod def context_timestamp(record, timestamp): - """Returns the given timestamp converted to the client's timezone. - This method is *not* meant for use as a default initializer, - because datetime fields are automatically converted upon - display on client side. For default values :meth:`fields.datetime.now` - should be used instead. - - :param datetime timestamp: naive datetime value (expressed in UTC) - to be converted to the client timezone - :rtype: datetime - :return: timestamp converted to timezone-aware datetime in context - timezone + """ + Returns the given timestamp converted to the client's timezone. + This method is *not* meant for use as a default initializer, + because datetime fields are automatically converted upon + display on client side. For default values, :meth:`fields.Datetime.now` + should be used instead. + + :param record: recordset from which the timezone will be obtained. + :param datetime timestamp: naive datetime value (expressed in UTC) + to be converted to the client timezone. + :rtype: datetime + :return: timestamp converted to timezone-aware datetime in context timezone. """ assert isinstance(timestamp, datetime), 'Datetime instance expected' tz_name = record._context.get('tz') or record.env.user.tz @@ -1618,35 +1663,53 @@ def context_timestamp(record, timestamp): return utc_timestamp @staticmethod - def from_string(value): - """ Convert an ORM ``value`` into a :class:`datetime` value. """ + def to_datetime(value): + """ + Convert an ORM ``value`` into a :class:`datetime` value. + + This function can take as input different kinds of types: + * A falsy object, in which case None will be returned. + * A string representing a date or datetime. + * A datetime object, in which case the object will be returned as-is. + * A date object, in which case it will be converted to a datetime object. + + :param value: value to convert. + :return: an object representing ``value``. + :rtype: datetime + """ if not value: return None - value = value[:DATETIME_LENGTH] - if len(value) == DATE_LENGTH: - value += " 00:00:00" - return datetime.strptime(value, DATETIME_FORMAT) + if isinstance(value, date): + if isinstance(value, datetime): + return value + return datetime.combine(value, time.min) + try: + return datetime.strptime(value[:DATETIME_LENGTH], DATETIME_FORMAT) + except ValueError: + return datetime.strptime(value, DATE_FORMAT) + + # kept for backwards compatibility, but consider `from_string` as deprecated, will probably + # be removed after V12 + from_string = to_datetime @staticmethod def to_string(value): - """ Convert a :class:`datetime` value into the format expected by the ORM. """ - return value.strftime(DATETIME_FORMAT) if value else False + """ + Convert a :class:`datetime` or :class:`date` object to a string. - def convert_to_column(self, value, record, values=None, validate=True): - return super(Datetime, self).convert_to_column(value or None, record, values, validate) + :param value: value to convert. + :return: a string representing ``value`` in the server's datetime format, if ``value`` is + of type :class:`date`, the time portion will be midnight (00:00:00). + :rtype: str + """ + return value.strftime(DATETIME_FORMAT) if value else False def convert_to_cache(self, value, record, validate=True): if not value: return False - if isinstance(value, pycompat.string_types): - if validate: - # force parsing for validation - self.from_string(value) - value = value[:DATETIME_LENGTH] - if len(value) == DATE_LENGTH: - value += " 00:00:00" - return value - return self.to_string(value) + if isinstance(value, date) and not isinstance(value, datetime): + raise TypeError("%s (field %s) must be string or datetime, not date." % (value, self)) + return self.from_string(value) def convert_to_export(self, value, record): if not value: diff --git a/odoo/http.py b/odoo/http.py index d32b4caa5e303..3d0126dbc4290 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -25,6 +25,7 @@ from zlib import adler32 import babel.core +from datetime import datetime, date import passlib.utils import psycopg2 import json @@ -44,10 +45,11 @@ psutil = None import odoo +from odoo import fields from .service.server import memory_info from .service import security, model as service_model from .tools.func import lazy_property -from .tools import ustr, consteq, frozendict, pycompat, unique +from .tools import ustr, consteq, frozendict, pycompat, unique, date_utils from .modules.module import module_manifest @@ -617,6 +619,7 @@ def handler(): self.context = self.params.pop('context', dict(self.session.context)) def _json_response(self, result=None, error=None): + response = { 'jsonrpc': '2.0', 'id': self.jsonrequest.get('id') @@ -632,10 +635,10 @@ def _json_response(self, result=None, error=None): # We need then to manage http sessions manually. response['session_id'] = self.session.sid mime = 'application/javascript' - body = "%s(%s);" % (self.jsonp, json.dumps(response, default=ustr),) + body = "%s(%s);" % (self.jsonp, json.dumps(response, default=date_utils.json_default)) else: mime = 'application/json' - body = json.dumps(response, default=ustr) + body = json.dumps(response, default=date_utils.json_default) return Response( body, status=error and error.pop('http_status', 200) or 200, diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py index 68d19edf04656..483f325322c63 100644 --- a/odoo/osv/expression.py +++ b/odoo/osv/expression.py @@ -120,6 +120,7 @@ from functools import partial from zlib import crc32 +from datetime import date, datetime, time import odoo.modules from odoo.tools import pycompat from ..models import MAGIC_COLUMNS, BaseModel @@ -1095,12 +1096,22 @@ def _get_expression(comodel, left, right, operator): # ------------------------------------------------- else: - if field.type == 'datetime' and right and len(right) == 10: - if operator in ('>', '<='): - right += ' 23:59:59' + if field.type == 'datetime' and right: + if isinstance(right, pycompat.string_types) and len(right) == 10: + if operator in ('>', '<='): + right += ' 23:59:59' + else: + right += ' 00:00:00' + push(create_substitution_leaf(leaf, (left, operator, right), model)) + elif isinstance(right, date) and not isinstance(right, datetime): + if operator in ('>', '<='): + right = datetime.combine(right, time.max) + else: + right = datetime.combine(right, time.min) + push(create_substitution_leaf(leaf, (left, operator, right), model)) else: - right += ' 00:00:00' - push(create_substitution_leaf(leaf, (left, operator, right), model)) + push_result(leaf) + elif field.translate is True and right: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') diff --git a/odoo/sql_db.py b/odoo/sql_db.py index 6d96fb8aa7412..ae1db0d64361d 100644 --- a/odoo/sql_db.py +++ b/odoo/sql_db.py @@ -26,12 +26,6 @@ _logger = logging.getLogger(__name__) -types_mapping = { - 'date': (1082,), - 'time': (1083,), - 'datetime': (1114,), -} - def unbuffer(symb, cr): if symb is None: return None @@ -42,8 +36,6 @@ def undecimalize(symb, cr): return None return float(symb) -for name, typeoid in types_mapping.items(): - psycopg2.extensions.register_type(psycopg2.extensions.new_type(typeoid, name, lambda x, cr: x)) psycopg2.extensions.register_type(psycopg2.extensions.new_type((700, 701, 1700,), 'float', undecimalize)) diff --git a/odoo/tools/date_utils.py b/odoo/tools/date_utils.py index 76b19c2cf5501..e7d1aa24ffddc 100644 --- a/odoo/tools/date_utils.py +++ b/odoo/tools/date_utils.py @@ -2,8 +2,10 @@ import math import calendar +from datetime import date, datetime, time from dateutil.relativedelta import relativedelta +from . import ustr def get_month(date): @@ -62,3 +64,117 @@ def get_fiscal_year(date, day=31, month=12): max_day = calendar.monthrange(date_to.year + 1, date_to.month)[1] date_to = type(date)(date.year + 1, month, min(day, max_day)) return date_from, date_to + + +def start_of(value, granularity): + """ + Get start of a time period from a date or a datetime. + + :param value: initial date or datetime. + :param granularity: type of period in string, can be year, quarter, month, week, day or hour. + :return: a date/datetime object corresponding to the start of the specified period. + """ + is_datetime = isinstance(value, datetime) + if granularity == "year": + result = value.replace(month=1, day=1) + elif granularity == "quarter": + # Q1 = Jan 1st + # Q2 = Apr 1st + # Q3 = Jul 1st + # Q4 = Oct 1st + result = get_quarter(value)[0] + elif granularity == "month": + result = value.replace(day=1) + elif granularity == 'week': + # `calendar.weekday` uses ISO8601 for start of week reference, this means that + # by default MONDAY is the first day of the week and SUNDAY is the last. + result = value - relativedelta(days=calendar.weekday(value.year, value.month, value.day)) + elif granularity == "day": + result = value + elif granularity == "hour" and is_datetime: + return datetime.combine(value, time.min).replace(hour=value.hour) + elif is_datetime: + raise ValueError( + "Granularity must be year, quarter, month, week, day or hour for value %s" % value + ) + else: + raise ValueError( + "Granularity must be year, quarter, month, week or day for value %s" % value + ) + + return datetime.combine(result, time.min) if is_datetime else result + + +def end_of(value, granularity): + """ + Get end of a time period from a date or a datetime. + + :param value: initial date or datetime. + :param granularity: Type of period in string, can be year, quarter, month, week, day or hour. + :return: A date/datetime object corresponding to the start of the specified period. + """ + is_datetime = isinstance(value, datetime) + if granularity == "year": + result = value.replace(month=12, day=31) + elif granularity == "quarter": + # Q1 = Mar 31st + # Q2 = Jun 30th + # Q3 = Sep 30th + # Q4 = Dec 31st + result = get_quarter(value)[1] + elif granularity == "month": + result = value + relativedelta(day=1, months=1, days=-1) + elif granularity == 'week': + # `calendar.weekday` uses ISO8601 for start of week reference, this means that + # by default MONDAY is the first day of the week and SUNDAY is the last. + result = value + relativedelta(days=6-calendar.weekday(value.year, value.month, value.day)) + elif granularity == "day": + result = value + elif granularity == "hour" and is_datetime: + return datetime.combine(value, time.max).replace(hour=value.hour) + elif is_datetime: + raise ValueError( + "Granularity must be year, quarter, month, week, day or hour for value %s" % value + ) + else: + raise ValueError( + "Granularity must be year, quarter, month, week or day for value %s" % value + ) + + return datetime.combine(result, time.max) if is_datetime else result + + +def add(value, *args, **kwargs): + """ + Return the sum of ``value`` and a :class:`relativedelta`. + + :param value: initial date or datetime. + :param args: positional args to pass directly to :class:`relativedelta`. + :param kwargs: keyword args to pass directly to :class:`relativedelta`. + :return: the resulting date/datetime. + """ + return value + relativedelta(*args, **kwargs) + + +def subtract(value, *args, **kwargs): + """ + Return the difference between ``value`` and a :class:`relativedelta`. + + :param value: initial date or datetime. + :param args: positional args to pass directly to :class:`relativedelta`. + :param kwargs: keyword args to pass directly to :class:`relativedelta`. + :return: the resulting date/datetime. + """ + return value - relativedelta(*args, **kwargs) + + +def json_default(obj): + """ + Properly serializes date and datetime objects. + """ + from odoo import fields + if isinstance(obj, date): + if isinstance(obj, datetime): + return fields.Datetime.to_string(obj) + return fields.Date.to_string(obj) + return ustr(obj)