diff options
| author | yhu-odoo <yhu@odoo.com> | 2020-04-21 15:22:46 +0000 |
|---|---|---|
| committer | yhu-odoo <yhu@odoo.com> | 2020-05-27 08:20:17 +0000 |
| commit | 181c7d82e30d0848bbac7f7d0188e81aced0af07 (patch) | |
| tree | d5868d318629c09ebb4da9d70ac2316d66fde338 | |
| parent | d83334ef8ee566e56ad8b25264d66704120ba598 (diff) | |
[IMP] purchase: send reminder mail to vendor
1. automatically send a reminder mail to vendor to confirm the receipt
date. If confirmed, (confirmed by vendor) will be added next to the
receipt date. If not, vendor can update the date on the portal website.
An warning activity will be set for the purchase representative for this
update.
2. Vendor can also comfirm recieption of the PO when we 'send PO by mail'.
If confirm, (confirmed by vendor) will be added next to the confirmation
date. An filter is added in the PO search view to show all unconfirmed
PO.
Task 2230811
PR #49921
Signed-off-by: Simon Lejeune (sle) <sle@openerp.com>
19 files changed, 453 insertions, 40 deletions
diff --git a/addons/purchase/__manifest__.py b/addons/purchase/__manifest__.py index 426f628dfcf..42e8a832961 100644 --- a/addons/purchase/__manifest__.py +++ b/addons/purchase/__manifest__.py @@ -16,6 +16,7 @@ 'views/assets.xml', 'views/account_move_views.xml', 'data/purchase_data.xml', + 'data/ir_cron_data.xml', 'report/purchase_reports.xml', 'views/purchase_views.xml', 'views/res_config_settings_views.xml', diff --git a/addons/purchase/controllers/portal.py b/addons/purchase/controllers/portal.py index 402a6700dd3..92136935681 100644 --- a/addons/purchase/controllers/portal.py +++ b/addons/purchase/controllers/portal.py @@ -13,7 +13,6 @@ from odoo.addons.portal.controllers.portal import pager as portal_pager, Custome from odoo.addons.web.controllers.main import Binary - class CustomerPortal(CustomerPortal): def _prepare_portal_layout_values(self): @@ -111,6 +110,41 @@ class CustomerPortal(CustomerPortal): report_type = kw.get('report_type') if report_type in ('html', 'pdf', 'text'): - return self._show_report(model=order_sudo, report_type=report_type, report_ref='purchase.action_report_purchase_order',download=kw.get('download')) + return self._show_report(model=order_sudo, report_type=report_type, report_ref='purchase.action_report_purchase_order', download=kw.get('download')) + + confirm_type = kw.get('confirm') + if confirm_type == 'reminder': + order_sudo.confirm_reminder_mail(kw.get('confirmed_date')) + if confirm_type == 'reception': + order_sudo._confirm_reception_mail() + + values = self._purchase_order_get_page_view_values(order_sudo, access_token, **kw) + update_date = kw.get('update') + if update_date == 'True': + return request.render("purchase.portal_my_purchase_order_update_date", values) + return request.render("purchase.portal_my_purchase_order", values) + + @http.route(['/my/purchase/<int:order_id>/update'], type='http', methods=['POST'], auth="public", website=True) + def portal_my_purchase_order_update_dates(self, order_id=None, access_token=None, **kw): + """User update scheduled date on purchase order line. + """ + try: + order_sudo = self._document_check_access('purchase.order', order_id, access_token=access_token) + except (AccessError, MissingError): + return request.redirect('/my') + + updated_dates = [] + try: + for id_str, date in kw.items(): + line = order_sudo.order_line.filtered(lambda l: l.id == int(id_str)) + if not line: + return request.redirect(order_sudo.get_portal_url()) + updated_dates.append((line, date)) + except ValueError: + return request.redirect(order_sudo.get_portal_url()) + + if updated_dates: + order_sudo._update_date_planned_for_lines(updated_dates) + values = self._purchase_order_get_page_view_values(order_sudo, access_token, **kw) return request.render("purchase.portal_my_purchase_order", values) diff --git a/addons/purchase/data/ir_cron_data.xml b/addons/purchase/data/ir_cron_data.xml new file mode 100644 index 00000000000..3c70792ceb6 --- /dev/null +++ b/addons/purchase/data/ir_cron_data.xml @@ -0,0 +1,15 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record forcecreate="True" id="purchase_send_reminder_mail" model="ir.cron"> + <field name="name">Purchase reminder</field> + <field name="active" eval="True" /> + <field name="user_id" ref="base.user_root" /> + <field name="interval_number">1</field> + <field name="interval_type">days</field> + <field name="numbercall">-1</field> + <field name="doall">1</field> + <field name="model_id" ref="model_purchase_order"/> + <field name="state">code</field> + <field name="code">model._send_reminder_mail()</field> + </record> +</odoo> diff --git a/addons/purchase/data/mail_template_data.xml b/addons/purchase/data/mail_template_data.xml index dff26e49227..35bd59cf12c 100644 --- a/addons/purchase/data/mail_template_data.xml +++ b/addons/purchase/data/mail_template_data.xml @@ -49,11 +49,46 @@ with reference: ${object.partner_ref} % endif amounting in <strong>${format_amount(object.amount_total, object.currency_id)}</strong> - from ${object.company_id.name}. - <br/><br/> - If you have any questions, please do not hesitate to contact us. + from ${object.company_id.name}. + % if object.date_planned or object.expected_date: + The receipt is planned for + % if object.date_planned: + <strong>${object.date_planned.date()}</strong>. + % elif object.expected_date: + <strong>${object.expected_date.date()}</strong>. + % endif + <br/><br/> + Could you please acknowledge the receipt of this order? + % endif + </p> +</div></field> + <field name="report_template" ref="action_report_purchase_order"/> + <field name="report_name">PO_${(object.name or '').replace('/','_')}</field> + <field name="lang">${object.partner_id.lang}</field> + <field name="auto_delete" eval="True"/> + </record> + + <record id="email_template_edi_purchase_reminder" model="mail.template"> + <field name="name">Purchase Order: Vendor Reminder</field> + <field name="model_id" ref="purchase.model_purchase_order"/> + <field name="email_from">${(object.user_id.email_formatted or user.email_formatted) |safe}</field> + <field name="subject">${object.company_id.name} Order (Ref ${object.name or 'n/a' })</field> + <field name="partner_to">${object.partner_id.id}</field> + <field name="body_html" type="html"> +<div style="margin: 0px; padding: 0px;"> + <p style="margin: 0px; padding: 0px; font-size: 13px;"> + Dear ${object.partner_id.name} + % if object.partner_id.parent_id: + (${object.partner_id.parent_id.name}) + % endif <br/><br/> - Best regards, + Here is a reminder that the delivery of the purchase order <strong>${object.name}</strong> is planned for + % if object.date_planned: + <strong>${object.date_planned.date()}</strong>. + % else: + <strong>${object.expected_date.date()}</strong>. + % endif + Could you please confirm it will be on time? </p> </div></field> <field name="report_template" ref="action_report_purchase_order"/> @@ -61,5 +96,41 @@ <field name="lang">${object.partner_id.lang}</field> <field name="auto_delete" eval="True"/> </record> + + <template id="mail_notification_confirm" inherit_id="mail.mail_notification_paynow" name="Purchase: Confirmation mail notification template"> + <xpath expr="//t[@t-set='access_name']" position="after"> + <t t-if="record._name == 'purchase.order'"> + <t t-if="record.state == 'purchase' and not record.env.context.get('is_reminder')"> + <t t-set="access_name">Confirm</t> + <t t-set="access_url" t-value="record.get_confirm_url(confirm_type='reception')"/> + </t> + </t> + </xpath> + <xpath expr="//a[@t-att-href='access_url']" position="replace"> + <t t-if="record._name == 'purchase.order' and record.env.context.get('is_reminder')"> + <a t-att-href="record.get_confirm_url(confirm_type='reminder')" + style="margin-right: 10px; background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;"> + Yes + </a> + <a t-att-href="record.get_update_url()" + style="margin-left: 10px; background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;"> + No, Update Dates + </a> + <div>&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> diff --git a/addons/purchase/data/purchase_data.xml b/addons/purchase/data/purchase_data.xml index d4360ef74d4..fa1af422096 100644 --- a/addons/purchase/data/purchase_data.xml +++ b/addons/purchase/data/purchase_data.xml @@ -36,5 +36,19 @@ <field name="state">code</field> <field name="code">action = records.action_share()</field> </record> + + <!-- Default value for company_dependant field --> + <record forcecreate="True" id="receipt_reminder_email" model="ir.property"> + <field name="name">receipt_reminder_email</field> + <field name="type" eval="'boolean'"/> + <field name="fields_id" search="[('model','=','res.partner'),('name','=','receipt_reminder_email')]"/> + <field eval="True" name="value"/> + </record> + <record forcecreate="True" id="reminder_date_before_receipt" model="ir.property"> + <field name="name">reminder_date_before_receipt</field> + <field name="type" eval="'integer'"/> + <field name="fields_id" search="[('model','=','res.partner'),('name','=','reminder_date_before_receipt')]"/> + <field eval="1" name="value"/> + </record> </data> </odoo> diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 5fbb236777e..ff000500ac7 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - from datetime import datetime from dateutil.relativedelta import relativedelta from itertools import groupby +from werkzeug.urls import url_encode from odoo import api, fields, models, SUPERUSER_ID, _ from odoo.osv import expression @@ -127,6 +127,14 @@ class PurchaseOrder(models.Model): company_id = fields.Many2one('res.company', 'Company', required=True, index=True, states=READONLY_STATES, default=lambda self: self.env.company.id) currency_rate = fields.Float("Currency Rate", compute='_compute_currency_rate', compute_sudo=True, store=True, readonly=True, help='Ratio between the purchase order currency and the company currency') + mail_reminder_confirmed = fields.Boolean("Reminder Confirmed", default=False, readonly=True, copy=False, help="True if the reminder email is confirmed by the vendor.") + mail_reception_confirmed = fields.Boolean("Reception Confirmed", default=False, readonly=True, copy=False, help="True if PO reception is confirmed by the vendor.") + + receipt_reminder_email = fields.Boolean('Receipt Reminder Email', related='partner_id.receipt_reminder_email', readonly=False, + help='Automatically send a reminder email to your vendors before receipt') + reminder_date_before_receipt = fields.Integer('Days Before Receipt', related='partner_id.reminder_date_before_receipt', readonly=False, + help="Number of days to send reminder email before the promised receipt date") + @api.constrains('company_id', 'order_line') def _check_order_line_company_id(self): for order in self: @@ -178,6 +186,11 @@ class PurchaseOrder(models.Model): result.append((po.id, name)) return result + @api.onchange('date_planned') + def onchange_date_planned(self): + if self.date_planned: + self.order_line.date_planned = self.date_planned + @api.model def create(self, vals): if vals.get('name', 'New') == 'New': @@ -361,6 +374,8 @@ class PurchaseOrder(models.Model): order.button_approve() else: order.write({'state': 'to approve'}) + if order.partner_id not in order.message_partner_ids: + order.message_subscribe([order.partner_id.id]) return True def button_cancel(self): @@ -613,6 +628,79 @@ class PurchaseOrder(models.Model): return result + @api.model + def _send_reminder_mail(self): + if not self.user_has_groups('purchase.group_send_reminder') and not self.receipt_reminder_email: + return + + template = self.env.ref('purchase.email_template_edi_purchase_reminder', raise_if_not_found=False) + if template: + for order in self._get_orders_to_remind(): + date = order.date_planned or order.expected_date + if date and (date - relativedelta(days=order.reminder_date_before_receipt)).date() == datetime.today().date(): + order.with_context(is_reminder=True).message_post_with_template(template.id, email_layout_xmlid="mail.mail_notification_paynow", composition_mode='comment') + + @api.model + def _get_orders_to_remind(self): + """When auto sending a reminder mail, only send for unconfirmed purchase + order and not all products are service.""" + return self.search([ + ('state', 'in', ['purchase', 'done']), + ('mail_reminder_confirmed', '=', False) + ]).filtered(lambda p: p.mapped('order_line.product_id.product_tmpl_id.type') != ['service']) + + def get_confirm_url(self, confirm_type=None): + """Create url for confirm reminder or purchase reception email for sending + in mail.""" + if confirm_type in ['reminder', 'reception']: + param = url_encode({ + 'confirm': confirm_type, + 'confirmed_date': (self.date_planned or self.expected_date).date(), + }) + return self.get_portal_url(query_string='&%s' % param) + return self.get_portal_url() + + def get_update_url(self): + """Create portal url for user to update the scheduled date on purchase + order lines.""" + update_param = url_encode({'update': 'True'}) + return self.get_portal_url(query_string='&%s' % update_param) + + def confirm_reminder_mail(self, confirmed_date=False): + for order in self: + if order.state in ['purchase', 'done'] and not order.mail_reminder_confirmed: + order.mail_reminder_confirmed = True + date = confirmed_date or (self.date_planned or self.expected_date).date() + order.message_post(body="%s confirmed the receipt will take place on %s." % (order.partner_id.name, date)) + + def _confirm_reception_mail(self): + for order in self: + if order.state in ['purchase', 'done'] and not order.mail_reception_confirmed: + order.mail_reception_confirmed = True + order.message_post(body="The order receipt has been acknowledged by %s." % order.partner_id.name) + + def _update_date_planned_for_lines(self, updated_dates): + if updated_dates: + note = self._compose_note(updated_dates) + self.activity_schedule( + 'mail.mail_activity_data_warning', + summary="Date Updated", + note=note, + user_id=self.user_id.id + ) + + for line, date in updated_dates: + line._update_date_planned(date) + + def _compose_note(self, updated_dates): + """Helper method for creating log note when user update scheduled date + on portal website.""" + note = _('<p> %s modified receipt dates for the following products:</p>') % self.partner_id.name + for line, date in updated_dates: + note += _('<p> - %s from %s to %s </p>') % (line.product_id.display_name, line.date_planned, date) + return note + + class PurchaseOrderLine(models.Model): _name = 'purchase.order.line' _description = 'Purchase Order Line' @@ -832,7 +920,7 @@ class PurchaseOrderLine(models.Model): return # Reset date, price and quantity since _onchange_quantity will provide default values - self.date_planned = datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT) + self.date_planned = self.order_id.date_planned or datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT) self.price_unit = self.product_qty = 0.0 self._product_id_change() @@ -887,7 +975,7 @@ class PurchaseOrderLine(models.Model): params=params) if seller or not self.date_planned: - self.date_planned = self._get_date_planned(seller).strftime(DEFAULT_SERVER_DATETIME_FORMAT) + self.date_planned = self.order_id.date_planned or self._get_date_planned(seller).strftime(DEFAULT_SERVER_DATETIME_FORMAT) if not seller: self.price_unit = self.product_id.standard_price @@ -1009,7 +1097,7 @@ class PurchaseOrderLine(models.Model): if product_lang.description_purchase: name += '\n' + product_lang.description_purchase - date_planned = self._get_date_planned(seller, po=po) + date_planned = self.order_id.date_planned or self._get_date_planned(seller, po=po) return { 'name': name, @@ -1021,3 +1109,6 @@ class PurchaseOrderLine(models.Model): 'taxes_id': [(6, 0, taxes_id.ids)], 'order_id': po.id, } + + def _update_date_planned(self, updated_date): + self.date_planned = updated_date diff --git a/addons/purchase/models/res_config_settings.py b/addons/purchase/models/res_config_settings.py index fc7a80d6f54..f289b9c4317 100644 --- a/addons/purchase/models/res_config_settings.py +++ b/addons/purchase/models/res_config_settings.py @@ -30,6 +30,8 @@ class ResConfigSettings(models.TransientModel): config_parameter='purchase.use_po_lead', help="Margin of error for vendor lead times. When the system generates Purchase Orders for reordering products,they will be scheduled that many days earlier to cope with unexpected vendor delays.") + group_send_reminder = fields.Boolean("Receipt Reminder", implied_group='purchase.group_send_reminder', default=True, help="Allow automatically send email to reminder your vendor the receipt date") + @api.onchange('use_po_lead') def _onchange_use_po_lead(self): if not self.use_po_lead: diff --git a/addons/purchase/models/res_partner.py b/addons/purchase/models/res_partner.py index a65bc899944..69c7bc1be48 100644 --- a/addons/purchase/models/res_partner.py +++ b/addons/purchase/models/res_partner.py @@ -59,3 +59,8 @@ class res_partner(models.Model): supplier_invoice_count = fields.Integer(compute='_compute_supplier_invoice_count', string='# Vendor Bills') purchase_warn = fields.Selection(WARNING_MESSAGE, 'Purchase Order', help=WARNING_HELP, default="no-message") purchase_warn_msg = fields.Text('Message for Purchase Order') + + receipt_reminder_email = fields.Boolean('Receipt Reminder Email', default=True, company_dependent=True, + help='Automatically send a reminder email to your vendors before receipt') + reminder_date_before_receipt = fields.Integer('Days Before Receipt', default=1, company_dependent=True, + help="Number of days to send reminder email before the promised receipt date") diff --git a/addons/purchase/security/purchase_security.xml b/addons/purchase/security/purchase_security.xml index 1187e1769c3..7edf1066426 100644 --- a/addons/purchase/security/purchase_security.xml +++ b/addons/purchase/security/purchase_security.xml @@ -25,8 +25,17 @@ <field name="category_id" ref="base.module_category_hidden"/> </record> + <record id="group_send_reminder" model="res.groups"> + <field name="name">Send an automatic reminder email to confirm delivery</field> + <field name="category_id" ref="base.module_category_hidden"/> + </record> + </data> <data noupdate="1"> + <record model="res.groups" id="base.group_user"> + <field name="implied_ids" eval="[(4, ref('purchase.group_send_reminder'))]"/> + </record> + <record id="base.default_user" model="res.users"> <field name="groups_id" eval="[(4,ref('purchase.group_purchase_manager'))]"/> </record> diff --git a/addons/purchase/static/src/js/purchase_datetimepicker.js b/addons/purchase/static/src/js/purchase_datetimepicker.js new file mode 100644 index 00000000000..c2aaad196c4 --- /dev/null +++ b/addons/purchase/static/src/js/purchase_datetimepicker.js @@ -0,0 +1,3 @@ +$(function () { + $('input.o-purchase-datetimepicker').datetimepicker(); +}) diff --git a/addons/purchase/tests/test_access_rights.py b/addons/purchase/tests/test_access_rights.py index d7251f4d007..b895523cecc 100644 --- a/addons/purchase/tests/test_access_rights.py +++ b/addons/purchase/tests/test_access_rights.py @@ -15,6 +15,7 @@ class TestPurchaseInvoice(SavepointCase): # Create a users group_purchase_user = cls.env.ref('purchase.group_purchase_user') group_employee = cls.env.ref('base.group_user') + group_partner_manager = cls.env.ref('base.group_partner_manager') cls.purchase_user = cls.env['res.users'].with_context( no_reset_password=True @@ -22,7 +23,7 @@ class TestPurchaseInvoice(SavepointCase): 'name': 'Purchase user', 'login': 'purchaseUser', 'email': 'pu@odoo.com', - 'groups_id': [(6, 0, [group_purchase_user.id, group_employee.id])], + 'groups_id': [(6, 0, [group_purchase_user.id, group_employee.id, group_partner_manager.id])], }) cls.vendor = cls.env['res.partner'].create({ diff --git a/addons/purchase/views/assets.xml b/addons/purchase/views/assets.xml index 1532d8b70eb..321a34cb065 100644 --- a/addons/purchase/views/assets.xml +++ b/addons/purchase/views/assets.xml @@ -9,4 +9,10 @@ <script type="text/javascript" src="/purchase/static/src/js/tours/purchase.js"></script> </xpath> </template> + + <template id="assets_frontend" name="purchase assets" inherit_id="web.assets_frontend"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/purchase/static/src/js/purchase_datetimepicker.js"></script> + </xpath> + </template> </odoo> diff --git a/addons/purchase/views/portal_templates.xml b/addons/purchase/views/portal_templates.xml index e9553037b0b..0618c182943 100644 --- a/addons/purchase/views/portal_templates.xml +++ b/addons/purchase/views/portal_templates.xml @@ -214,4 +214,100 @@ </t> </template> + <template id="portal_my_purchase_order_update_date" name="Portal: My Purchase Order Update Dates" inherit_id="purchase.portal_my_purchase_order" primary="True"> + <xpath expr="//div[hasclass('card-body')]" position="replace"> + <div class="card-body"> + <div class="mb-4"> + <strong>Confirmation Date:</strong> <span t-field="order.date_approve" t-options='{"widget": "date"}'/><br/> + <div t-att-class="'d-inline' if order.date_planned else 'd-none'"> + <strong>Receipt Date:</strong><span class="ml-1" t-field="order.date_planned" t-options='{"widget": "date"}'/> + </div> + </div> + <h3 class="font-weight-normal">Pricing</h3> + + <form t-attf-action="/my/purchase/#{order.id}/update?access_token=#{order.access_token}" method="post"> + <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/> + + <table class="table table-sm"> + <thead class="bg-100"> + <tr> + <th>Products</th> + <th class="text-right d-none d-sm-table-cell">Unit Price</th> + <th class="text-right">Quantity</th> + <th class="text-right">Scheduled Date</th> + <th class="text-right">Updated Date</th> + <th class="text-right">Subtotal</th> + </tr> + </thead> + <tbody> + <t t-foreach="order.order_line" t-as="ol"> + <tr> + <td> + <img t-att-src="image_data_uri(resize_to_48(ol.product_id.image_1024))" alt="Product" class="d-none d-lg-inline"/> + <span t-esc="ol.name"/> + </td> + <td class="text-right d-none d-sm-table-cell"> + <span t-field="ol.price_unit" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/> + </td> + <td class="text-right"> + <span t-esc="ol.product_qty"/> + </td> + <td class="text-right"> + <span t-esc="ol.date_planned"/> + </td> + <td class="text-right"> + <div class="container"> + <div class="form-group"> + <div class="input-group date"> + <input type="text" class="form-control datetimepicker-input o-purchase-datetimepicker" t-attf-id="datetimepicker_#{ol.id}" t-att-name="ol.id" + data-toggle="datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" t-attf-data-target="#datetimepicker_#{ol.id}"/> + </div> + </div> + </div> + </td> + <td class="text-right"> + <span t-field="ol.price_subtotal" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/> + </td> + </tr> + </t> + </tbody> + </table> + <div class="row"> + <div class="col-sm-7 col-md-5 ml-auto"> + <table class="table table-sm"> + <tbody> + <tr> + <td> + <strong>Untaxed Amount:</strong> + </td> + <td class="text-right"> + <span t-field="order.amount_untaxed" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/> + </td> + </tr> + <tr> + <td> + <strong>Taxes:</strong> + </td> + <td class="text-right"> + <span t-field="order.amount_tax" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/> + </td> + </tr> + <tr> + <td> + <strong>Total:</strong> + </td> + <td class="text-right"> + <span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.currency_id}'/> + </td> + </tr> + </tbody> + </table> + </div> + </div> + <input class="btn btn-primary" type="submit" value="Send Updated Dates"/> + </form> + </div> + </xpath> + </template> + </odoo> diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml index e3188e424a1..a90aafc226c 100644 --- a/addons/purchase/views/purchase_views.xml +++ b/addons/purchase/views/purchase_views.xml @@ -143,6 +143,7 @@ <button name="print_quotation" string="Print RFQ" type="object" states="sent" groups="base.group_user"/> <button name="button_confirm" type="object" states="draft" string="Confirm Order" id="draft_confirm"/> <button name="action_rfq_send" states="purchase" string="Send PO by Email" type="object" context="{'send_rfq':False}"/> + <button name="confirm_reminder_mail" string="Confirm Receipt Date" type="object" attrs="{'invisible': ['|', ('state', 'not in', ('purchase', 'done')), ('mail_reminder_confirmed', '=', True)]}"/> <button name="action_create_invoice" string="Create Bill" type="object" context="{'create_bill':True}" attrs="{'invisible': ['|', '|', ('state', 'not in', ('purchase', 'done')), ('invoice_status', 'not in', ('no', 'invoiced')), ('order_line', '=', [])]}"/> <button name="button_draft" states="cancel" string="Set to Draft" type="object" /> <button name="button_cancel" states="draft,to approve,sent,purchase" string="Cancel" type="object" /> @@ -176,9 +177,28 @@ </group> <group> <field name="date_order" attrs="{'invisible': [('state','=','purchase')]}"/> - <field name="date_approve" attrs="{'invisible': [('state','!=','purchase')]}"/> + <label for="date_approve" attrs="{'invisible': [('state','!=','purchase')]}"/> + <div name="date_approve" attrs="{'invisible': [('state','!=','purchase')]}" class="o_row"> + <field name="date_approve"/> + <field name="mail_reception_confirmed" invisible="1"/> + <span class="text-muted" attrs="{'invisible': [('mail_reception_confirmed','=', False)]}">(confirmed by vendor)</span> + </div> + <label for="date_planned"/> + <div name="date_planned_and_confirmation"> + <div name="date_planned_div" class="o_row"> + <field name="date_planned" attrs="{'readonly': [('state', 'not in', ('draft', 'sent', 'to approve', 'purchase'))]}"/> + <span name='expected_date_span' class="text-muted" attrs="{'invisible': ['|', ('expected_date', '=', False), ('date_planned', '!=', False)]}">Per line, first on <field name="expected_date" widget="date"/></span> + <field name="mail_reminder_confirmed" invisible="1"/> + <span class="text-muted" attrs="{'invisible': [('mail_reminder_confirmed', '=', False)]}">(confirmed by vendor)</span> + </div> + <div name="confirmation" class="oe_edit_only o_row" groups='purchase.group_send_reminder'> + <field name="receipt_reminder_email"/> + <span>Ask confirmation</span> + <field name="reminder_date_before_receipt" attrs="{'readonly': [('receipt_reminder_email', '=', False)]}"/> + <span> days before</span> + </div> + </div> <field name="origin" attrs="{'invisible': [('origin','=',False)]}"/> - <field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/> </group> </group> <notebook> @@ -209,7 +229,7 @@ context="{'partner_id':parent.partner_id, 'quantity':product_qty,'uom':product_uom, 'company_id': parent.company_id}" force_save="1" domain="[('purchase_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/> <field name="name" widget="section_and_note_text"/> - <field name="date_planned" optional="hide" attrs="{'required': [('display_type', '=', False)]}"/> + <field name="date_planned" optional="hide" attrs="{'required': [('display_type', '=', False)], 'readonly': [('parent.date_planned', '!=', False)]}" force_save="1"/> <field name="account_analytic_id" optional="hide" context="{'default_partner_id':parent.partner_id}" groups="analytic.group_analytic_accounting" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/> <field name="analytic_tag_ids" optional="hide" groups="analytic.group_analytic_tags" widget="many2many_tags" options="{'color_field': 'color'}" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"/> <field name="product_qty"/> @@ -253,7 +273,6 @@ <field name="date_planned" widget="date" attrs="{'required': [('display_type', '=', False)]}"/> <field name="account_analytic_id" colspan="2" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" groups="analytic.group_analytic_accounting"/> <field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" widget="many2many_tags" options="{'color_field': 'color'}"/> - <field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/> </group> <group colspan="12"> <notebook> @@ -340,15 +359,11 @@ </page> <page string="Other Information" name="purchase_delivery_invoice"> <group> - <group name="planning"> - <label for="date_planned" attrs="{'invisible': [('state', '!=', 'draft'), ('date_planned', '=', False)]}"/> - <div name="date_planned_div" class="o_row" attrs="{'invisible': [('state', '!=', 'draft'), ('date_planned', '=', False)]}"> - <field name="date_planned" attrs="{'readonly': [('state', 'not in', ('draft', 'sent', 'to approve'))]}"/> - <span name="expected_date_span" class="text-muted">Expected: <field name="expected_date" widget="date"/></span> - </div> - </group> <group name="other_info"> <field name="user_id" domain="[('share', '=', False)]" widget="many2one_avatar_user"/> + <field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/> + </group> + <group name="invoice_info"> <field name="invoice_status" attrs="{'invisible': [('state', 'in', ('draft', 'sent', 'to approve', 'cancel'))]}"/> <field name="payment_term_id" attrs="{'readonly': ['|', ('invoice_status','=', 'invoiced'), ('state', '=', 'done')]}" options="{'no_create': True}"/> <field name="fiscal_position_id" options="{'no_create': True}" attrs="{'readonly': ['|', ('invoice_status','=', 'invoiced'), ('state', '=', 'done')]}"/> @@ -418,6 +433,7 @@ <field name="product_id"/> <filter name="my_Orders" string="My Orders" domain="[('user_id', '=', uid)]"/> <separator/> + <filter name="unconfirmed" string="Not Acknowledged" domain="[('mail_reception_confirmed', '=', False), ('state', '=', 'purchase')]"/> <filter name="not_invoiced" string="Waiting Bills" domain="[('invoice_status', '=', 'to invoice')]" help="Purchase orders that include lines not invoiced."/> <filter name="invoiced" string="Bills Received" domain="[('invoice_status', '=', 'invoiced')]" help="Purchase orders that have been invoiced."/> <separator/> diff --git a/addons/purchase/views/res_config_settings_views.xml b/addons/purchase/views/res_config_settings_views.xml index 52c0a1dabf4..c143f2088d7 100644 --- a/addons/purchase/views/res_config_settings_views.xml +++ b/addons/purchase/views/res_config_settings_views.xml @@ -74,6 +74,17 @@ </div> </div> </div> + <div class="col-12 col-lg-6 o_setting_box" id="auto_receipt_reminder"> + <div class="o_setting_left_pane"> + <field name="group_send_reminder"/> + </div> + <div class="o_setting_right_pane"> + <label for="group_send_reminder"/> + <div class="text-muted"> + Automatically remind the receipt date to your vendors + </div> + </div> + </div> </div> <h2>Invoicing</h2> <div class="row mt16 o_settings_container" name="invoicing_settings_container"> diff --git a/addons/purchase/views/res_partner_views.xml b/addons/purchase/views/res_partner_views.xml index 05b46795a76..58d675d5a16 100644 --- a/addons/purchase/views/res_partner_views.xml +++ b/addons/purchase/views/res_partner_views.xml @@ -6,13 +6,15 @@ <field name="model">res.partner</field> <field name="inherit_id" ref="base.view_partner_form"/> <field name="priority">36</field> - <field name="groups_id" eval="[(4, ref('base.group_multi_currency'))]"/> <field name="arch" type="xml"> <group name="purchase" position="inside"> - <field name="property_purchase_currency_id" options="{'no_create': True, 'no_open': True}"/> + <field name="receipt_reminder_email" groups='purchase.group_send_reminder'/> + <field name="reminder_date_before_receipt" attrs="{'invisible': [('receipt_reminder_email', '=', False)]}" groups='purchase.group_send_reminder'/> + <field name="property_purchase_currency_id" options="{'no_create': True, 'no_open': True}" groups='base.group_multi_currency'/> </group> </field> </record> + <record id="act_res_partner_2_purchase_order" model="ir.actions.act_window"> <field name="name">RFQs and Purchases</field> <field name="res_model">purchase.order</field> diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py index 5ff7c7f67fd..cb8b060eaea 100644 --- a/addons/purchase_stock/models/purchase.py +++ b/addons/purchase_stock/models/purchase.py @@ -3,6 +3,7 @@ from odoo import api, fields, models, SUPERUSER_ID, _ from odoo.tools.float_utils import float_compare +from datetime import datetime from dateutil import relativedelta from odoo.exceptions import UserError @@ -252,6 +253,31 @@ class PurchaseOrder(models.Model): subtype_id=self.env.ref('mail.mt_note').id) return True + def _update_date_planned_for_lines(self, updated_dates): + note = self._compose_note(updated_dates) + + validated_picking = self.picking_ids.filtered(lambda p: p.state == 'done') + if validated_picking: + note += _("<p>Those dates couldn’t be modified accordingly on the receipt %s which had already been validated.</p>") % validated_picking.name + else: + note += _("<p>Those dates have been updated accordingly on the receipt %s.</p>") % self.picking_ids[0].name + for line, date in updated_dates: + date = datetime.strptime(date, '%Y-%m-%d %H:%M:%S') + line._update_date_planned(date) + + self.activity_schedule( + 'mail.mail_activity_data_warning', + summary="Date Updated", + note=note, + user_id=self.user_id.id or SUPERUSER_ID + ) + + @api.model + def _get_orders_to_remind(self): + """When auto sending reminder mails, don't send for purchase order with + validated receipts.""" + return super()._get_orders_to_remind().filtered(lambda p: not p.effective_date) + class PurchaseOrderLine(models.Model): _inherit = 'purchase.order.line' @@ -512,6 +538,12 @@ class PurchaseOrderLine(models.Model): return outgoing_moves, incoming_moves + def _update_date_planned(self, updated_date): + move_to_update = self.move_ids.filtered(lambda m: m.state not in ['done', 'cancel']) + if move_to_update: + super()._update_date_planned(updated_date) + self._update_move_expected_date(updated_date) + @api.model def _update_qty_received_method(self): """Update qty_received_method for old PO before install this module.""" diff --git a/addons/purchase_stock/report/vendor_delay_report.xml b/addons/purchase_stock/report/vendor_delay_report.xml index 7391d5eade7..b5d4cbf0362 100644 --- a/addons/purchase_stock/report/vendor_delay_report.xml +++ b/addons/purchase_stock/report/vendor_delay_report.xml @@ -32,6 +32,4 @@ <field name="target">current</field> <field name="context">{'search_default_later_than_a_year_ago':1}</field> </record> - - <menuitem id="delay_report" name="Vendor Delay" parent="purchase.menu_purchase_root" sequence="99" groups="purchase.group_purchase_manager" action="action_purchase_vendor_delay_report"/> </odoo> diff --git a/addons/purchase_stock/views/purchase_views.xml b/addons/purchase_stock/views/purchase_views.xml index 981802114c5..9457ee3ea45 100644 --- a/addons/purchase_stock/views/purchase_views.xml +++ b/addons/purchase_stock/views/purchase_views.xml @@ -9,6 +9,9 @@ <xpath expr="//header/button[@name='action_rfq_send']" position="after"> <button name="action_view_picking" string="Receive Products" class="oe_highlight" type="object" attrs="{'invisible': ['|', '|' , ('is_shipped', '=', True), ('state','not in', ('purchase','done')), ('picking_count', '=', 0)]}"/> </xpath> + <xpath expr="//header/button[@name='confirm_reminder_mail']" position="replace"> + <button name="confirm_reminder_mail" string="Confirm Receipt Date" type="object" attrs="{'invisible': ['|', '|', ('state', 'not in', ('purchase', 'done')), ('mail_reminder_confirmed', '=', True), ('effective_date', '!=', False)]}"/> + </xpath> <xpath expr="//div[hasclass('oe_button_box')]" position="inside"> <button type="object" name="action_view_picking" @@ -20,15 +23,15 @@ </xpath> <xpath expr="//field[@name='partner_id']" position="replace"> <label for="partner_id"/> - <div class="o_row"> - <field name="partner_id" widget="res_partner_many2one" context="{'res_partner_search_mode': 'supplier', 'show_vat': True}" - placeholder="Name, TIN, Email, or Reference" - /> - <div class="oe_edit_only"> - <button name="%(action_purchase_vendor_delay_report)d" type="action" class="oe_stat_button" context="{'search_default_partner_id': partner_id}"> - <field name="on_time_rate" class="text-muted" widget='percentpie'/> - </button> - </div> + <div> + <field name="partner_id" widget="res_partner_many2one" context="{'res_partner_search_mode': 'supplier', 'show_vat': True}" + placeholder="Name, TIN, Email, or Reference" + /> + <div class="oe_edit_only"> + <button name="%(action_purchase_vendor_delay_report)d" type="action" class="oe_stat_button" context="{'search_default_partner_id': partner_id}"> + <field name="on_time_rate" class="text-muted" widget='percentpie'/> + </button> + </div> </div> </xpath> <xpath expr="//field[@name='currency_id']" position="after"> @@ -37,12 +40,15 @@ <xpath expr="//field[@name='order_line']/tree//field[@name='date_planned']" position="after"> <field name="move_dest_ids" invisible="1"/> </xpath> - <xpath expr="//span[@name='expected_date_span']" position="attributes"> - <attribute name="attrs"> - {'invisible': [('effective_date', '!=', False), ('date_planned', '!=', False)]} - </attribute> + <xpath expr="//div[@name='confirmation']" position="replace"> + <div name="confirmation" class="oe_edit_only o_row" attrs="{'invisible': [('effective_date', '!=', False)]}" groups='purchase.group_send_reminder'> + <field name="receipt_reminder_email"/> + <span>Ask confirmation</span> + <field name="reminder_date_before_receipt" attrs="{'readonly': [('receipt_reminder_email', '=', False)]}"/> + <span> days before</span> + </div> </xpath> - <xpath expr="//div[@name='date_planned_div']" position="after"> + <xpath expr="//div[@name='date_planned_and_confirmation']" position="after"> <field name="effective_date" attrs="{'invisible': [('effective_date', '=', False)]}"/> </xpath> <xpath expr="//field[@name='order_line']/form//field[@name='invoice_lines']" position="after"> |
