diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 1b213b65b69..c0302f04432 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import json
import datetime
from collections import defaultdict
from itertools import groupby
from odoo import api, fields, models, _
from odoo.exceptions import AccessError, UserError
-from odoo.tools import date_utils, float_round, float_is_zero
+from odoo.tools import date_utils, float_round, float_is_zero, format_datetime
class MrpProduction(models.Model):
@@ -205,6 +206,8 @@ class MrpProduction(models.Model):
'Propagate cancel and split',
help='If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too')
delay_alert = fields.Boolean(string='Delay alert')
+ delay_alert_date = fields.Datetime('Delay Alert Date', compute='_compute_delay_alert_date', search='_search_delay_alert_date')
+ json_popover = fields.Char('JSON data for the popover widget', compute='_compute_json_popover')
propagate_date = fields.Boolean(string="Propagate Rescheduling",
help='The rescheduling is propagated to the next move.')
propagate_date_minimum_delta = fields.Integer(string='Reschedule if Higher Than',
@@ -232,6 +235,26 @@ class MrpProduction(models.Model):
def _set_date_planned_finished(self):
self.move_finished_ids.write({'date_expected': self.date_planned_finished})
+ @api.depends('move_raw_ids.delay_alert_date')
+ def _compute_delay_alert_date(self):
+ delay_alert_date_data = self.env['stock.move'].read_group([('id', 'in', self.move_raw_ids.ids), ('delay_alert_date', '!=', False)], ['delay_alert_date:max'], 'raw_material_production_id')
+ delay_alert_date_data = {data['raw_material_production_id'][0]: data['delay_alert_date'] for data in delay_alert_date_data}
+ for production in self:
+ production.delay_alert_date = delay_alert_date_data.get(production.id, False)
+
+ def _compute_json_popover(self):
+ for production in self:
+ production.json_popover = json.dumps({
+ 'popoverTemplate': 'stock.PopoverStockRescheduling',
+ 'delay_alert_date': format_datetime(self.env, production.delay_alert_date, dt_format=False) if production.delay_alert_date else False,
+ 'late_elements': [{
+ 'id': late_document.id,
+ 'name': late_document.display_name,
+ 'model': late_document._name,
+ } for late_document in production.move_raw_ids.filtered(lambda m: m.delay_alert_date).move_orig_ids._delay_alert_get_documents()
+ ]
+ })
+
@api.depends('move_raw_ids.state', 'move_finished_ids.state')
def _compute_confirm_cancel(self):
""" If the manufacturing order contains some done move (via an intermediate
@@ -403,6 +426,11 @@ class MrpProduction(models.Model):
('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'),
]
+ @api.model
+ def _search_delay_alert_date(self, operator, value):
+ late_stock_moves = self.env['stock.move'].search([('delay_alert_date', operator, value)])
+ return ['|', ('move_raw_ids', 'in', late_stock_moves.ids), ('move_finished_ids', 'in', late_stock_moves.ids)]
+
@api.onchange('company_id')
def onchange_company_id(self):
if self.company_id:
diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml
index 8fe96b9a71a..4a506886504 100644
--- a/addons/mrp/views/mrp_production_views.xml
+++ b/addons/mrp/views/mrp_production_views.xml
@@ -25,6 +25,8 @@
+
+
@@ -157,6 +159,8 @@
('routing_id', '!=', False),
('state', 'in', ['draft', 'confirmed'])
]}"/>
+
+
@@ -234,6 +238,8 @@
+
+
@@ -317,6 +323,7 @@
+
+
+
+
The deliveries
+
+ ,
+ will be late.
+
+
diff --git a/addons/sale_stock/views/sale_order_views.xml b/addons/sale_stock/views/sale_order_views.xml
index 896797ad8f6..7a50cfa7cf8 100644
--- a/addons/sale_stock/views/sale_order_views.xml
+++ b/addons/sale_stock/views/sale_order_views.xml
@@ -25,6 +25,10 @@
+
+
+
+
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index 423c505c5eb..10d8b31dd78 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -6,10 +6,11 @@ from datetime import date
from itertools import groupby
from operator import itemgetter
import time
+import json
from odoo import api, fields, models, SUPERUSER_ID, _
from odoo.osv import expression
-from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
+from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, format_datetime
from odoo.tools.float_utils import float_compare, float_is_zero, float_round
from odoo.exceptions import UserError
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
@@ -294,6 +295,8 @@ class Picking(models.Model):
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
help="Creation Date, usually the time of the order")
date_done = fields.Datetime('Date of Transfer', copy=False, readonly=True, help="Date at which the transfer has been processed or cancelled.")
+ delay_alert_date = fields.Datetime('Delay Alert Date', compute='_compute_delay_alert_date', search='_search_delay_alert_date')
+ json_popover = fields.Char('JSON data for the popover widget', compute='_compute_json_popover')
location_id = fields.Many2one(
'stock.location', "Source Location",
default=lambda self: self.env['stock.picking.type'].browse(self._context.get('default_picking_type_id')).default_location_src_id,
@@ -377,6 +380,13 @@ class Picking(models.Model):
for picking in self:
picking.has_tracking = any(m.has_tracking != 'none' for m in picking.move_lines)
+ @api.depends('move_lines.delay_alert_date')
+ def _compute_delay_alert_date(self):
+ delay_alert_date_data = self.env['stock.move'].read_group([('id', 'in', self.move_lines.ids), ('delay_alert_date', '!=', False)], ['delay_alert_date:max'], 'picking_id')
+ delay_alert_date_data = {data['picking_id'][0]: data['delay_alert_date'] for data in delay_alert_date_data}
+ for picking in self:
+ picking.delay_alert_date = delay_alert_date_data.get(picking.id, False)
+
@api.depends('picking_type_id.show_operations')
def _compute_show_operations(self):
for picking in self:
@@ -403,6 +413,19 @@ class Picking(models.Model):
else:
picking.show_lots_text = False
+ def _compute_json_popover(self):
+ for picking in self:
+ picking.json_popover = json.dumps({
+ 'popoverTemplate': 'stock.PopoverStockRescheduling',
+ 'delay_alert_date': format_datetime(self.env, picking.delay_alert_date, dt_format=False) if picking.delay_alert_date else False,
+ 'late_elements': [{
+ 'id': late_move.id,
+ 'name': late_move.display_name,
+ 'model': late_move._name,
+ } for late_move in picking.move_lines.filtered(lambda m: m.delay_alert_date).move_orig_ids._delay_alert_get_documents()
+ ]
+ })
+
@api.depends('move_type', 'move_lines.state', 'move_lines.picking_id')
def _compute_state(self):
''' State of a picking depends on the state of its related stock.move
@@ -510,6 +533,11 @@ class Picking(models.Model):
else:
picking.show_validate = True
+ @api.model
+ def _search_delay_alert_date(self, operator, value):
+ late_stock_moves = self.env['stock.move'].search([('delay_alert_date', operator, value)])
+ return [('move_lines', 'in', late_stock_moves.ids)]
+
@api.onchange('picking_type_id', 'partner_id')
def onchange_picking_type(self):
if self.picking_type_id:
@@ -1265,3 +1293,4 @@ class Picking(models.Model):
body=message,
)
return True
+
diff --git a/addons/stock/static/src/js/stock_rescheduling_popover.js b/addons/stock/static/src/js/stock_rescheduling_popover.js
new file mode 100644
index 00000000000..e30b88bace7
--- /dev/null
+++ b/addons/stock/static/src/js/stock_rescheduling_popover.js
@@ -0,0 +1,38 @@
+odoo.define('stock.PopoverStockPicking', function (require) {
+"use strict";
+
+var core = require('web.core');
+
+var PopoverWidgetField = require('stock.popover_widget');
+var registry = require('web.field_registry');
+var _t = core._t;
+
+var PopoverStockPicking = PopoverWidgetField.extend({
+ title: _t('Planning Issue'),
+ color: 'text-warning',
+ trigger: 'focus',
+
+ _render: function () {
+ this._super();
+ if (this.$popover) {
+ var self = this;
+ this.$popover.find('a').on('click', function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ self.do_action({
+ type: 'ir.actions.act_window',
+ res_model: ev.currentTarget.getAttribute('element-model'),
+ res_id: parseInt(ev.currentTarget.getAttribute('element-id'), 10),
+ views: [[false, 'form']],
+ target: 'current'
+ });
+ });
+ }
+ },
+
+});
+
+registry.add('stock_rescheduling_popover', PopoverStockPicking);
+
+return PopoverStockPicking;
+});
diff --git a/addons/stock/static/src/xml/popover_widget.xml b/addons/stock/static/src/xml/popover_widget.xml
index 0cd4d8c7645..37cffd522df 100644
--- a/addons/stock/static/src/xml/popover_widget.xml
+++ b/addons/stock/static/src/xml/popover_widget.xml
@@ -9,4 +9,11 @@
+
+
Preceding operations
+
+ ,
+
+ planned on .
+
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index eeb1761f241..cb7338f9b98 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -171,6 +171,8 @@
+
+
@@ -206,6 +208,8 @@
+
+
@@ -294,7 +298,12 @@
+
+
+
+
+
+
diff --git a/addons/stock/views/stock_template.xml b/addons/stock/views/stock_template.xml
index 86757eba15c..aeef320b070 100644
--- a/addons/stock/views/stock_template.xml
+++ b/addons/stock/views/stock_template.xml
@@ -13,6 +13,7 @@
+