[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:
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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>&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>&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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> - %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:
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user