[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>
This commit is contained in:
yhu-odoo
2020-05-27 08:20:17 +00:00
parent d83334ef8e
commit 181c7d82e3
19 changed files with 454 additions and 41 deletions
+1
View File
@@ -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',
+36 -2
View File
@@ -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)
+15
View File
@@ -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>
+76 -5
View File
@@ -49,11 +49,17 @@
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.
<br/><br/>
Best regards,
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"/>
@@ -61,5 +67,70 @@
<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/>
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"/>
<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>
<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>
+14
View File
@@ -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>
+95 -4
View File
@@ -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
@@ -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:
+5
View File
@@ -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")
@@ -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>
@@ -0,0 +1,3 @@
$(function () {
$('input.o-purchase-datetimepicker').datetimepicker();
})
+2 -1
View File
@@ -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({
+6
View File
@@ -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>
@@ -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>
+27 -11
View File
@@ -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/>
@@ -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">
+4 -2
View File
@@ -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>
+32
View File
@@ -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."""
@@ -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>
+20 -14
View File
@@ -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">