[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:
Debauche Stéphane
2019-12-30 14:27:17 +00:00
co-authored by sle
parent 697d90e658
commit 490addf86b
2 changed files with 145 additions and 21 deletions
+60 -21
View File
@@ -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.
"""
+85
View File
@@ -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)