[IMP] stock: improve the alert system
Remove the "manual" system alert and replace it by a date field, `delay_alert_date`. This field is computed in the method `_delay_alert_check` when we write on the date of a stock move. The method `_delay_alert_check` look to the next stock move and to the previous know to check if they will be late or not. If a stock move will be late because of one of the previous stock move, `delay_alert_date` will contains the minimal date that the stock move should have. Otherwise, `delay_alert_date` will be False. Task #1970595 Co-authored-by: sle <sle@odoo.com>
This commit is contained in:
@@ -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 <a href='#' data-oe-model='%s' data-oe-id='%s'>%s</a>.") % (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 <a href='#' data-oe-model='%s' data-oe-id='%s'>%s</a>.") % (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 <a href='#' data-oe-model='%s' data-oe-id='%s'>%s</a>.") % (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.
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user