diff --git a/addons/purchase/controllers/portal.py b/addons/purchase/controllers/portal.py index 92136935681..600f7b8f751 100644 --- a/addons/purchase/controllers/portal.py +++ b/addons/purchase/controllers/portal.py @@ -3,10 +3,11 @@ import base64 from collections import OrderedDict +from datetime import datetime from odoo import http from odoo.exceptions import AccessError, MissingError -from odoo.http import request +from odoo.http import request, Response from odoo.tools import image_process from odoo.tools.translate import _ from odoo.addons.portal.controllers.portal import pager as portal_pager, CustomerPortal @@ -134,17 +135,22 @@ class CustomerPortal(CustomerPortal): return request.redirect('/my') updated_dates = [] - try: - for id_str, date in kw.items(): - line = order_sudo.order_line.filtered(lambda l: l.id == int(id_str)) - if not line: - return request.redirect(order_sudo.get_portal_url()) - updated_dates.append((line, date)) - except ValueError: - return request.redirect(order_sudo.get_portal_url()) + for id_str, date_str in kw.items(): + try: + line_id = int(id_str) + except ValueError: + return request.redirect(order_sudo.get_portal_url()) + line = order_sudo.order_line.filtered(lambda l: l.id == line_id) + if not line: + return request.redirect(order_sudo.get_portal_url()) + + try: + updated_date = line._convert_to_last_minute_of_day(datetime.strptime(date_str, '%Y-%m-%d')) + except ValueError: + continue + + updated_dates.append((line, updated_date)) if updated_dates: order_sudo._update_date_planned_for_lines(updated_dates) - - values = self._purchase_order_get_page_view_values(order_sudo, access_token, **kw) - return request.render("purchase.portal_my_purchase_order", values) + return Response(status=204) diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index d2a43bea5bd..dc233ce9498 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -711,30 +711,39 @@ class PurchaseOrder(models.Model): order.message_post(body="The order receipt has been acknowledged by %s." % order.partner_id.name) def _update_date_planned_for_lines(self, updated_dates): - if updated_dates: - note = self._compose_note(updated_dates) - self.activity_schedule( - 'mail.mail_activity_data_warning', - summary="Date Updated", - note=note, - user_id=self.user_id.id - ) + # create or update the activity + activity = self.env['mail.activity'].search([ + ('summary', '=', _('Date Updated')), + ('res_model_id', '=', 'purchase.order'), + ('res_id', '=', self.id), + ('user_id', '=', self.user_id.id)], limit=1) + if activity: + self._update_update_date_activity(updated_dates, activity) + else: + self._create_update_date_activity(updated_dates) - for line, date in updated_dates: - line._update_date_planned(date) - - def _compose_note(self, updated_dates): - """Helper method for creating log note when user update scheduled date - on portal website.""" - note = _('
%s modified receipt dates for the following products:
', self.partner_id.name) + # update the date on PO line for line, date in updated_dates: - note += _( - '- %(product_name)s from %(date_start)s to %(date_end)s
', - product_name=line.product_id.display_name, - date_start=line.date_planned, - date_end=date - ) - return note + line._update_date_planned(date) + + def _create_update_date_activity(self, updated_dates): + note = _('%s modified receipt dates for the following products:
') % self.partner_id.name + for line, date in updated_dates: + note += _('- %s from %s to %s
') % (line.product_id.display_name, line.date_planned.date(), date.date()) + activity = self.activity_schedule( + 'mail.mail_activity_data_warning', + summary=_("Date Updated"), + user_id=self.user_id.id + ) + # add the note after we post the activity because the note can be soon + # changed when updating the date of the next PO line. So instead of + # sending a mail with incomplete note, we send one with no note. + activity.note = note + return activity + + def _update_update_date_activity(self, updated_dates, activity): + for line, date in updated_dates: + activity.note += _('- %s from %s to %s
') % (line.product_id.display_name, line.date_planned.date(), date.date()) class PurchaseOrderLine(models.Model): @@ -936,7 +945,7 @@ class PurchaseOrderLine(models.Model): date_planned = date_order + relativedelta(days=seller.delay if seller else 0) else: date_planned = datetime.today() + relativedelta(days=seller.delay if seller else 0) - return timezone(self.order_id.user_id.tz or 'UTC').localize(datetime.combine(date_planned.date(), time.max)).astimezone(UTC).replace(tzinfo=None, microsecond=0, second=0) + return self._convert_to_last_minute_of_day(date_planned) @api.depends('product_id', 'date_order') def _compute_analytic_id_and_tag_ids(self): @@ -957,7 +966,7 @@ class PurchaseOrderLine(models.Model): return # Reset date, price and quantity since _onchange_quantity will provide default values - self.date_planned = self.order_id.date_planned or timezone(self.order_id.user_id.tz or 'UTC').localize(datetime.combine(datetime.today(), time.max)).astimezone(UTC).replace(tzinfo=None, microsecond=0, second=0) + self.date_planned = self.order_id.date_planned or self._convert_to_last_minute_of_day(datetime.today()) self.price_unit = self.product_qty = 0.0 self._product_id_change() @@ -1147,5 +1156,11 @@ class PurchaseOrderLine(models.Model): 'order_id': po.id, } + def _convert_to_last_minute_of_day(self, date): + """Return a datetime which is the last minute of the input date(time) + according to order user's time zone, convert to UTC time. + """ + return timezone(self.order_id.user_id.tz or 'UTC').localize(datetime.combine(date, time.max)).astimezone(UTC).replace(tzinfo=None, microsecond=0, second=0) + def _update_date_planned(self, updated_date): self.date_planned = updated_date diff --git a/addons/purchase/static/src/js/purchase_datetimepicker.js b/addons/purchase/static/src/js/purchase_datetimepicker.js index c2aaad196c4..de1be92f6df 100644 --- a/addons/purchase/static/src/js/purchase_datetimepicker.js +++ b/addons/purchase/static/src/js/purchase_datetimepicker.js @@ -1,3 +1,6 @@ $(function () { $('input.o-purchase-datetimepicker').datetimepicker(); + $('input.o-purchase-datetimepicker').on("hide.datetimepicker", function () { + $(this).parents('form').submit(); + }); }) diff --git a/addons/purchase/tests/test_purchase.py b/addons/purchase/tests/test_purchase.py index 19734a815fa..b02154520b0 100644 --- a/addons/purchase/tests/test_purchase.py +++ b/addons/purchase/tests/test_purchase.py @@ -144,3 +144,41 @@ class TestPurchase(SavepointCase): messages_send = po.message_ids - old_messages # check no reminder send self.assertFalse(messages_send) + + def test_update_date_planned(self): + po = Form(self.env['purchase.order']) + po.partner_id = self.vendor + with po.order_line.new() as po_line: + po_line.product_id = self.product_consu + po_line.product_qty = 1 + po_line.price_unit = 100 + po_line.date_planned = '2020-06-06 00:00:00' + with po.order_line.new() as po_line: + po_line.product_id = self.product_consu2 + po_line.product_qty = 10 + po_line.price_unit = 200 + po_line.date_planned = '2020-06-06 00:00:00' + po = po.save() + po.button_confirm() + + # update first line + po._update_date_planned_for_lines([(po.order_line[0], fields.Datetime.today())]) + self.assertEqual(po.order_line[0].date_planned, fields.Datetime.today()) + activity = self.env['mail.activity'].search([ + ('summary', '=', 'Date Updated'), + ('res_model_id', '=', 'purchase.order'), + ('res_id', '=', po.id), + ]) + self.assertTrue(activity) + self.assertEqual( + activity.note, + 'vendor1 modified receipt dates for the following products:
\xa0 - Product A from 2020-06-06 to %s
' % fields.Date.today() + ) + + # update second line + po._update_date_planned_for_lines([(po.order_line[1], fields.Datetime.today())]) + self.assertEqual(po.order_line[1].date_planned, fields.Datetime.today()) + self.assertEqual( + activity.note, + 'vendor1 modified receipt dates for the following products:
\xa0 - Product A from 2020-06-06 to %s
\xa0 - Product B from 2020-06-06 to %s
' % (fields.Date.today(), fields.Date.today()) + ) diff --git a/addons/purchase/views/portal_templates.xml b/addons/purchase/views/portal_templates.xml index 0618c182943..5ec71184b6d 100644 --- a/addons/purchase/views/portal_templates.xml +++ b/addons/purchase/views/portal_templates.xml @@ -224,10 +224,6 @@Those dates couldn’t be modified accordingly on the receipt %s which had already been validated.
", validated_picking.name) + activity.note += _("Those dates couldn’t be modified accordingly on the receipt %s which had already been validated.
") % validated_picking[0].name + elif not self.picking_ids: + activity.note += _("Corresponding receipt not found.
") else: - note += _("Those dates have been updated accordingly on the receipt %s.
", self.picking_ids[0].name) - for line, date in updated_dates: - date = datetime.strptime(date, '%Y-%m-%d %H:%M:%S') - line._update_date_planned(date) + activity.note += _("Those dates have been updated accordingly on the receipt %s.
") % self.picking_ids[0].name - self.activity_schedule( - 'mail.mail_activity_data_warning', - summary="Date Updated", - note=note, - user_id=self.user_id.id or SUPERUSER_ID - ) + def _create_update_date_activity(self, updated_dates): + activity = super()._create_update_date_activity(updated_dates) + self._add_picking_info(activity) + + def _update_update_date_activity(self, updated_dates, activity): + # remove old picking info to update it + note_lines = activity.note.split('') + note_lines.pop() + activity.note = '
'.join(note_lines) + super()._update_update_date_activity(updated_dates, activity) + self._add_picking_info(activity) @api.model def _get_orders_to_remind(self): diff --git a/addons/purchase_stock/tests/test_purchase_order.py b/addons/purchase_stock/tests/test_purchase_order.py index 4f9060ba1fd..333a53e8bf6 100644 --- a/addons/purchase_stock/tests/test_purchase_order.py +++ b/addons/purchase_stock/tests/test_purchase_order.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime +from datetime import datetime, timedelta from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from odoo.addons.account.tests.common import AccountTestCommon @@ -224,3 +224,37 @@ class TestPurchaseOrder(AccountTestCommon): self.po.button_confirm() self.assertFalse(self.po.order_line.mapped('move_dest_ids')) self.assertEqual(self.po.picking_ids.move_lines[0].propagate_date, warehouse.buy_pull_id.propagate_date) + + def test_update_date_planned(self): + po = self.PurchaseOrder.create(self.po_vals) + po.button_confirm() + + today = datetime.today().replace(microsecond=0) + tomorrow = datetime.today().replace(microsecond=0) + timedelta(days=1) + # update first line + po._update_date_planned_for_lines([(po.order_line[0], tomorrow)]) + self.assertEqual(po.order_line[0].date_planned, tomorrow) + activity = self.env['mail.activity'].search([ + ('summary', '=', 'Date Updated'), + ('res_model_id', '=', 'purchase.order'), + ('res_id', '=', po.id), + ]) + self.assertTrue(activity) + self.assertEqual( + activity.note, + '
Wood Corner Partner modified receipt dates for the following products:
\xa0 - Large Desk from %s to %s
Those dates have been updated accordingly on the receipt %s.
' % (today.date(), tomorrow.date(), po.picking_ids.name) + ) + + # receive products + wiz_act = po.picking_ids.button_validate() + wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save() + wiz.process() + + # update second line + old_date = po.order_line[1].date_planned + po._update_date_planned_for_lines([(po.order_line[1], tomorrow)]) + self.assertEqual(po.order_line[1].date_planned, old_date) + self.assertEqual( + activity.note, + 'Wood Corner Partner modified receipt dates for the following products:
\xa0 - Large Desk from %s to %s
\xa0 - Conference Chair from %s to %s
Those dates couldn’t be modified accordingly on the receipt %s which had already been validated.
' % (today.date(), tomorrow.date(), today.date(), tomorrow.date(), po.picking_ids.name) + )