From c514e8ae6ac99a22e2deacb6353e44b75a2a42bf Mon Sep 17 00:00:00 2001 From: Haresh Shyara Date: Thu, 13 Dec 2018 11:54:34 +0000 Subject: [PATCH] [REF] stock: rescheduling propagation This commit moves the "propagation minimum delta" from a company setting to a per-rule setting. We implement this idea in the stock module and take this opportunity to clean up the override of stock move's write. We add a few basics tests. task-1913401 --- addons/stock/models/res_company.py | 1 - addons/stock/models/res_config_settings.py | 11 --- addons/stock/models/stock_move.py | 33 +++---- addons/stock/models/stock_rule.py | 6 ++ addons/stock/tests/test_proc_rule.py | 92 +++++++++++++++++++ .../stock/views/res_config_settings_views.xml | 16 ---- addons/stock/views/stock_rule_views.xml | 7 ++ 7 files changed, 119 insertions(+), 47 deletions(-) diff --git a/addons/stock/models/res_company.py b/addons/stock/models/res_company.py index 1854916bee3..121f3c1e81e 100644 --- a/addons/stock/models/res_company.py +++ b/addons/stock/models/res_company.py @@ -7,7 +7,6 @@ from odoo import api, fields, models, _ class Company(models.Model): _inherit = "res.company" - propagation_minimum_delta = fields.Integer('Minimum Delta for Propagation of a Date Change on moves linked together', default=1) internal_transit_location_id = fields.Many2one( 'stock.location', 'Internal Transit Location', ondelete="restrict", help="Technical field used for resupply routes between warehouses that belong to this company") diff --git a/addons/stock/models/res_config_settings.py b/addons/stock/models/res_config_settings.py index 371dd96145a..c52262e9515 100644 --- a/addons/stock/models/res_config_settings.py +++ b/addons/stock/models/res_config_settings.py @@ -26,12 +26,6 @@ class ResConfigSettings(models.TransientModel): implied_group='stock.group_adv_location', help="Add and customize route operations to process product moves in your warehouse(s): e.g. unload > quality control > stock for incoming products, pick > pack > ship for outgoing products. \n You can also set putaway strategies on warehouse locations in order to send incoming products into specific child locations straight away (e.g. specific bins, racks).") group_warning_stock = fields.Boolean("Warnings for Stock", implied_group='stock.group_warning_stock') - propagation_minimum_delta = fields.Integer(related='company_id.propagation_minimum_delta', string="Minimum Delta for Propagation", readonly=False) - use_propagation_minimum_delta = fields.Boolean( - string="No Rescheduling Propagation", - oldname='default_new_propagation_minimum_delta', - config_parameter='stock.use_propagation_minimum_delta', - help="Rescheduling applies to any chain of operations (e.g. Make To Order, Pick Pack Ship). In the case of MTO sales, a vendor delay (updated incoming date) impacts the expected delivery date to the customer. \n This option allows to not propagate the rescheduling if the change is not critical.") module_stock_picking_batch = fields.Boolean("Batch Pickings", oldname="module_stock_picking_wave") module_stock_barcode = fields.Boolean("Barcode Scanner") module_delivery_dhl = fields.Boolean("DHL USA") @@ -44,11 +38,6 @@ class ResConfigSettings(models.TransientModel): help="Store products in specific locations of your warehouse (e.g. bins, racks) and to track inventory accordingly.") group_stock_multi_warehouses = fields.Boolean('Multi-Warehouses', implied_group='stock.group_stock_multi_warehouses') - @api.onchange('use_propagation_minimum_delta') - def _onchange_use_propagation_minimum_delta(self): - if not self.use_propagation_minimum_delta: - self.propagation_minimum_delta = 1 - @api.onchange('group_stock_multi_locations') def _onchange_group_stock_multi_locations(self): if not self.group_stock_multi_locations: diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 00d5ea57ea5..f2e277f8245 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -10,7 +10,6 @@ from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError from odoo.osv import expression -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from odoo.tools.float_utils import float_compare, float_round, float_is_zero PROCUREMENT_PRIORITIES = [('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')] @@ -128,6 +127,10 @@ class StockMove(models.Model): propagate = fields.Boolean( 'Propagate cancel and split', default=True, help='If checked, when this move is cancelled, cancel the linked move too') + propagate_date = fields.Boolean(string="Propagate Rescheduling", + 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') 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') @@ -403,7 +406,6 @@ class StockMove(models.Model): # TDE CLEANME: it is a gros bordel + tracking Picking = self.env['stock.picking'] - propagated_changes_dict = {} #propagation of expected date: propagated_date_field = False if vals.get('date_expected'): @@ -413,24 +415,17 @@ class StockMove(models.Model): #propagate also any delta observed when setting the move as done propagated_date_field = 'date' - if not self._context.get('do_not_propagate', False) and (propagated_date_field or propagated_changes_dict): + if propagated_date_field: #any propagation is (maybe) needed for move in self: - if move.move_dest_ids and move.propagate: - if 'date_expected' in propagated_changes_dict: - propagated_changes_dict.pop('date_expected') - if propagated_date_field: - current_date = move.date_expected - new_date = fields.Datetime.from_string(vals.get(propagated_date_field)) - delta_days = (new_date - current_date).total_seconds() / 86400 - if abs(delta_days) >= move.company_id.propagation_minimum_delta: - old_move_date = move.move_dest_ids[0].date_expected - new_move_date = (old_move_date + relativedelta.relativedelta(days=delta_days or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) - propagated_changes_dict['date_expected'] = new_move_date - #For pushed moves as well as for pulled moves, propagate by recursive call of write(). - #Note that, for pulled moves we intentionally don't propagate on the procurement. - if propagated_changes_dict: - move.move_dest_ids.filtered(lambda m: m.state not in ('done', 'cancel')).write(propagated_changes_dict) + 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) 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]) @@ -1217,7 +1212,7 @@ class StockMove(models.Model): # compatible with the move's UOM. new_product_qty = self.product_id.uom_id._compute_quantity(self.product_qty - qty, self.product_uom, round=False) new_product_qty = float_round(new_product_qty, precision_digits=self.env['decimal.precision'].precision_get('Product Unit of Measure')) - self.with_context(do_not_propagate=True, do_not_unreserve=True, rounding_method='HALF-UP').write({'product_uom_qty': new_product_qty}) + self.with_context(do_not_unreserve=True, rounding_method='HALF-UP').write({'product_uom_qty': new_product_qty}) new_move = new_move._action_confirm(merge=False) return new_move.id diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py index 11bfa91a442..275d955f841 100644 --- a/addons/stock/models/stock_rule.py +++ b/addons/stock/models/stock_rule.py @@ -70,6 +70,10 @@ class StockRule(models.Model): help="The 'Manual Operation' value will create a stock move after the current one. " "With 'Automatic No Step Added', the location is replaced in the original move.") rule_message = fields.Html(compute='_compute_action_message') + propagate_date = fields.Boolean(string="Propagate Rescheduling", default=True, + 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) @api.onchange('picking_type_id') def _onchange_picking_type(self): @@ -262,6 +266,8 @@ class StockRule(models.Model): 'date': date_expected, 'date_expected': date_expected, 'propagate': self.propagate, + 'propagate_date': self.propagate_date, + 'propagate_date_minimum_delta': self.propagate_date_minimum_delta, 'description_picking': product_id._get_description(self.picking_type_id), 'priority': values.get('priority', "1"), } diff --git a/addons/stock/tests/test_proc_rule.py b/addons/stock/tests/test_proc_rule.py index 1679d40637f..27c1d9fef39 100644 --- a/addons/stock/tests/test_proc_rule.py +++ b/addons/stock/tests/test_proc_rule.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import datetime, timedelta + from odoo.tests.common import TransactionCase from odoo.tools import mute_logger @@ -64,3 +66,93 @@ class TestProcRule(TransactionCase): ('move_dest_ids', 'in', [pick_output.move_lines[0].id]) ]) self.assertEqual(len(moves.ids), 1, "It should have created a picking from Stock to Output with the original picking as destination") + + def test_rule_propagate_1(self): + move_dest = self.env['stock.move'].create({ + 'name': 'move_dest', + 'product_id': self.ref('product.product_product_3'), + 'product_uom': self.ref('uom.product_uom_unit'), + 'location_id': self.ref('stock.stock_location_output'), + 'location_dest_id': self.ref('stock.stock_location_customers'), + }) + + move_orig = self.env['stock.move'].create({ + 'name': 'move_orig', + 'product_id': self.ref('product.product_product_3'), + 'product_uom': self.ref('uom.product_uom_unit'), + 'move_dest_ids': [(4, move_dest.id)], + 'propagate_date': True, + 'propagate_date_minimum_delta': 5, + 'location_id': self.ref('stock.stock_location_stock'), + 'location_dest_id': self.ref('stock.stock_location_output'), + }) + + move_dest_initial_date = move_dest.date_expected + + # change above the minimum delta + move_orig.date_expected += timedelta(days=6) + self.assertEquals(move_dest.date_expected, move_dest_initial_date + timedelta(days=6), 'date should be propagated as the minimum delta is below') + + # change below the minimum delta + move_dest_initial_date = move_dest.date_expected + move_orig.date_expected += timedelta(days=4) + + self.assertAlmostEquals(move_dest.date_expected, move_dest_initial_date, delta=timedelta(seconds=1), msg='date should not be propagated as the minimum delta is above') + + def test_rule_propagate_2(self): + move_dest = self.env['stock.move'].create({ + 'name': 'move_dest', + 'product_id': self.ref('product.product_product_3'), + 'product_uom': self.ref('uom.product_uom_unit'), + 'location_id': self.ref('stock.stock_location_output'), + 'location_dest_id': self.ref('stock.stock_location_customers'), + }) + + move_orig = self.env['stock.move'].create({ + 'name': 'move_orig', + 'product_id': self.ref('product.product_product_3'), + 'product_uom': self.ref('uom.product_uom_unit'), + 'move_dest_ids': [(4, move_dest.id)], + 'propagate_date': False, + 'propagate_date_minimum_delta': 5, + 'location_id': self.ref('stock.stock_location_stock'), + 'location_dest_id': self.ref('stock.stock_location_output'), + }) + + move_dest_initial_date = move_dest.date_expected + + # change below the minimum delta + move_orig.date_expected += timedelta(days=4) + self.assertEquals(move_dest.date_expected, move_dest_initial_date, 'date should not be propagated') + + # change above the minimum delta + move_orig.date_expected += timedelta(days=2) + self.assertEquals(move_dest.date_expected, move_dest_initial_date, 'date should not be propagated') + + def test_rule_propagate_3(self): + move_dest = self.env['stock.move'].create({ + 'name': 'move_dest', + 'product_id': self.ref('product.product_product_3'), + 'product_uom': self.ref('uom.product_uom_unit'), + 'location_id': self.ref('stock.stock_location_output'), + 'location_dest_id': self.ref('stock.stock_location_customers'), + }) + + move_orig = self.env['stock.move'].create({ + 'name': 'move_orig', + 'product_id': self.ref('product.product_product_3'), + 'product_uom': self.ref('uom.product_uom_unit'), + 'move_dest_ids': [(4, move_dest.id)], + 'propagate_date': True, + 'propagate_date_minimum_delta': 5, + 'location_id': self.ref('stock.stock_location_stock'), + 'location_dest_id': self.ref('stock.stock_location_output'), + 'quantity_done': 10, + }) + move_orig.date_expected -= timedelta(days=6) + move_dest_initial_date = move_dest.date_expected + move_orig_initial_date = move_orig.date_expected + move_orig._action_done() + self.assertEquals(move_orig.date_expected, move_orig_initial_date, 'schedule date should not be impacted by action_done') + self.assertAlmostEquals(move_orig.date, datetime.now(), delta=timedelta(seconds=1), msg='date should be now') + self.assertEquals(move_dest.date_expected, move_dest_initial_date + timedelta(days=6), 'date should be propagated') diff --git a/addons/stock/views/res_config_settings_views.xml b/addons/stock/views/res_config_settings_views.xml index 9d05e42c36e..49a64f6990a 100644 --- a/addons/stock/views/res_config_settings_views.xml +++ b/addons/stock/views/res_config_settings_views.xml @@ -302,22 +302,6 @@
-
-
- -
-
-
-
diff --git a/addons/stock/views/stock_rule_views.xml b/addons/stock/views/stock_rule_views.xml index cd00020092c..f326dae5403 100644 --- a/addons/stock/views/stock_rule_views.xml +++ b/addons/stock/views/stock_rule_views.xml @@ -103,6 +103,13 @@ + + +