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

+ Dear ${object.partner_id.name} + % if object.partner_id.parent_id: + (${object.partner_id.parent_id.name}) + % endif +

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

+
+ + PO_${(object.name or '').replace('/','_')} + ${object.partner_id.lang} + +
+ + + 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 @@ code action = 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 @@ + + diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml index e3188e424a1..a90aafc226c 100644 --- a/addons/purchase/views/purchase_views.xml +++ b/addons/purchase/views/purchase_views.xml @@ -143,6 +143,7 @@ - +
+ +
+ +
@@ -37,12 +40,15 @@ - - - {'invisible': [('effective_date', '!=', False), ('date_planned', '!=', False)]} - + +
+ + Ask confirmation + + days before +
- +