From 181c7d82e30d0848bbac7f7d0188e81aced0af07 Mon Sep 17 00:00:00 2001
From: yhu-odoo
Date: Tue, 21 Apr 2020 15:22:46 +0000
Subject: [PATCH] [IMP] purchase: send reminder mail to vendor
1. automatically send a reminder mail to vendor to confirm the receipt
date. If confirmed, (confirmed by vendor) will be added next to the
receipt date. If not, vendor can update the date on the portal website.
An warning activity will be set for the purchase representative for this
update.
2. Vendor can also comfirm recieption of the PO when we 'send PO by mail'.
If confirm, (confirmed by vendor) will be added next to the confirmation
date. An filter is added in the PO search view to show all unconfirmed
PO.
Task 2230811
PR #49921
Signed-off-by: Simon Lejeune (sle)
---
addons/purchase/__manifest__.py | 1 +
addons/purchase/controllers/portal.py | 38 ++++++-
addons/purchase/data/ir_cron_data.xml | 15 +++
addons/purchase/data/mail_template_data.xml | 81 ++++++++++++++-
addons/purchase/data/purchase_data.xml | 14 +++
addons/purchase/models/purchase.py | 99 ++++++++++++++++++-
addons/purchase/models/res_config_settings.py | 2 +
addons/purchase/models/res_partner.py | 5 +
.../purchase/security/purchase_security.xml | 9 ++
.../static/src/js/purchase_datetimepicker.js | 3 +
addons/purchase/tests/test_access_rights.py | 3 +-
addons/purchase/views/assets.xml | 6 ++
addons/purchase/views/portal_templates.xml | 96 ++++++++++++++++++
addons/purchase/views/purchase_views.xml | 38 ++++---
.../views/res_config_settings_views.xml | 11 +++
addons/purchase/views/res_partner_views.xml | 6 +-
addons/purchase_stock/models/purchase.py | 32 ++++++
.../report/vendor_delay_report.xml | 2 -
.../purchase_stock/views/purchase_views.xml | 34 ++++---
19 files changed, 454 insertions(+), 41 deletions(-)
create mode 100644 addons/purchase/data/ir_cron_data.xml
create mode 100644 addons/purchase/static/src/js/purchase_datetimepicker.js
diff --git a/addons/purchase/__manifest__.py b/addons/purchase/__manifest__.py
index 426f628dfcf..42e8a832961 100644
--- a/addons/purchase/__manifest__.py
+++ b/addons/purchase/__manifest__.py
@@ -16,6 +16,7 @@
'views/assets.xml',
'views/account_move_views.xml',
'data/purchase_data.xml',
+ 'data/ir_cron_data.xml',
'report/purchase_reports.xml',
'views/purchase_views.xml',
'views/res_config_settings_views.xml',
diff --git a/addons/purchase/controllers/portal.py b/addons/purchase/controllers/portal.py
index 402a6700dd3..92136935681 100644
--- a/addons/purchase/controllers/portal.py
+++ b/addons/purchase/controllers/portal.py
@@ -13,7 +13,6 @@ from odoo.addons.portal.controllers.portal import pager as portal_pager, Custome
from odoo.addons.web.controllers.main import Binary
-
class CustomerPortal(CustomerPortal):
def _prepare_portal_layout_values(self):
@@ -111,6 +110,41 @@ class CustomerPortal(CustomerPortal):
report_type = kw.get('report_type')
if report_type in ('html', 'pdf', 'text'):
- return self._show_report(model=order_sudo, report_type=report_type, report_ref='purchase.action_report_purchase_order',download=kw.get('download'))
+ return self._show_report(model=order_sudo, report_type=report_type, report_ref='purchase.action_report_purchase_order', download=kw.get('download'))
+
+ confirm_type = kw.get('confirm')
+ if confirm_type == 'reminder':
+ order_sudo.confirm_reminder_mail(kw.get('confirmed_date'))
+ if confirm_type == 'reception':
+ order_sudo._confirm_reception_mail()
+
+ values = self._purchase_order_get_page_view_values(order_sudo, access_token, **kw)
+ update_date = kw.get('update')
+ if update_date == 'True':
+ return request.render("purchase.portal_my_purchase_order_update_date", values)
+ return request.render("purchase.portal_my_purchase_order", values)
+
+ @http.route(['/my/purchase//update'], type='http', methods=['POST'], auth="public", website=True)
+ def portal_my_purchase_order_update_dates(self, order_id=None, access_token=None, **kw):
+ """User update scheduled date on purchase order line.
+ """
+ try:
+ order_sudo = self._document_check_access('purchase.order', order_id, access_token=access_token)
+ except (AccessError, MissingError):
+ 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())
+
+ 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)
diff --git a/addons/purchase/data/ir_cron_data.xml b/addons/purchase/data/ir_cron_data.xml
new file mode 100644
index 00000000000..3c70792ceb6
--- /dev/null
+++ b/addons/purchase/data/ir_cron_data.xml
@@ -0,0 +1,15 @@
+
+
+
+ Purchase reminder
+
+
+ 1
+ days
+ -1
+ 1
+
+ code
+ model._send_reminder_mail()
+
+
diff --git a/addons/purchase/data/mail_template_data.xml b/addons/purchase/data/mail_template_data.xml
index dff26e49227..35bd59cf12c 100644
--- a/addons/purchase/data/mail_template_data.xml
+++ b/addons/purchase/data/mail_template_data.xml
@@ -49,11 +49,17 @@
with reference: ${object.partner_ref}
% endif
amounting in ${format_amount(object.amount_total, object.currency_id)}
- from ${object.company_id.name}.
-
- If you have any questions, please do not hesitate to contact us.
-
- Best regards,
+ from ${object.company_id.name}.
+ % if object.date_planned or object.expected_date:
+ The receipt is planned for
+ % if object.date_planned:
+ ${object.date_planned.date()}.
+ % elif object.expected_date:
+ ${object.expected_date.date()}.
+ % endif
+
+ Could you please acknowledge the receipt of this order?
+ % endif
@@ -61,5 +67,70 @@
${object.partner_id.lang}
+
+
+ Purchase Order: Vendor Reminder
+
+ ${(object.user_id.email_formatted or user.email_formatted) |safe}
+ ${object.company_id.name} Order (Ref ${object.name or 'n/a' })
+ ${object.partner_id.id}
+
+
+ Here is a reminder that the delivery of the purchase order ${object.name} is planned for
+ % if object.date_planned:
+ ${object.date_planned.date()}.
+ % else:
+ ${object.expected_date.date()}.
+ % endif
+ Could you please confirm it will be on time?
+
+ If you have any questions, please do not hesitate to contact us.
+
+ Best regards,
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/purchase/data/purchase_data.xml b/addons/purchase/data/purchase_data.xml
index d4360ef74d4..fa1af422096 100644
--- a/addons/purchase/data/purchase_data.xml
+++ b/addons/purchase/data/purchase_data.xml
@@ -36,5 +36,19 @@
codeaction = records.action_share()
+
+
+
+ receipt_reminder_email
+
+
+
+
+
+ reminder_date_before_receipt
+
+
+
+
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index 5fbb236777e..ff000500ac7 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
from datetime import datetime
from dateutil.relativedelta import relativedelta
from itertools import groupby
+from werkzeug.urls import url_encode
from odoo import api, fields, models, SUPERUSER_ID, _
from odoo.osv import expression
@@ -127,6 +127,14 @@ class PurchaseOrder(models.Model):
company_id = fields.Many2one('res.company', 'Company', required=True, index=True, states=READONLY_STATES, default=lambda self: self.env.company.id)
currency_rate = fields.Float("Currency Rate", compute='_compute_currency_rate', compute_sudo=True, store=True, readonly=True, help='Ratio between the purchase order currency and the company currency')
+ mail_reminder_confirmed = fields.Boolean("Reminder Confirmed", default=False, readonly=True, copy=False, help="True if the reminder email is confirmed by the vendor.")
+ mail_reception_confirmed = fields.Boolean("Reception Confirmed", default=False, readonly=True, copy=False, help="True if PO reception is confirmed by the vendor.")
+
+ receipt_reminder_email = fields.Boolean('Receipt Reminder Email', related='partner_id.receipt_reminder_email', readonly=False,
+ help='Automatically send a reminder email to your vendors before receipt')
+ reminder_date_before_receipt = fields.Integer('Days Before Receipt', related='partner_id.reminder_date_before_receipt', readonly=False,
+ help="Number of days to send reminder email before the promised receipt date")
+
@api.constrains('company_id', 'order_line')
def _check_order_line_company_id(self):
for order in self:
@@ -178,6 +186,11 @@ class PurchaseOrder(models.Model):
result.append((po.id, name))
return result
+ @api.onchange('date_planned')
+ def onchange_date_planned(self):
+ if self.date_planned:
+ self.order_line.date_planned = self.date_planned
+
@api.model
def create(self, vals):
if vals.get('name', 'New') == 'New':
@@ -361,6 +374,8 @@ class PurchaseOrder(models.Model):
order.button_approve()
else:
order.write({'state': 'to approve'})
+ if order.partner_id not in order.message_partner_ids:
+ order.message_subscribe([order.partner_id.id])
return True
def button_cancel(self):
@@ -613,6 +628,79 @@ class PurchaseOrder(models.Model):
return result
+ @api.model
+ def _send_reminder_mail(self):
+ if not self.user_has_groups('purchase.group_send_reminder') and not self.receipt_reminder_email:
+ return
+
+ template = self.env.ref('purchase.email_template_edi_purchase_reminder', raise_if_not_found=False)
+ if template:
+ for order in self._get_orders_to_remind():
+ date = order.date_planned or order.expected_date
+ if date and (date - relativedelta(days=order.reminder_date_before_receipt)).date() == datetime.today().date():
+ order.with_context(is_reminder=True).message_post_with_template(template.id, email_layout_xmlid="mail.mail_notification_paynow", composition_mode='comment')
+
+ @api.model
+ def _get_orders_to_remind(self):
+ """When auto sending a reminder mail, only send for unconfirmed purchase
+ order and not all products are service."""
+ return self.search([
+ ('state', 'in', ['purchase', 'done']),
+ ('mail_reminder_confirmed', '=', False)
+ ]).filtered(lambda p: p.mapped('order_line.product_id.product_tmpl_id.type') != ['service'])
+
+ def get_confirm_url(self, confirm_type=None):
+ """Create url for confirm reminder or purchase reception email for sending
+ in mail."""
+ if confirm_type in ['reminder', 'reception']:
+ param = url_encode({
+ 'confirm': confirm_type,
+ 'confirmed_date': (self.date_planned or self.expected_date).date(),
+ })
+ return self.get_portal_url(query_string='&%s' % param)
+ return self.get_portal_url()
+
+ def get_update_url(self):
+ """Create portal url for user to update the scheduled date on purchase
+ order lines."""
+ update_param = url_encode({'update': 'True'})
+ return self.get_portal_url(query_string='&%s' % update_param)
+
+ def confirm_reminder_mail(self, confirmed_date=False):
+ for order in self:
+ if order.state in ['purchase', 'done'] and not order.mail_reminder_confirmed:
+ order.mail_reminder_confirmed = True
+ date = confirmed_date or (self.date_planned or self.expected_date).date()
+ order.message_post(body="%s confirmed the receipt will take place on %s." % (order.partner_id.name, date))
+
+ def _confirm_reception_mail(self):
+ for order in self:
+ if order.state in ['purchase', 'done'] and not order.mail_reception_confirmed:
+ order.mail_reception_confirmed = True
+ 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
+ )
+
+ 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
+ for line, date in updated_dates:
+ note += _('
- %s from %s to %s
') % (line.product_id.display_name, line.date_planned, date)
+ return note
+
+
class PurchaseOrderLine(models.Model):
_name = 'purchase.order.line'
_description = 'Purchase Order Line'
@@ -832,7 +920,7 @@ class PurchaseOrderLine(models.Model):
return
# Reset date, price and quantity since _onchange_quantity will provide default values
- self.date_planned = datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+ self.date_planned = self.order_id.date_planned or datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT)
self.price_unit = self.product_qty = 0.0
self._product_id_change()
@@ -887,7 +975,7 @@ class PurchaseOrderLine(models.Model):
params=params)
if seller or not self.date_planned:
- self.date_planned = self._get_date_planned(seller).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+ self.date_planned = self.order_id.date_planned or self._get_date_planned(seller).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
if not seller:
self.price_unit = self.product_id.standard_price
@@ -1009,7 +1097,7 @@ class PurchaseOrderLine(models.Model):
if product_lang.description_purchase:
name += '\n' + product_lang.description_purchase
- date_planned = self._get_date_planned(seller, po=po)
+ date_planned = self.order_id.date_planned or self._get_date_planned(seller, po=po)
return {
'name': name,
@@ -1021,3 +1109,6 @@ class PurchaseOrderLine(models.Model):
'taxes_id': [(6, 0, taxes_id.ids)],
'order_id': po.id,
}
+
+ def _update_date_planned(self, updated_date):
+ self.date_planned = updated_date
diff --git a/addons/purchase/models/res_config_settings.py b/addons/purchase/models/res_config_settings.py
index fc7a80d6f54..f289b9c4317 100644
--- a/addons/purchase/models/res_config_settings.py
+++ b/addons/purchase/models/res_config_settings.py
@@ -30,6 +30,8 @@ class ResConfigSettings(models.TransientModel):
config_parameter='purchase.use_po_lead',
help="Margin of error for vendor lead times. When the system generates Purchase Orders for reordering products,they will be scheduled that many days earlier to cope with unexpected vendor delays.")
+ group_send_reminder = fields.Boolean("Receipt Reminder", implied_group='purchase.group_send_reminder', default=True, help="Allow automatically send email to reminder your vendor the receipt date")
+
@api.onchange('use_po_lead')
def _onchange_use_po_lead(self):
if not self.use_po_lead:
diff --git a/addons/purchase/models/res_partner.py b/addons/purchase/models/res_partner.py
index a65bc899944..69c7bc1be48 100644
--- a/addons/purchase/models/res_partner.py
+++ b/addons/purchase/models/res_partner.py
@@ -59,3 +59,8 @@ class res_partner(models.Model):
supplier_invoice_count = fields.Integer(compute='_compute_supplier_invoice_count', string='# Vendor Bills')
purchase_warn = fields.Selection(WARNING_MESSAGE, 'Purchase Order', help=WARNING_HELP, default="no-message")
purchase_warn_msg = fields.Text('Message for Purchase Order')
+
+ receipt_reminder_email = fields.Boolean('Receipt Reminder Email', default=True, company_dependent=True,
+ help='Automatically send a reminder email to your vendors before receipt')
+ reminder_date_before_receipt = fields.Integer('Days Before Receipt', default=1, company_dependent=True,
+ help="Number of days to send reminder email before the promised receipt date")
diff --git a/addons/purchase/security/purchase_security.xml b/addons/purchase/security/purchase_security.xml
index 1187e1769c3..7edf1066426 100644
--- a/addons/purchase/security/purchase_security.xml
+++ b/addons/purchase/security/purchase_security.xml
@@ -25,8 +25,17 @@
+
+ Send an automatic reminder email to confirm delivery
+
+
+
+
+
+
+
diff --git a/addons/purchase/static/src/js/purchase_datetimepicker.js b/addons/purchase/static/src/js/purchase_datetimepicker.js
new file mode 100644
index 00000000000..c2aaad196c4
--- /dev/null
+++ b/addons/purchase/static/src/js/purchase_datetimepicker.js
@@ -0,0 +1,3 @@
+$(function () {
+ $('input.o-purchase-datetimepicker').datetimepicker();
+})
diff --git a/addons/purchase/tests/test_access_rights.py b/addons/purchase/tests/test_access_rights.py
index d7251f4d007..b895523cecc 100644
--- a/addons/purchase/tests/test_access_rights.py
+++ b/addons/purchase/tests/test_access_rights.py
@@ -15,6 +15,7 @@ class TestPurchaseInvoice(SavepointCase):
# Create a users
group_purchase_user = cls.env.ref('purchase.group_purchase_user')
group_employee = cls.env.ref('base.group_user')
+ group_partner_manager = cls.env.ref('base.group_partner_manager')
cls.purchase_user = cls.env['res.users'].with_context(
no_reset_password=True
@@ -22,7 +23,7 @@ class TestPurchaseInvoice(SavepointCase):
'name': 'Purchase user',
'login': 'purchaseUser',
'email': 'pu@odoo.com',
- 'groups_id': [(6, 0, [group_purchase_user.id, group_employee.id])],
+ 'groups_id': [(6, 0, [group_purchase_user.id, group_employee.id, group_partner_manager.id])],
})
cls.vendor = cls.env['res.partner'].create({
diff --git a/addons/purchase/views/assets.xml b/addons/purchase/views/assets.xml
index 1532d8b70eb..321a34cb065 100644
--- a/addons/purchase/views/assets.xml
+++ b/addons/purchase/views/assets.xml
@@ -9,4 +9,10 @@
+
+
+
+
+
+
diff --git a/addons/purchase/views/portal_templates.xml b/addons/purchase/views/portal_templates.xml
index e9553037b0b..0618c182943 100644
--- a/addons/purchase/views/portal_templates.xml
+++ b/addons/purchase/views/portal_templates.xml
@@ -214,4 +214,100 @@
+
+
+