summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoryhu-odoo <yhu@odoo.com>2020-04-21 15:22:46 +0000
committeryhu-odoo <yhu@odoo.com>2020-05-27 08:20:17 +0000
commit181c7d82e30d0848bbac7f7d0188e81aced0af07 (patch)
treed5868d318629c09ebb4da9d70ac2316d66fde338
parentd83334ef8ee566e56ad8b25264d66704120ba598 (diff)
[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) <sle@openerp.com>
-rw-r--r--addons/purchase/__manifest__.py1
-rw-r--r--addons/purchase/controllers/portal.py38
-rw-r--r--addons/purchase/data/ir_cron_data.xml15
-rw-r--r--addons/purchase/data/mail_template_data.xml79
-rw-r--r--addons/purchase/data/purchase_data.xml14
-rw-r--r--addons/purchase/models/purchase.py99
-rw-r--r--addons/purchase/models/res_config_settings.py2
-rw-r--r--addons/purchase/models/res_partner.py5
-rw-r--r--addons/purchase/security/purchase_security.xml9
-rw-r--r--addons/purchase/static/src/js/purchase_datetimepicker.js3
-rw-r--r--addons/purchase/tests/test_access_rights.py3
-rw-r--r--addons/purchase/views/assets.xml6
-rw-r--r--addons/purchase/views/portal_templates.xml96
-rw-r--r--addons/purchase/views/purchase_views.xml38
-rw-r--r--addons/purchase/views/res_config_settings_views.xml11
-rw-r--r--addons/purchase/views/res_partner_views.xml6
-rw-r--r--addons/purchase_stock/models/purchase.py32
-rw-r--r--addons/purchase_stock/report/vendor_delay_report.xml2
-rw-r--r--addons/purchase_stock/views/purchase_views.xml34
19 files changed, 453 insertions, 40 deletions
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/<int:order_id>/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 @@
+<?xml version='1.0' encoding='utf-8'?>
+<odoo>
+ <record forcecreate="True" id="purchase_send_reminder_mail" model="ir.cron">
+ <field name="name">Purchase reminder</field>
+ <field name="active" eval="True" />
+ <field name="user_id" ref="base.user_root" />
+ <field name="interval_number">1</field>
+ <field name="interval_type">days</field>
+ <field name="numbercall">-1</field>
+ <field name="doall">1</field>
+ <field name="model_id" ref="model_purchase_order"/>
+ <field name="state">code</field>
+ <field name="code">model._send_reminder_mail()</field>
+ </record>
+</odoo>
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,46 @@
with reference: ${object.partner_ref}
% endif
amounting in <strong>${format_amount(object.amount_total, object.currency_id)}</strong>
- from ${object.company_id.name}.
- <br/><br/>
- If you have any questions, please do not hesitate to contact us.
+ from ${object.company_id.name}.
+ % if object.date_planned or object.expected_date:
+ The receipt is planned for
+ % if object.date_planned:
+ <strong>${object.date_planned.date()}</strong>.
+ % elif object.expected_date:
+ <strong>${object.expected_date.date()}</strong>.
+ % endif
+ <br/><br/>
+ Could you please acknowledge the receipt of this order?
+ % endif
+ </p>
+</div></field>
+ <field name="report_template" ref="action_report_purchase_order"/>
+ <field name="report_name">PO_${(object.name or '').replace('/','_')}</field>
+ <field name="lang">${object.partner_id.lang}</field>
+ <field name="auto_delete" eval="True"/>
+ </record>
+
+ <record id="email_template_edi_purchase_reminder" model="mail.template">
+ <field name="name">Purchase Order: Vendor Reminder</field>
+ <field name="model_id" ref="purchase.model_purchase_order"/>
+ <field name="email_from">${(object.user_id.email_formatted or user.email_formatted) |safe}</field>
+ <field name="subject">${object.company_id.name} Order (Ref ${object.name or 'n/a' })</field>
+ <field name="partner_to">${object.partner_id.id}</field>
+ <field name="body_html" type="html">
+<div style="margin: 0px; padding: 0px;">
+ <p style="margin: 0px; padding: 0px; font-size: 13px;">
+ Dear ${object.partner_id.name}
+ % if object.partner_id.parent_id:
+ (${object.partner_id.parent_id.name})
+ % endif
<br/><br/>
- Best regards,
+ Here is a reminder that the delivery of the purchase order <strong>${object.name}</strong> is planned for
+ % if object.date_planned:
+ <strong>${object.date_planned.date()}</strong>.
+ % else:
+ <strong>${object.expected_date.date()}</strong>.
+ % endif
+ Could you please confirm it will be on time?
</p>
</div></field>
<field name="report_template" ref="action_report_purchase_order"/>
@@ -61,5 +96,41 @@
<field name="lang">${object.partner_id.lang}</field>
<field name="auto_delete" eval="True"/>
</record>
+
+ <template id="mail_notification_confirm" inherit_id="mail.mail_notification_paynow" name="Purchase: Confirmation mail notification template">
+ <xpath expr="//t[@t-set='access_name']" position="after">
+ <t t-if="record._name == 'purchase.order'">
+ <t t-if="record.state == 'purchase' and not record.env.context.get('is_reminder')">
+ <t t-set="access_name">Confirm</t>
+ <t t-set="access_url" t-value="record.get_confirm_url(confirm_type='reception')"/>
+ </t>
+ </t>
+ </xpath>
+ <xpath expr="//a[@t-att-href='access_url']" position="replace">
+ <t t-if="record._name == 'purchase.order' and record.env.context.get('is_reminder')">
+ <a t-att-href="record.get_confirm_url(confirm_type='reminder')"
+ style="margin-right: 10px; background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
+ Yes
+ </a>
+ <a t-att-href="record.get_update_url()"
+ style="margin-left: 10px; background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
+ No, Update Dates
+ </a>
+ <div>&amp;nbsp;</div>
+ <div style="margin: 0px; padding: 0px; font-size:13px; text-align: left;">
+ If you have any questions, please do not hesitate to contact us.
+ <div>&amp;nbsp;</div>
+ Best regards,
+ </div>
+ </t>
+ <t t-else="">
+ <a t-att-href="access_url"
+ style="margin-left: 10px; background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
+ <t t-esc="access_name"/>
+ </a>
+ </t>
+ </xpath>
+ </template>
+
</data>
</odoo>
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 @@
<field name="state">code</field>
<field name="code">action = records.action_share()</field>
</record>
+
+ <!-- Default value for company_dependant field -->
+ <record forcecreate="True" id="receipt_reminder_email" model="ir.property">
+ <field name="name">receipt_reminder_email</field>
+ <field name="type" eval="'boolean'"/>
+ <field name="fields_id" search="[('model','=','res.partner'),('name','=','receipt_reminder_email')]"/>
+ <field eval="True" name="value"/>
+ </record>
+ <record forcecreate="True" id="reminder_date_before_receipt" model="ir.property">
+ <field name="name">reminder_date_before_receipt</field>
+ <field name="type" eval="'integer'"/>
+ <field name="fields_id" search="[('model','=','res.partner'),('name','=','reminder_date_before_receipt')]"/>
+ <field eval="1" name="value"/>
+ </record>
</data>
</odoo>
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 = _('<p> %s modified receipt dates for the following products:</p>') % self.partner_id.name
+ for line, date in updated_dates:
+ note += _('<p> &nbsp; - %s from %s to %s </p>') % (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 @@
<field name="category_id" ref="base.module_category_hidden"/>
</record>
+ <record id="group_send_reminder" model="res.groups">
+ <field name="name">Send an automatic reminder email to confirm delivery</field>
+ <field name="category_id" ref="base.module_category_hidden"/>
+ </record>
+
</data>
<data noupdate="1">
+ <record model="res.groups" id="base.group_user">
+ <field name="implied_ids" eval="[(4, ref('purchase.group_send_reminder'))]"/>
+ </record>
+
<record id="base.default_user" model="res.users">
<field name="groups_id" eval="[(4,ref('purchase.group_purchase_manager'))]"/>
</record>
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 @@
<script type="text/javascript" src="/purchase/static/src/js/tours/purchase.js"></script>
</xpath>
</template>
+
+ <template id="assets_frontend" name="purchase assets" inherit_id="web.assets_frontend">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/purchase/static/src/js/purchase_datetimepicker.js"></script>
+ </xpath>
+ </template>
</odoo>
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 @@
</t>
</template>
+ <template id="portal_my_purchase_order_update_date" name="Portal: My Purchase Order Update Dates" inherit_id="purchase.portal_my_purchase_order" primary="True">
+ <xpath expr="//div[hasclass('card-body')]" position="replace">
+ <div class="card-body">
+ <div class="mb-4">
+ <strong>Confirmation Date:</strong> <span t-field="order.date_approve" t-options='{"widget": "date"}'/><br/>
+ <div t-att-class="'d-inline' if order.date_planned else 'd-none'">
+ <strong>Receipt Date:</strong><span class="ml-1" t-field="order.date_planned" t-options='{"widget": "date"}'/>
+ </div>
+ </div>
+ <h3 class="font-weight-normal">Pricing</h3>
+
+ <form t-attf-action="/my/purchase/#{order.id}/update?access_token=#{order.access_token}" method="post">
+ <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
+
+ <table class="table table-sm">
+ <thead class="bg-100">
+ <tr>
+ <th>Products</th>
+ <th class="text-right d-none d-sm-table-cell">Unit Price</th>
+ <th class="text-right">Quantity</th>
+ <th class="text-right">Scheduled Date</th>
+ <th class="text-right">Updated Date</th>
+ <th class="text-right">Subtotal</th>
+ </tr>
+ </thead>
+ <tbody>
+ <t t-foreach="order.order_line" t-as="ol">
+ <tr>
+ <td>
+ <img t-att-src="image_data_uri(resize_to_48(ol.product_id.image_1024))" alt="Product" class="d-none d-lg-inline"/>
+ <span t-esc="ol.name"/>
+ </td>
+ <td class="text-right d-none d-sm-table-cell">
+ <span t-field="ol.price_unit" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
+ </td>
+ <td class="text-right">
+ <span t-esc="ol.product_qty"/>
+ </td>
+ <td class="text-right">
+ <span t-esc="ol.date_planned"/>
+ </td>
+ <td class="text-right">
+ <div class="container">
+ <div class="form-group">
+ <div class="input-group date">
+ <input type="text" class="form-control datetimepicker-input o-purchase-datetimepicker" t-attf-id="datetimepicker_#{ol.id}" t-att-name="ol.id"
+ data-toggle="datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" t-attf-data-target="#datetimepicker_#{ol.id}"/>
+ </div>
+ </div>
+ </div>
+ </td>
+ <td class="text-right">
+ <span t-field="ol.price_subtotal" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
+ </td>
+ </tr>
+ </t>
+ </tbody>
+ </table>
+ <div class="row">
+ <div class="col-sm-7 col-md-5 ml-auto">
+ <table class="table table-sm">
+ <tbody>
+ <tr>
+ <td>
+ <strong>Untaxed Amount:</strong>
+ </td>
+ <td class="text-right">
+ <span t-field="order.amount_untaxed" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <strong>Taxes:</strong>
+ </td>
+ <td class="text-right">
+ <span t-field="order.amount_tax" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <strong>Total:</strong>
+ </td>
+ <td class="text-right">
+ <span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <input class="btn btn-primary" type="submit" value="Send Updated Dates"/>
+ </form>
+ </div>
+ </xpath>
+ </template>
+
</odoo>
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 @@
<button name="print_quotation" string="Print RFQ" type="object" states="sent" groups="base.group_user"/>
<button name="button_confirm" type="object" states="draft" string="Confirm Order" id="draft_confirm"/>
<button name="action_rfq_send" states="purchase" string="Send PO by Email" type="object" context="{'send_rfq':False}"/>
+ <button name="confirm_reminder_mail" string="Confirm Receipt Date" type="object" attrs="{'invisible': ['|', ('state', 'not in', ('purchase', 'done')), ('mail_reminder_confirmed', '=', True)]}"/>
<button name="action_create_invoice" string="Create Bill" type="object" context="{'create_bill':True}" attrs="{'invisible': ['|', '|', ('state', 'not in', ('purchase', 'done')), ('invoice_status', 'not in', ('no', 'invoiced')), ('order_line', '=', [])]}"/>
<button name="button_draft" states="cancel" string="Set to Draft" type="object" />
<button name="button_cancel" states="draft,to approve,sent,purchase" string="Cancel" type="object" />
@@ -176,9 +177,28 @@
</group>
<group>
<field name="date_order" attrs="{'invisible': [('state','=','purchase')]}"/>
- <field name="date_approve" attrs="{'invisible': [('state','!=','purchase')]}"/>
+ <label for="date_approve" attrs="{'invisible': [('state','!=','purchase')]}"/>
+ <div name="date_approve" attrs="{'invisible': [('state','!=','purchase')]}" class="o_row">
+ <field name="date_approve"/>
+ <field name="mail_reception_confirmed" invisible="1"/>
+ <span class="text-muted" attrs="{'invisible': [('mail_reception_confirmed','=', False)]}">(confirmed by vendor)</span>
+ </div>
+ <label for="date_planned"/>
+ <div name="date_planned_and_confirmation">
+ <div name="date_planned_div" class="o_row">
+ <field name="date_planned" attrs="{'readonly': [('state', 'not in', ('draft', 'sent', 'to approve', 'purchase'))]}"/>
+ <span name='expected_date_span' class="text-muted" attrs="{'invisible': ['|', ('expected_date', '=', False), ('date_planned', '!=', False)]}">Per line, first on <field name="expected_date" widget="date"/></span>
+ <field name="mail_reminder_confirmed" invisible="1"/>
+ <span class="text-muted" attrs="{'invisible': [('mail_reminder_confirmed', '=', False)]}">(confirmed by vendor)</span>
+ </div>
+ <div name="confirmation" class="oe_edit_only o_row" groups='purchase.group_send_reminder'>
+ <field name="receipt_reminder_email"/>
+ <span>Ask confirmation</span>
+ <field name="reminder_date_before_receipt" attrs="{'readonly': [('receipt_reminder_email', '=', False)]}"/>
+ <span> days before</span>
+ </div>
+ </div>
<field name="origin" attrs="{'invisible': [('origin','=',False)]}"/>
- <field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</group>
</group>
<notebook>
@@ -209,7 +229,7 @@
context="{'partner_id':parent.partner_id, 'quantity':product_qty,'uom':product_uom, 'company_id': parent.company_id}"
force_save="1" domain="[('purchase_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/>
<field name="name" widget="section_and_note_text"/>
- <field name="date_planned" optional="hide" attrs="{'required': [('display_type', '=', False)]}"/>
+ <field name="date_planned" optional="hide" attrs="{'required': [('display_type', '=', False)], 'readonly': [('parent.date_planned', '!=', False)]}" force_save="1"/>
<field name="account_analytic_id" optional="hide" context="{'default_partner_id':parent.partner_id}" groups="analytic.group_analytic_accounting" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/>
<field name="analytic_tag_ids" optional="hide" groups="analytic.group_analytic_tags" widget="many2many_tags" options="{'color_field': 'color'}" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/>
<field name="product_qty"/>
@@ -253,7 +273,6 @@
<field name="date_planned" widget="date" attrs="{'required': [('display_type', '=', False)]}"/>
<field name="account_analytic_id" colspan="2" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" widget="many2many_tags" options="{'color_field': 'color'}"/>
- <field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</group>
<group colspan="12">
<notebook>
@@ -340,15 +359,11 @@
</page>
<page string="Other Information" name="purchase_delivery_invoice">
<group>
- <group name="planning">
- <label for="date_planned" attrs="{'invisible': [('state', '!=', 'draft'), ('date_planned', '=', False)]}"/>
- <div name="date_planned_div" class="o_row" attrs="{'invisible': [('state', '!=', 'draft'), ('date_planned', '=', False)]}">
- <field name="date_planned" attrs="{'readonly': [('state', 'not in', ('draft', 'sent', 'to approve'))]}"/>
- <span name="expected_date_span" class="text-muted">Expected: <field name="expected_date" widget="date"/></span>
- </div>
- </group>
<group name="other_info">
<field name="user_id" domain="[('share', '=', False)]" widget="many2one_avatar_user"/>
+ <field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
+ </group>
+ <group name="invoice_info">
<field name="invoice_status" attrs="{'invisible': [('state', 'in', ('draft', 'sent', 'to approve', 'cancel'))]}"/>
<field name="payment_term_id" attrs="{'readonly': ['|', ('invoice_status','=', 'invoiced'), ('state', '=', 'done')]}" options="{'no_create': True}"/>
<field name="fiscal_position_id" options="{'no_create': True}" attrs="{'readonly': ['|', ('invoice_status','=', 'invoiced'), ('state', '=', 'done')]}"/>
@@ -418,6 +433,7 @@
<field name="product_id"/>
<filter name="my_Orders" string="My Orders" domain="[('user_id', '=', uid)]"/>
<separator/>
+ <filter name="unconfirmed" string="Not Acknowledged" domain="[('mail_reception_confirmed', '=', False), ('state', '=', 'purchase')]"/>
<filter name="not_invoiced" string="Waiting Bills" domain="[('invoice_status', '=', 'to invoice')]" help="Purchase orders that include lines not invoiced."/>
<filter name="invoiced" string="Bills Received" domain="[('invoice_status', '=', 'invoiced')]" help="Purchase orders that have been invoiced."/>
<separator/>
diff --git a/addons/purchase/views/res_config_settings_views.xml b/addons/purchase/views/res_config_settings_views.xml
index 52c0a1dabf4..c143f2088d7 100644
--- a/addons/purchase/views/res_config_settings_views.xml
+++ b/addons/purchase/views/res_config_settings_views.xml
@@ -74,6 +74,17 @@
</div>
</div>
</div>
+ <div class="col-12 col-lg-6 o_setting_box" id="auto_receipt_reminder">
+ <div class="o_setting_left_pane">
+ <field name="group_send_reminder"/>
+ </div>
+ <div class="o_setting_right_pane">
+ <label for="group_send_reminder"/>
+ <div class="text-muted">
+ Automatically remind the receipt date to your vendors
+ </div>
+ </div>
+ </div>
</div>
<h2>Invoicing</h2>
<div class="row mt16 o_settings_container" name="invoicing_settings_container">
diff --git a/addons/purchase/views/res_partner_views.xml b/addons/purchase/views/res_partner_views.xml
index 05b46795a76..58d675d5a16 100644
--- a/addons/purchase/views/res_partner_views.xml
+++ b/addons/purchase/views/res_partner_views.xml
@@ -6,13 +6,15 @@
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="priority">36</field>
- <field name="groups_id" eval="[(4, ref('base.group_multi_currency'))]"/>
<field name="arch" type="xml">
<group name="purchase" position="inside">
- <field name="property_purchase_currency_id" options="{'no_create': True, 'no_open': True}"/>
+ <field name="receipt_reminder_email" groups='purchase.group_send_reminder'/>
+ <field name="reminder_date_before_receipt" attrs="{'invisible': [('receipt_reminder_email', '=', False)]}" groups='purchase.group_send_reminder'/>
+ <field name="property_purchase_currency_id" options="{'no_create': True, 'no_open': True}" groups='base.group_multi_currency'/>
</group>
</field>
</record>
+
<record id="act_res_partner_2_purchase_order" model="ir.actions.act_window">
<field name="name">RFQs and Purchases</field>
<field name="res_model">purchase.order</field>
diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py
index 5ff7c7f67fd..cb8b060eaea 100644
--- a/addons/purchase_stock/models/purchase.py
+++ b/addons/purchase_stock/models/purchase.py
@@ -3,6 +3,7 @@
from odoo import api, fields, models, SUPERUSER_ID, _
from odoo.tools.float_utils import float_compare
+from datetime import datetime
from dateutil import relativedelta
from odoo.exceptions import UserError
@@ -252,6 +253,31 @@ class PurchaseOrder(models.Model):
subtype_id=self.env.ref('mail.mt_note').id)
return True
+ def _update_date_planned_for_lines(self, updated_dates):
+ note = self._compose_note(updated_dates)
+
+ validated_picking = self.picking_ids.filtered(lambda p: p.state == 'done')
+ if validated_picking:
+ note += _("<p>Those dates couldn’t be modified accordingly on the receipt %s which had already been validated.</p>") % validated_picking.name
+ else:
+ note += _("<p>Those dates have been updated accordingly on the receipt %s.</p>") % 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)
+
+ self.activity_schedule(
+ 'mail.mail_activity_data_warning',
+ summary="Date Updated",
+ note=note,
+ user_id=self.user_id.id or SUPERUSER_ID
+ )
+
+ @api.model
+ def _get_orders_to_remind(self):
+ """When auto sending reminder mails, don't send for purchase order with
+ validated receipts."""
+ return super()._get_orders_to_remind().filtered(lambda p: not p.effective_date)
+
class PurchaseOrderLine(models.Model):
_inherit = 'purchase.order.line'
@@ -512,6 +538,12 @@ class PurchaseOrderLine(models.Model):
return outgoing_moves, incoming_moves
+ def _update_date_planned(self, updated_date):
+ move_to_update = self.move_ids.filtered(lambda m: m.state not in ['done', 'cancel'])
+ if move_to_update:
+ super()._update_date_planned(updated_date)
+ self._update_move_expected_date(updated_date)
+
@api.model
def _update_qty_received_method(self):
"""Update qty_received_method for old PO before install this module."""
diff --git a/addons/purchase_stock/report/vendor_delay_report.xml b/addons/purchase_stock/report/vendor_delay_report.xml
index 7391d5eade7..b5d4cbf0362 100644
--- a/addons/purchase_stock/report/vendor_delay_report.xml
+++ b/addons/purchase_stock/report/vendor_delay_report.xml
@@ -32,6 +32,4 @@
<field name="target">current</field>
<field name="context">{'search_default_later_than_a_year_ago':1}</field>
</record>
-
- <menuitem id="delay_report" name="Vendor Delay" parent="purchase.menu_purchase_root" sequence="99" groups="purchase.group_purchase_manager" action="action_purchase_vendor_delay_report"/>
</odoo>
diff --git a/addons/purchase_stock/views/purchase_views.xml b/addons/purchase_stock/views/purchase_views.xml
index 981802114c5..9457ee3ea45 100644
--- a/addons/purchase_stock/views/purchase_views.xml
+++ b/addons/purchase_stock/views/purchase_views.xml
@@ -9,6 +9,9 @@
<xpath expr="//header/button[@name='action_rfq_send']" position="after">
<button name="action_view_picking" string="Receive Products" class="oe_highlight" type="object" attrs="{'invisible': ['|', '|' , ('is_shipped', '=', True), ('state','not in', ('purchase','done')), ('picking_count', '=', 0)]}"/>
</xpath>
+ <xpath expr="//header/button[@name='confirm_reminder_mail']" position="replace">
+ <button name="confirm_reminder_mail" string="Confirm Receipt Date" type="object" attrs="{'invisible': ['|', '|', ('state', 'not in', ('purchase', 'done')), ('mail_reminder_confirmed', '=', True), ('effective_date', '!=', False)]}"/>
+ </xpath>
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<button type="object"
name="action_view_picking"
@@ -20,15 +23,15 @@
</xpath>
<xpath expr="//field[@name='partner_id']" position="replace">
<label for="partner_id"/>
- <div class="o_row">
- <field name="partner_id" widget="res_partner_many2one" context="{'res_partner_search_mode': 'supplier', 'show_vat': True}"
- placeholder="Name, TIN, Email, or Reference"
- />
- <div class="oe_edit_only">
- <button name="%(action_purchase_vendor_delay_report)d" type="action" class="oe_stat_button" context="{'search_default_partner_id': partner_id}">
- <field name="on_time_rate" class="text-muted" widget='percentpie'/>
- </button>
- </div>
+ <div>
+ <field name="partner_id" widget="res_partner_many2one" context="{'res_partner_search_mode': 'supplier', 'show_vat': True}"
+ placeholder="Name, TIN, Email, or Reference"
+ />
+ <div class="oe_edit_only">
+ <button name="%(action_purchase_vendor_delay_report)d" type="action" class="oe_stat_button" context="{'search_default_partner_id': partner_id}">
+ <field name="on_time_rate" class="text-muted" widget='percentpie'/>
+ </button>
+ </div>
</div>
</xpath>
<xpath expr="//field[@name='currency_id']" position="after">
@@ -37,12 +40,15 @@
<xpath expr="//field[@name='order_line']/tree//field[@name='date_planned']" position="after">
<field name="move_dest_ids" invisible="1"/>
</xpath>
- <xpath expr="//span[@name='expected_date_span']" position="attributes">
- <attribute name="attrs">
- {'invisible': [('effective_date', '!=', False), ('date_planned', '!=', False)]}
- </attribute>
+ <xpath expr="//div[@name='confirmation']" position="replace">
+ <div name="confirmation" class="oe_edit_only o_row" attrs="{'invisible': [('effective_date', '!=', False)]}" groups='purchase.group_send_reminder'>
+ <field name="receipt_reminder_email"/>
+ <span>Ask confirmation</span>
+ <field name="reminder_date_before_receipt" attrs="{'readonly': [('receipt_reminder_email', '=', False)]}"/>
+ <span> days before</span>
+ </div>
</xpath>
- <xpath expr="//div[@name='date_planned_div']" position="after">
+ <xpath expr="//div[@name='date_planned_and_confirmation']" position="after">
<field name="effective_date" attrs="{'invisible': [('effective_date', '=', False)]}"/>
</xpath>
<xpath expr="//field[@name='order_line']/form//field[@name='invoice_lines']" position="after">