[IMP] stock: alert in case of rescheduling

In a move chain, if the scheduled date of a move N is updated, we would
like to create an alert on the move N+1.

This commit adds a boolean field on stock.rule that, if set to True, will
create a next activity on the stock.picking of the move that are created
from this rule, in case the previous move has its scheduled date changed.

The message will depends on the propagation of the schedule date between
the two moves:
 - If the date is propagated, the message informs the picking user that
  the transfer is delayed.
 - If the date is not propagated and the new date of move N is after the
   schedule date of move N+1, the message says that the scheduled date
   will be outdated.

Task: 2001643
This commit is contained in:
William Henrotin
2019-07-09 12:14:42 +00:00
parent 64907ba7aa
commit 4c3121dbfe
7 changed files with 227 additions and 17 deletions
+5
View File
@@ -228,6 +228,11 @@ class StockMove(models.Model):
else:
return super(StockMove, self)._get_upstream_documents_and_responsibles(visited)
def _delay_alert_get_documents(self):
res = super(StockMove, self)._delay_alert_get_documents()
productions = self.mapped('raw_material_production_id')
return res + list(productions)
def _should_be_assigned(self):
res = super(StockMove, self)._should_be_assigned()
return bool(res and not (self.production_id or self.raw_material_production_id))
@@ -824,10 +824,10 @@ class TestStockValuationWithCOA(AccountingTestCase):
# To allow testing validation of PO
def _today(*args, **kwargs):
return date_po
return datetime.strptime(date_po, "%Y-%m-%d").date()
# To allow testing validation of Delivery
def _now(*args, **kwargs):
return date_delivery + ' 01:00:00'
return datetime.strptime(date_delivery + ' 01:00:00', "%Y-%m-%d %H:%M:%S")
patchers = [
patch('odoo.fields.Date.context_today', _today),
@@ -993,11 +993,11 @@ class TestStockValuationWithCOA(AccountingTestCase):
# To allow testing validation of PO
def _today(*args, **kwargs):
return date_po
return datetime.strptime(date_po, "%Y-%m-%d").date()
# To allow testing validation of Delivery
delivery_now = date_delivery
def _now(*args, **kwargs):
return delivery_now + ' 01:00:00'
return datetime.strptime(delivery_now + ' 01:00:00', "%Y-%m-%d %H:%M:%S")
patchers = [
patch('odoo.fields.Date.context_today', _today),
+59 -11
View File
@@ -7,7 +7,7 @@ from itertools import groupby
from operator import itemgetter
from re import search as regex_search, split as regex_split
from odoo import api, fields, models, _
from odoo import api, fields, models, _, SUPERUSER_ID
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.tools.float_utils import float_compare, float_round, float_is_zero
@@ -131,6 +131,7 @@ class StockMove(models.Model):
help='The rescheduling is propagated to the next move.')
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')
picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type')
inventory_id = fields.Many2one('stock.inventory', 'Inventory')
move_line_ids = fields.One2many('stock.move.line', 'move_id')
@@ -431,15 +432,19 @@ class StockMove(models.Model):
if propagated_date_field:
#any propagation is (maybe) needed
new_date = vals.get(propagated_date_field)
for move in self:
if move.move_dest_ids and move.propagate_date:
new_date = vals.get(propagated_date_field)
delta_days = (new_date - move.date_expected).total_seconds() / 86400
if abs(delta_days) < move.propagate_date_minimum_delta:
continue
for move_dest in move.move_dest_ids:
if move_dest.state not in ('done', 'cancel'):
move_dest.date_expected += relativedelta.relativedelta(days=delta_days)
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)
track_pickings = not self._context.get('mail_notrack') and any(field in vals for field in ['state', 'picking_id', 'partially_available'])
if track_pickings:
to_track_picking_ids = set([move.picking_id.id for move in self if move.picking_id])
@@ -455,6 +460,47 @@ class StockMove(models.Model):
receipt_moves_to_reassign._action_assign()
return res
def _delay_alert_get_documents(self):
"""Returns a list of recordset of the documents linked to the stock.move in `self` in order
to post the delay alert next activity. These documents are deduplicated. This method is meant
to be overriden by other modules, each of them adding an element by type of recordset on
this list.
:return: a list of recordset of the documents linked to `self`
:rtype: list
"""
return list(self.mapped('picking_id'))
def _delay_alert_log_activity(self, mode, 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()
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)
# 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):
continue
doc.activity_schedule(
'mail.mail_activity_data_warning',
datetime.today().date(),
note=msg,
user_id=doc.user_id.id or SUPERUSER_ID
)
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
@@ -592,7 +638,8 @@ class StockMove(models.Model):
return [
'product_id', 'price_unit', 'product_packaging', 'procure_method',
'product_uom', 'restrict_partner_id', 'scrapped', 'origin_returned_move_id',
'package_level_id'
'package_level_id', 'propagate_cancel', 'propagate_date', 'propagate_date_minimum_delta',
'delay_alert',
]
@api.model
@@ -601,7 +648,8 @@ class StockMove(models.Model):
return [
move.product_id.id, move.price_unit, move.product_packaging.id, move.procure_method,
move.product_uom.id, move.restrict_partner_id.id, move.scrapped, move.origin_returned_move_id.id,
move.package_level_id.id
move.package_level_id.id, move.propagate_cancel, move.propagate_date, move.propagate_date_minimum_delta,
move.delay_alert,
]
def _clean_merged(self):
+9
View File
@@ -74,14 +74,21 @@ class StockRule(models.Model):
help='The rescheduling is propagated to the next move.')
propagate_date_minimum_delta = fields.Integer(string='Reschedule if Higher Than',
help='The change must be higher than this value to be propagated', default=1)
delay_alert = fields.Boolean(
'Alert if Delay',
help='Log an exception on the picking if this move has to be delayed (due to a change in the previous move scheduled date).',
)
@api.onchange('picking_type_id')
def _onchange_picking_type(self):
""" Modify locations to the default picking type's locations source and
destination.
Enable the delay alert if the picking type is a delivery
"""
self.location_src_id = self.picking_type_id.default_location_src_id.id
self.location_id = self.picking_type_id.default_location_dest_id.id
if self.picking_type_id.code == 'outgoing':
self.delay_alert = True
@api.onchange('route_id', 'company_id')
def _onchange_route(self):
@@ -172,6 +179,7 @@ class StockRule(models.Model):
'picking_type_id': self.picking_type_id.id,
'propagate_cancel': self.propagate_cancel,
'warehouse_id': self.warehouse_id.id,
'delay_alert': self.delay_alert
}
return new_move_vals
@@ -270,6 +278,7 @@ class StockRule(models.Model):
'propagate_date_minimum_delta': self.propagate_date_minimum_delta,
'description_picking': product_id._get_description(self.picking_type_id),
'priority': values.get('priority', "1"),
'delay_alert': self.delay_alert,
}
for field in self._get_custom_move_fields():
if field in values:
+2
View File
@@ -340,6 +340,7 @@ class Warehouse(models.Model):
'company_id': self.company_id.id,
'action': 'pull',
'auto': 'manual',
'delay_alert': True,
'route_id': self._find_global_route('stock.route_warehouse0_mto', _('Make To Order')).id
},
'update_values': {
@@ -677,6 +678,7 @@ class Warehouse(models.Model):
'procure_method': first_rule and 'make_to_stock' or 'make_to_order',
'warehouse_id': self.id,
'company_id': self.company_id.id,
'delay_alert': routing.picking_type.code == 'outgoing',
}
route_rule_values.update(values or {})
rules_list.append(route_rule_values)
+146 -1
View File
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo.addons.stock.tests.common import TestStockCommon
from odoo.exceptions import UserError
from odoo.tests import Form
@@ -1896,8 +1898,8 @@ class TestRoutes(TestStockCommon):
# create and get back the pick ship route
self.wh.write({'delivery_steps': 'pick_ship'})
self.pick_ship_route = self.wh.route_ids.filtered(lambda r: '(pick + ship)' in r.name)
def test_pick_ship_1(self):
""" Enable the pick ship route, force a procurement group on the
pick. When a second move is added, make sure the `partner_id` and
@@ -2225,3 +2227,146 @@ class TestRoutes(TestStockCommon):
# All the moves should be should have the same quantity as it is on each procurements
self.assertEquals(len(picking.move_lines), 1)
self.assertEquals(picking.move_lines.product_uom_qty, 2)
def test_delay_alert_1(self):
""" On a pick pack ship scenario, enable the delay alert flag on the pack rule. Edit the
schedule date on the pick, a delay alert should be created for the ship.
by default:
- delay alert set is only ship rule
- propagate date is True on all the pick-pack-ship rules
"""
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
warehouse.delivery_steps = 'pick_pack_ship'
partner_demo_customer = self.env.ref('base.res_partner_1')
final_location = partner_demo_customer.property_stock_customer
product_a = self.env['product.product'].create({
'name': 'ProductA',
'type': 'product',
})
product_b = self.env['product.product'].create({
'name': 'ProductA',
'type': 'product',
})
pg = self.env['procurement.group'].create({'name': 'Test-delay_alert'})
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
}
),
pg.Procurement(
product_b,
4.0,
product_a.uom_id,
final_location,
'delay',
'delay',
warehouse.company_id,
{
'warehouse_id': warehouse,
'group_id': pg
}
)
])
first_move = self.env['stock.move'].search([
('product_id', '=', product_a.id),
('state', '=', 'confirmed')
])
# Change the schedule date on the pick.
first_move.picking_id.scheduled_date += timedelta(days=2)
# No activity should be created on the pack.
activity = first_move.move_dest_ids.picking_id.activity_ids # PACK
self.assertFalse(activity)
# An activity is created on the ship.
activity = first_move.move_dest_ids.move_dest_ids.picking_id.activity_ids # SHIP
self.assertTrue(activity, '')
self.assertTrue('has been automatically' in activity.note)
# Change second time the schedule date on the pick.
first_move.picking_id.scheduled_date += timedelta(days=2)
activity = first_move.move_dest_ids.move_dest_ids.picking_id.activity_ids # SHIP
self.assertEqual(len(activity), 1)
def test_delay_alert_2(self):
""" On a pick ship scenario, two pick linked to a ship. The delay alert is set on the ship rule?
When editing the schedule date on the two pick, two delay alerts activty should be created
on the pack.
"""
self._enable_pick_ship()
# create a procurement group and set in on the pick stock rule
procurement_group0 = self.env['procurement.group'].create({})
procurement_group1 = self.env['procurement.group'].create({})
pick_rule = self.pick_ship_route.rule_ids.filtered(lambda rule: 'Stock → Output' in rule.name)
ship_rule = self.pick_ship_route.rule_ids - pick_rule
ship_rule.write({
'group_propagation_option': 'fixed',
'group_id': procurement_group0.id,
})
ship_location = pick_rule.location_id
customer_location = ship_rule.location_id
product1 = self.env['product.product'].create({'name': 'product1'})
product2 = self.env['product.product'].create({'name': 'product2'})
picking_ship = self.env['stock.picking'].create({
'location_id': ship_location.id,
'location_dest_id': self.customer_location,
'partner_id': self.partner_delta_id,
'picking_type_id': self.picking_type_out,
})
move1 = self.env['stock.move'].create({
'name': 'first out move',
'procure_method': 'make_to_order',
'location_id': ship_location.id,
'location_dest_id': customer_location.id,
'product_id': product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
'warehouse_id': self.wh.id,
'group_id': procurement_group0.id,
'origin': 'origin1',
'picking_id': picking_ship.id,
'delay_alert': True,
})
move2 = self.env['stock.move'].create({
'name': 'second out move',
'procure_method': 'make_to_order',
'location_id': ship_location.id,
'location_dest_id': customer_location.id,
'product_id': product2.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
'warehouse_id': self.wh.id,
'group_id': procurement_group1.id,
'origin': 'origin2',
'picking_id': picking_ship.id,
'delay_alert': True,
})
# confirm the picking to create the orig moves
picking_ship.action_confirm()
picking_pick_1 = move1.move_orig_ids.picking_id
picking_pick_2 = move2.move_orig_ids.picking_id
picking_pick_1.scheduled_date += timedelta(days=2)
picking_pick_2.scheduled_date += timedelta(days=2)
activity = picking_ship.activity_ids
self.assertEqual(len(activity), 2, 'not enough activity created')
self.assertTrue(picking_pick_1.name in activity[0].note, 'Wrong activity message')
self.assertTrue(picking_pick_2.name in activity[1].note, 'Wrong activity message')
+2 -1
View File
@@ -104,7 +104,8 @@
<field name="partner_address_id" attrs="{'invisible': [('action', '=', 'push')]}"/>
<field name="delay"/>
</group>
<group name="scheduling_group" string="Scheduling" attrs="{'invisible': [('action', '=', 'push')]}" groups="base.group_no_one">
<group name="scheduling_group" string="Scheduling" attrs="{'invisible': [('action', '=', 'push')]}">
<field name="delay_alert"/>
<field name="propagate_date"/>
<label for="propagate_date_minimum_delta" attrs="{'invisible': [('propagate_date', '=', False)]}"/>
<div attrs="{'invisible': [('propagate_date', '=', False)]}">