[IMP] purchase: Improve reminder mail usability

Some change to imporve reminder mail usability:

1. Scheduled date on POL of the PO form now is always editable.
2. Improve tooltip for the reminder email
3. don't show number of days when reminder email not checked on
   both PO and partner form.
4. only show "confirm receipt date" button in debug model
5. In the mail, format date by partner.lang
6. Add button to send preview reminder mail
7. On default, receipt_reminder_email on res.partner is False
8. merge expected_date with date_planned (done by FP)

Task 2265912
PR #52693
This commit is contained in:
Fabien Pinckaers
2020-06-09 13:59:10 +00:00
committed by yhu-odoo
parent d3d0fd7c59
commit ccfc54113d
11 changed files with 117 additions and 68 deletions
+10 -12
View File
@@ -50,13 +50,9 @@
% endif
amounting in <strong>${format_amount(object.amount_total, object.currency_id)}</strong>
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/>
% if object.date_planned:
The receipt is expected for <strong>${format_date(object.date_planned)}</strong>.
<br/><br/>
Could you please acknowledge the receipt of this order?
% endif
@@ -82,13 +78,15 @@
(${object.partner_id.parent_id.name})
% endif
<br/><br/>
Here is a reminder that the delivery of the purchase order <strong>${object.name}</strong> is planned for
Here is a reminder that the delivery of the purchase order <strong>${object.name}</strong>
% if object.partner_ref:
<strong>(${object.partner_ref})</strong>
% endif
is expected for
% if object.date_planned:
<strong>${object.date_planned.date()}</strong>.
% else:
<strong>${object.expected_date.date()}</strong>.
<strong>${format_date(object.date_planned)}</strong>.
% endif
Could you please confirm it will be on time?
Could you please confirm it will be delivered on time?
</p>
</div></field>
<field name="report_template" ref="action_report_purchase_order"/>
+1 -1
View File
@@ -42,7 +42,7 @@
<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"/>
<field eval="False" name="value"/>
</record>
<record forcecreate="True" id="reminder_date_before_receipt" model="ir.property">
<field name="name">reminder_date_before_receipt</field>
+12
View File
@@ -6,6 +6,18 @@
<field eval="[(4, ref('group_purchase_user'))]" name="groups_id"/>
</record>
<record id="base.res_partner_1" model="res.partner">
<field name="receipt_reminder_email">True</field>
</record>
<record id="base.res_partner_2" model="res.partner">
<field name="receipt_reminder_email">True</field>
</record>
<record id="base.res_partner_12" model="res.partner">
<field name="receipt_reminder_email">True</field>
</record>
</data>
<record id="purchase_order_1" model="purchase.order">
+44 -19
View File
@@ -106,10 +106,8 @@ class PurchaseOrder(models.Model):
('invoiced', 'Fully Billed'),
], string='Billing Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no')
date_planned = fields.Datetime(
string='Receipt Date', index=True, copy=False,
string='Receipt Date', index=True, copy=False, compute='_compute_date_planned', store=True, readonly=False,
help="Delivery date promised by vendor. This date is used to determine expected arrival of products.")
expected_date = fields.Datetime("Expected Date", compute='_compute_expected_date', store=True,
help="Delivery date expected by vendor, computed from the minimum lead time of the order lines.")
date_calendar_start = fields.Datetime(compute='_compute_date_calendar_start', readonly=True, store=True)
amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', tracking=True)
@@ -130,10 +128,8 @@ class PurchaseOrder(models.Model):
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")
receipt_reminder_email = fields.Boolean('Receipt Reminder Email', related='partner_id.receipt_reminder_email', readonly=False)
reminder_date_before_receipt = fields.Integer('Days Before Receipt', related='partner_id.reminder_date_before_receipt', readonly=False)
@api.constrains('company_id', 'order_line')
def _check_order_line_company_id(self):
@@ -168,11 +164,14 @@ class PurchaseOrder(models.Model):
order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.currency_id, order.company_id, order.date_order)
@api.depends('order_line.date_planned')
def _compute_expected_date(self):
""" expected_date = the earliest date_planned across all order lines. """
def _compute_date_planned(self):
""" date_planned = the earliest date_planned across all order lines. """
for order in self:
dates_list = order.order_line.filtered(lambda x: not x.display_type).mapped('date_planned')
order.expected_date = fields.Datetime.to_string(min(dates_list)) if dates_list else False
dates_list = order.order_line.filtered(lambda x: not x.display_type and x.date_planned).mapped('date_planned')
if dates_list:
order.date_planned = fields.Datetime.to_string(min(dates_list))
else:
order.date_planned = False
@api.depends('name', 'partner_ref')
def name_get(self):
@@ -219,6 +218,17 @@ class PurchaseOrder(models.Model):
line.date_planned = line._get_date_planned(seller)
return new_po
def onchange(self, values, field_name, field_onchange):
"""Override onchange to NOT to update all date_planned on PO lines when
date_planned on PO is updated by the change of date_planned on PO lines.
"""
result = super(PurchaseOrder, self).onchange(values, field_name, field_onchange)
if field_name == 'order_line' and 'value' in result:
for line in result['value'].get('order_line', []):
if line[0] < 2 and 'date_planned' in line[2]:
del line[2]['date_planned']
return result
def _track_subtype(self, init_values):
self.ensure_one()
if 'state' in init_values and self.state == 'purchase':
@@ -628,23 +638,38 @@ 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:
def _send_reminder_mail(self, send_single=False):
if not self.user_has_groups('purchase.group_send_reminder'):
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():
orders = self if send_single else self._get_orders_to_remind()
for order in orders:
date = order.date_planned
if date and (send_single or (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')
def send_reminder_preview(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 and self.env.user.email:
for order in self:
template.with_context(is_reminder=True).send_mail(
order.id,
force_send=True,
raise_exception=False,
email_values={'email_to': self.env.user.email, 'recipient_ids': []},
notif_layout="mail.mail_notification_paynow")
@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([
('receipt_reminder_email', '=', True),
('state', 'in', ['purchase', 'done']),
('mail_reminder_confirmed', '=', False)
]).filtered(lambda p: p.mapped('order_line.product_id.product_tmpl_id.type') != ['service'])
@@ -655,7 +680,7 @@ class PurchaseOrder(models.Model):
if confirm_type in ['reminder', 'reception']:
param = url_encode({
'confirm': confirm_type,
'confirmed_date': (self.date_planned or self.expected_date).date(),
'confirmed_date': self.date_planned.date(),
})
return self.get_portal_url(query_string='&%s' % param)
return self.get_portal_url()
@@ -670,7 +695,7 @@ class PurchaseOrder(models.Model):
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()
date = confirmed_date or self.date_planned.date()
order.message_post(body="%s confirmed the receipt will take place on %s." % (order.partner_id.name, date))
def _confirm_reception_mail(self):
+2 -2
View File
@@ -60,7 +60,7 @@ class res_partner(models.Model):
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')
receipt_reminder_email = fields.Boolean('Receipt Reminder', default=False, company_dependent=True,
help="Automatically send a confirmation email to the vendor X days before the expected receipt date, asking him to confirm the exact date.")
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")
+6 -6
View File
@@ -22,9 +22,9 @@ class TestPurchase(SavepointCase):
cls.vendor = cls.env['res.partner'].create({'name': 'vendor1'})
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
def test_expected_date(self):
"""Set a date planned on 2 PO lines. Check that the PO expected date is the earliest PO line date
planned. Change one of the dates so it is even earlier and check that the expected date is set to
def test_date_planned(self):
"""Set a date planned on 2 PO lines. Check that the PO date_planned is the earliest PO line date
planned. Change one of the dates so it is even earlier and check that the date_planned is set to
this earlier date.
"""
po = Form(self.env['purchase.order'])
@@ -42,19 +42,19 @@ class TestPurchase(SavepointCase):
# Check that the same date is planned on both PO lines.
self.assertNotEqual(po.order_line[0].date_planned, False)
self.assertAlmostEqual(po.order_line[0].date_planned, po.order_line[1].date_planned, delta=timedelta(seconds=10))
self.assertAlmostEqual(po.order_line[0].date_planned, po.expected_date, delta=timedelta(seconds=10))
self.assertAlmostEqual(po.order_line[0].date_planned, po.date_planned, delta=timedelta(seconds=10))
orig_date_planned = po.order_line[0].date_planned
# Set an earlier date planned on a PO line and check that the PO expected date matches it.
new_date_planned = orig_date_planned - timedelta(hours=1)
po.order_line[0].date_planned = new_date_planned
self.assertAlmostEqual(po.order_line[0].date_planned, po.expected_date, delta=timedelta(seconds=10))
self.assertAlmostEqual(po.order_line[0].date_planned, po.date_planned, delta=timedelta(seconds=10))
# Set an even earlier date planned on the other PO line and check that the PO expected date matches it.
new_date_planned = orig_date_planned - timedelta(hours=72)
po.order_line[1].date_planned = new_date_planned
self.assertAlmostEqual(po.order_line[1].date_planned, po.expected_date, delta=timedelta(seconds=10))
self.assertAlmostEqual(po.order_line[1].date_planned, po.date_planned, delta=timedelta(seconds=10))
def test_purchase_order_sequence(self):
PurchaseOrder = self.env['purchase.order'].with_context(tracking_disable=True)
+27 -14
View File
@@ -143,7 +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="confirm_reminder_mail" string="Confirm Receipt Date" type="object" attrs="{'invisible': ['|', ('state', 'not in', ('purchase', 'done')), ('mail_reminder_confirmed', '=', True)]}" groups="base.group_no_one"/>
<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" />
@@ -184,18 +184,18 @@
<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 name="date_planned_div" class="o_row">
<field name="date_planned" attrs="{'readonly': [('state', 'not in', ('draft', 'sent', 'to approve', 'purchase'))]}"/>
<field name="mail_reminder_confirmed" invisible="1"/>
<span class="text-muted" attrs="{'invisible': [('mail_reminder_confirmed', '=', False)]}">(confirmed by vendor)</span>
</div>
<label for="receipt_reminder_email" invisible='1'/>
<div name="reminder" class="o_row" groups='purchase.group_send_reminder' title="Automatically send a confirmation email to the vendor X days before the expected receipt date, asking him to confirm the exact date.">
<field name="receipt_reminder_email"/>
<span>Ask confirmation</span>
<div class="o_row oe_inline" attrs="{'invisible': [('receipt_reminder_email', '=', False)]}">
<field name="reminder_date_before_receipt" class="oe_inline"/>
days before <button name="send_reminder_preview" icon="fa-info-circle" type="object" class="oe_inline oe_link" title="Preveiw the reminder email by sending it to yourself."/>
</div>
</div>
<field name="origin" attrs="{'invisible': [('origin','=',False)]}"/>
@@ -229,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)], 'readonly': [('parent.date_planned', '!=', False)]}" force_save="1"/>
<field name="date_planned" optional="hide" attrs="{'required': [('display_type', '=', 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"/>
@@ -730,6 +730,19 @@
action = records.action_create_invoice()
</field>
</record>
<record id="action_purchase_send_reminder" model="ir.actions.server">
<field name="name">Send Reminder</field>
<field name="model_id" ref="purchase.model_purchase_order"/>
<field name="binding_model_id" ref="purchase.model_purchase_order"/>
<field name='groups_id' eval="[(4, ref('purchase.group_send_reminder'))]"/>
<field name="binding_view_types">form</field>
<field name="state">code</field>
<field name="code">
if records:
action = records._send_reminder_mail(send_single=True)
</field>
</record>
<!-- Dashboard action buttons -->
<!-- Dashboard action buttons: End in List view -->
+8 -2
View File
@@ -8,8 +8,14 @@
<field name="priority">36</field>
<field name="arch" type="xml">
<group name="purchase" position="inside">
<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'/>
<label for="receipt_reminder_email" groups='purchase.group_send_reminder'/>
<div name="receipt_reminder" class="o_row" groups='purchase.group_send_reminder'>
<field name="receipt_reminder_email"/>
<div attrs="{'invisible': [('receipt_reminder_email', '=', False)]}">
<field name="reminder_date_before_receipt" class="oe_inline"/>
<span> days before</span>
</div>
</div>
<field name="property_purchase_currency_id" options="{'no_create': True, 'no_open': True}" groups='base.group_multi_currency'/>
</group>
</field>
+1 -1
View File
@@ -340,7 +340,7 @@ class PurchaseOrderLine(models.Model):
def write(self, values):
for line in self.filtered(lambda l: not l.display_type):
# PO date_planned overrides any PO line date_planned values
if not line.order_id.date_planned and values.get('date_planned') and line.propagate_date:
if values.get('date_planned') and line.propagate_date:
new_date = fields.Datetime.to_datetime(values['date_planned'])
self._update_move_expected_date(new_date)
result = super(PurchaseOrderLine, self).write(values)
@@ -51,7 +51,7 @@ class PurchaseReport(models.Model):
if avg_receipt_delay:
query = """ SELECT AVG(receipt_delay.po_receipt_delay)::decimal(16,2) AS avg_receipt_delay
FROM (
SELECT extract(epoch from age(po.effective_date,COALESCE(po.date_planned, po.expected_date)))/(24*60*60) AS po_receipt_delay
SELECT extract(epoch from age(po.effective_date, po.date_planned))/(24*60*60) AS po_receipt_delay
FROM purchase_order po
WHERE po.id IN (
SELECT "purchase_report"."order_id" FROM %s WHERE %s)
+5 -10
View File
@@ -9,8 +9,8 @@
<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 expr="//header/button[@name='confirm_reminder_mail']" position="attributes">
<attribute name="attrs">{'invisible': ['|', '|', ('state', 'not in', ('purchase', 'done')), ('mail_reminder_confirmed', '=', True), ('effective_date', '!=', False)]}</attribute>
</xpath>
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<button type="object"
@@ -32,15 +32,10 @@
<span><field name="on_time_rate" widget="integer" class="oe_inline"/>% On-Time Delivery</span>
</button>
</xpath>
<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 expr="//div[@name='reminder']" position="attributes">
<attribute name="attrs">{'invisible': [('effective_date', '!=', False)]}</attribute>
</xpath>
<xpath expr="//div[@name='date_planned_and_confirmation']" position="after">
<xpath expr="//div[@name='reminder']" 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">