[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
This commit is contained in:
committed by
William Henrotin
parent
1d6bc7286c
commit
c514e8ae6a
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -302,22 +302,6 @@
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div id="sale_security_lead"/>
|
||||
<div id="purchase_po_lead"/>
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="propagation_info" title="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. This option allows to not propagate the rescheduling if the change is not critical.">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_propagation_minimum_delta"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_propagation_minimum_delta"/>
|
||||
<div class="text-muted">
|
||||
Don’t propagate scheduling changes through chains of operations
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<div class="mt16" attrs="{'invisible': [('use_propagation_minimum_delta', '=', False)]}">
|
||||
<span>Change must be higher than <field name="propagation_minimum_delta" class="oe_inline"/> days to be propagated</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
@@ -103,6 +103,13 @@
|
||||
<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">
|
||||
<field name="propagate_date"/>
|
||||
<label for="propagate_date_minimum_delta" attrs="{'invisible': [('propagate_date', '=', False)]}"/>
|
||||
<div attrs="{'invisible': [('propagate_date', '=', False)]}">
|
||||
<field name="propagate_date_minimum_delta" class="oe_inline"/> day(s)
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user