diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 174e2b7a177..4113b282e86 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -135,6 +135,7 @@ class StockMove(models.Model):
propagate_date_minimum_delta = fields.Integer(string='Reschedule if Higher Than',
help='The change must be higher than this value to be propagated')
delay_alert = fields.Boolean('Alert if Delay')
+ delay_alert_date = fields.Datetime('Delay Alert Date', help='Process at this date to be on time')
picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type', check_company=True)
inventory_id = fields.Many2one('stock.inventory', 'Inventory', check_company=True)
move_line_ids = fields.One2many('stock.move.line', 'move_id')
@@ -275,7 +276,7 @@ class StockMove(models.Model):
move.product_uom_qty, move.product_id.uom_id, rounding_method=rounding_method)
def _get_move_lines(self):
- """ This will return the move lines to consider when applying _quantity_done_compute on a stock.move.
+ """ This will return the move lines to consider when applying _quantity_done_compute on a stock.move.
In some context, such as MRP, it is necessary to compute quantity_done on filtered sock.move.line."""
self.ensure_one()
return self.move_line_ids or self.move_line_nosuggest_ids
@@ -338,7 +339,7 @@ class StockMove(models.Model):
and is represented by the aggregated `product_qty` on the linked move lines. If the move
is force assigned, the value will be 0.
"""
- result = {data['move_id'][0]: data['product_qty'] for data in
+ result = {data['move_id'][0]: data['product_qty'] for data in
self.env['stock.move.line'].read_group([('move_id', 'in', self.ids)], ['move_id','product_qty'], ['move_id'])}
for rec in self:
rec.reserved_availability = rec.product_id.uom_id._compute_quantity(result.get(rec.id, 0.0), rec.product_uom, rounding_method='HALF-UP')
@@ -466,15 +467,13 @@ class StockMove(models.Model):
new_date = fields.Datetime.to_datetime(vals.get(propagated_date_field))
for move in self:
move_dest_ids = move.move_dest_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
- if not move_dest_ids:
- continue
delta_days = (new_date - move.date_expected).total_seconds() / 86400
- if not move.propagate_date or abs(delta_days) < move.propagate_date_minimum_delta:
- move_dest_ids._delay_alert_log_activity('manual', move)
- continue
- for move_dest in move_dest_ids:
- move_dest.date_expected += relativedelta.relativedelta(days=delta_days)
- move_dest_ids._delay_alert_log_activity('auto', move)
+ if move.propagate_date and abs(delta_days) >= move.propagate_date_minimum_delta and move_dest_ids:
+ for move_dest in move_dest_ids:
+ move_dest.date_expected += relativedelta.relativedelta(days=delta_days)
+ move_dest_ids.filtered(lambda m: m.delay_alert)._propagate_date_log_activity(move)
+ if move.delay_alert:
+ move._delay_alert_check(new_date)
# Manual tracking of the `state` field for the stock.picking records.
track_pickings = (
@@ -514,25 +513,18 @@ class StockMove(models.Model):
"""
return list(self.mapped('picking_id'))
- def _delay_alert_log_activity(self, mode, move_orig):
+ def _propagate_date_log_activity(self, move_orig):
"""Post a delay alert next activity on the documents linked to `self`. If the delay alert
is already present on the document, it isn't posted twice.
- :param mode: 'auto' or 'manual' as a string
:param move_orig: the stock move triggering the delay alert on the next document
"""
- assert mode in ('auto', 'manual')
-
doc_orig = move_orig._delay_alert_get_documents()
- documents = self.filtered(lambda m: m.delay_alert)._delay_alert_get_documents()
+ documents = self._delay_alert_get_documents()
if not documents or not doc_orig:
return
- if mode == 'auto':
- msg = _("The scheduled date has been automatically updated due to a delay on %s.") % (doc_orig[0]._name, doc_orig[0].id, doc_orig[0].name)
- else:
- msg = _("The scheduled date should be updated due to a delay on %s.") % (doc_orig[0]._name, doc_orig[0].id, doc_orig[0].name)
-
+ msg = _("The scheduled date has been automatically updated due to a delay on %s.") % (doc_orig[0]._name, doc_orig[0].id, doc_orig[0].name)
# write the message on each document
for doc in documents:
if doc.activity_ids.filtered(lambda a: a.automated and doc_orig[0].name in a.note):
@@ -544,6 +536,53 @@ class StockMove(models.Model):
user_id=doc.user_id.id or SUPERUSER_ID
)
+ def _delay_alert_check(self, new_date):
+ """Set an alert on late moves by using the `delay_alert_date` field.
+ The alert is always on the move that cannot be done because its preceding moves are late.
+
+ :param new_date: the new expected date, to set when calling this method before `create` or `write`
+ :param new_date: datetime, optional
+ """
+ self.ensure_one()
+ if self.state in ('done', 'cancel'):
+ return
+
+ # Check if `self` is scheduled after the next moves. If so, the next moves are late.
+ next_done_moves = self.browse()
+ next_nondone_moves = self.browse()
+ next_moves_dates = []
+ for move in self.move_dest_ids:
+ if move.state == 'done':
+ next_done_moves |= move
+ elif move.state != 'cancel':
+ next_nondone_moves |= move
+ next_moves_dates += next_done_moves.mapped('date')
+ next_moves_dates += next_nondone_moves.mapped('date_expected')
+ if next_moves_dates:
+ next_moves_date = min(next_moves_dates)
+ if new_date > next_moves_date:
+ next_nondone_moves.write({'delay_alert_date': new_date})
+ else:
+ next_nondone_moves.write({'delay_alert_date': False})
+
+ # Check if `self` is scheduled before the previous moves. If so, `self` if late.
+ previous_done_moves = self.browse()
+ previous_nondone_moves = self.browse()
+ previous_moves_dates = []
+ for move in self.move_orig_ids:
+ if move.state == 'done':
+ previous_done_moves |= move
+ elif move.state != 'cancel':
+ previous_nondone_moves |= move
+ previous_moves_dates += previous_done_moves.mapped('date')
+ previous_moves_dates += previous_nondone_moves.mapped('date_expected')
+ if previous_moves_dates:
+ previous_moves_date = max(previous_moves_dates)
+ if new_date < previous_moves_date:
+ self.write({'delay_alert_date': previous_moves_date})
+ else:
+ self.write({'delay_alert_date': False})
+
def action_show_details(self):
""" Returns an action that will open a form view (in a popup) allowing to work on all the
move lines of a particular move. This form view is used when "show operations" is not
@@ -1311,7 +1350,7 @@ class StockMove(models.Model):
""" If the quantity done on a move exceeds its quantity todo, this method will create an
extra move attached to a (potentially split) move line. If the previous condition is not
met, it'll return an empty recordset.
-
+
The rationale for the creation of an extra move is the application of a potential push
rule that will handle the extra quantities.
"""
diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py
index e84a44b6f91..f037dce934a 100644
--- a/addons/stock/tests/test_move2.py
+++ b/addons/stock/tests/test_move2.py
@@ -2531,3 +2531,88 @@ class TestRoutes(TestStockCommon):
self.assertEqual(len(activity), 2, 'not enough activity created')
self.assertTrue(picking_pick_1.name in activity[0].note + activity[1].note, 'Wrong activity message')
self.assertTrue(picking_pick_2.name in activity[0].note + activity[1].note, 'Wrong activity message')
+
+ def test_delay_alert_3(self):
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
+ warehouse.delivery_steps = 'pick_pack_ship'
+ partner_demo_customer = self.partner
+ final_location = partner_demo_customer.property_stock_customer
+ product_a = self.env['product.product'].create({
+ 'name': 'ProductA',
+ 'type': 'product',
+ })
+ pg = self.env['procurement.group'].create({'name': 'Test-delay_alert_3'})
+ self.env['procurement.group'].run([
+ pg.Procurement(
+ product_a,
+ 4.0,
+ product_a.uom_id,
+ final_location,
+ 'delay',
+ 'delay',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg
+ }
+ ),
+ ])
+ ship, pack, pick = self.env['stock.move'].search([('product_id', '=', product_a.id)])
+ (ship + pack + pick).propagate_date = False
+ (ship + pack + pick).delay_alert = True
+
+ # by default they all the the same `date_expected`
+ self.assertEqual(set((ship + pack + pick).mapped('date_expected')), {pick.date_expected})
+
+ # pick - pack - ship
+ ship.date_expected += timedelta(days=2)
+ pack.date_expected += timedelta(days=1)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertFalse(pack.delay_alert_date)
+ self.assertFalse(ship.delay_alert_date)
+
+ # move the pack after the ship
+ # pick - ship - pack
+ pack.date_expected += timedelta(days=2)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertFalse(pack.delay_alert_date)
+ self.assertTrue(ship.delay_alert_date)
+ self.assertAlmostEqual(ship.delay_alert_date, pack.date_expected)
+
+ # restore the pack before the ship
+ # pick - pack - ship
+ pack.date_expected -= timedelta(days=2)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertFalse(pack.delay_alert_date)
+ self.assertFalse(ship.delay_alert_date)
+
+ # move the pick after the pack
+ # pack - ship - pick
+ pick.date_expected += timedelta(days=3)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertTrue(pack.delay_alert_date)
+ self.assertFalse(ship.delay_alert_date)
+ self.assertAlmostEqual(pack.delay_alert_date, pick.date_expected)
+
+ # move the ship before the pack
+ # ship - pack - pick
+ ship.date_expected -= timedelta(days=2)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertTrue(pack.delay_alert_date)
+ self.assertTrue(ship.delay_alert_date)
+ self.assertAlmostEqual(pack.delay_alert_date, pick.date_expected)
+ self.assertAlmostEqual(ship.delay_alert_date, pack.date_expected)
+
+ # move the pack at the end
+ # ship - pick - pack
+ pack.date_expected = pick.date_expected + timedelta(days=2)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertFalse(pack.delay_alert_date)
+ self.assertTrue(ship.delay_alert_date)
+ self.assertAlmostEqual(ship.delay_alert_date, pack.date_expected)
+
+ # fix the ship
+ ship.date_expected = pack.date_expected + timedelta(days=2)
+ self.assertFalse(pick.delay_alert_date)
+ self.assertFalse(pack.delay_alert_date)
+ self.assertFalse(ship.delay_alert_date)