[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:
@@ -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),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)]}">
|
||||
|
||||
Reference in New Issue
Block a user