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 @@
+
+
+