diff --git a/setup/stock_move_source_relocate_dynamic_routing/odoo/addons/stock_move_source_relocate_dynamic_routing b/setup/stock_move_source_relocate_dynamic_routing/odoo/addons/stock_move_source_relocate_dynamic_routing new file mode 120000 index 0000000000..67882d9f30 --- /dev/null +++ b/setup/stock_move_source_relocate_dynamic_routing/odoo/addons/stock_move_source_relocate_dynamic_routing @@ -0,0 +1 @@ +../../../../stock_move_source_relocate_dynamic_routing \ No newline at end of file diff --git a/setup/stock_move_source_relocate_dynamic_routing/setup.py b/setup/stock_move_source_relocate_dynamic_routing/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/stock_move_source_relocate_dynamic_routing/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_move_source_relocate/models/stock_move.py b/stock_move_source_relocate/models/stock_move.py index fad7d6dde0..aaa965ddb5 100644 --- a/stock_move_source_relocate/models/stock_move.py +++ b/stock_move_source_relocate/models/stock_move.py @@ -30,31 +30,40 @@ def _apply_source_relocate(self): relocation = self.env["stock.source.relocate"]._rule_for_move(move) if not relocation or relocation.relocate_location_id == move.location_id: continue + move._apply_source_relocate_rule( + relocation, reserved_availability, roundings + ) - rounding = roundings[move] - if not reserved_availability[move]: - # nothing could be reserved, however, we want to source the - # move on the specific relocation (for replenishment), so - # update it's source location - move.location_id = relocation.relocate_location_id - else: - missing_reserved_uom_quantity = ( - move.product_uom_qty - reserved_availability[move] - ) - need = move.product_uom._compute_quantity( - missing_reserved_uom_quantity, - move.product_id.uom_id, - rounding_method="HALF-UP", - ) - - if float_is_zero(need, precision_rounding=rounding): - continue - - # A part of the quantity could be reserved in the original - # location, so keep this part in the move and split the rest - # in a new move, where will take the goods in the relocation - new_move_id = self._split(need) - # recheck first move which should now be available - new_move = self.browse(new_move_id) - new_move.location_id = relocation.relocate_location_id - move._action_assign() + def _apply_source_relocate_rule(self, relocation, reserved_availability, roundings): + relocated = self.env["stock.move"].browse() + + rounding = roundings[self] + if not reserved_availability[self]: + # nothing could be reserved, however, we want to source the + # move on the specific relocation (for replenishment), so + # update it's source location + self.location_id = relocation.relocate_location_id + relocated = self + else: + missing_reserved_uom_quantity = ( + self.product_uom_qty - reserved_availability[self] + ) + need = self.product_uom._compute_quantity( + missing_reserved_uom_quantity, + self.product_id.uom_id, + rounding_method="HALF-UP", + ) + + if float_is_zero(need, precision_rounding=rounding): + return relocated + + # A part of the quantity could be reserved in the original + # location, so keep this part in the move and split the rest + # in a new move, where will take the goods in the relocation + new_move_id = self._split(need) + # recheck first move which should now be available + new_move = self.browse(new_move_id) + new_move.location_id = relocation.relocate_location_id + self._action_assign() + relocated = new_move + return relocated diff --git a/stock_move_source_relocate/tests/test_source_relocate.py b/stock_move_source_relocate/tests/test_source_relocate.py index be203915ed..5696605611 100644 --- a/stock_move_source_relocate/tests/test_source_relocate.py +++ b/stock_move_source_relocate/tests/test_source_relocate.py @@ -4,7 +4,7 @@ from odoo.tests import common -class TestSourceRelocate(common.SavepointCase): +class SourceRelocateCommon(common.SavepointCase): @classmethod def setUpClass(cls): super().setUpClass() @@ -53,9 +53,21 @@ def _create_single_move(self, product, picking_type): } return self.env["stock.move"].create(move_vals) + def _create_relocate_rule(self, location, relocation, picking_type, domain=None): + self.env["stock.source.relocate"].create( + { + "location_id": location.id, + "picking_type_id": picking_type.id, + "relocate_location_id": relocation.id, + "rule_domain": domain or "[]", + } + ) + def _update_qty_in_location(self, location, product, quantity): self.env["stock.quant"]._update_available_quantity(product, location, quantity) + +class TestSourceRelocate(SourceRelocateCommon): def test_relocate_child_of_location(self): # relocate location is a child, valid self.env["stock.source.relocate"].create( @@ -77,16 +89,6 @@ def test_relocate_not_child_of_location(self): } ) - def _create_relocate_rule(self, location, relocation, picking_type, domain=None): - self.env["stock.source.relocate"].create( - { - "location_id": location.id, - "picking_type_id": picking_type.id, - "relocate_location_id": relocation.id, - "rule_domain": domain or "[]", - } - ) - def test_relocate_whole_move(self): self._create_relocate_rule( self.wh.lot_stock_id, self.loc_replenish, self.wh.pick_type_id diff --git a/stock_move_source_relocate_dynamic_routing/__init__.py b/stock_move_source_relocate_dynamic_routing/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/stock_move_source_relocate_dynamic_routing/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_move_source_relocate_dynamic_routing/__manifest__.py b/stock_move_source_relocate_dynamic_routing/__manifest__.py new file mode 100644 index 0000000000..84ee965af4 --- /dev/null +++ b/stock_move_source_relocate_dynamic_routing/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2019 Camptocamp (https://www.camptocamp.com) +{ + "name": "Stock Source Relocate - Dynamic Routing", + "summary": "Glue module", + "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-warehouse", + "category": "Warehouse Management", + "version": "13.0.1.0.0", + "license": "AGPL-3", + "depends": ["stock_dynamic_routing", "stock_move_source_relocate"], + "demo": [], + "data": ["views/stock_routing_views.xml", "views/stock_source_relocate_views.xml"], + "auto_install": True, + "installable": True, + "development_status": "Alpha", +} diff --git a/stock_move_source_relocate_dynamic_routing/models/__init__.py b/stock_move_source_relocate_dynamic_routing/models/__init__.py new file mode 100644 index 0000000000..24fbae0f04 --- /dev/null +++ b/stock_move_source_relocate_dynamic_routing/models/__init__.py @@ -0,0 +1,3 @@ +from . import stock_move +from . import stock_source_relocate +from . import stock_routing diff --git a/stock_move_source_relocate_dynamic_routing/models/stock_move.py b/stock_move_source_relocate_dynamic_routing/models/stock_move.py new file mode 100644 index 0000000000..3c94a9f587 --- /dev/null +++ b/stock_move_source_relocate_dynamic_routing/models/stock_move.py @@ -0,0 +1,19 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _apply_source_relocate_rule(self, relocation, reserved_availability, roundings): + relocated = super( + StockMove, + # disable application of routing in write() method of + # stock_dynamic_routing, we'll apply it here whatever the state of + # the move is + self.with_context(__applying_routing_rule=True), + )._apply_source_relocate_rule(relocation, reserved_availability, roundings) + relocated._chain_apply_routing() + return relocated diff --git a/stock_move_source_relocate_dynamic_routing/models/stock_routing.py b/stock_move_source_relocate_dynamic_routing/models/stock_routing.py new file mode 100644 index 0000000000..a9a1313b7a --- /dev/null +++ b/stock_move_source_relocate_dynamic_routing/models/stock_routing.py @@ -0,0 +1,28 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, models + + +class StockRouting(models.Model): + + _inherit = "stock.routing" + + def action_view_source_relocate(self): + picking_types = self.mapped("picking_type_id") + routing = self.env["stock.routing"].search( + [("picking_type_id", "in", picking_types.ids)] + ) + context = self.env.context + if len(picking_types) == 1: + context = dict(context, default_picking_type_id=picking_types.id) + return { + "name": _("Source Relocation"), + "domain": [("id", "in", routing.ids)], + "res_model": "stock.source.relocate", + "type": "ir.actions.act_window", + "view_id": False, + "view_mode": "tree,form", + "limit": 20, + "context": context, + } diff --git a/stock_move_source_relocate_dynamic_routing/models/stock_source_relocate.py b/stock_move_source_relocate_dynamic_routing/models/stock_source_relocate.py new file mode 100644 index 0000000000..4e5a9d3e0c --- /dev/null +++ b/stock_move_source_relocate_dynamic_routing/models/stock_source_relocate.py @@ -0,0 +1,28 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, models + + +class StockSourceRelocate(models.Model): + + _inherit = "stock.source.relocate" + + def action_view_dynamic_routing(self): + picking_types = self.mapped("picking_type_id") + routing = self.env["stock.routing"].search( + [("picking_type_id", "in", picking_types.ids)] + ) + context = self.env.context + if len(picking_types) == 1: + context = dict(context, default_picking_type_id=picking_types.id) + return { + "name": _("Dynamic Routing"), + "domain": [("id", "in", routing.ids)], + "res_model": "stock.routing", + "type": "ir.actions.act_window", + "view_id": False, + "view_mode": "tree,form", + "limit": 20, + "context": context, + } diff --git a/stock_move_source_relocate_dynamic_routing/readme/CONTRIBUTORS.rst b/stock_move_source_relocate_dynamic_routing/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..48286263cd --- /dev/null +++ b/stock_move_source_relocate_dynamic_routing/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Guewen Baconnier diff --git a/stock_move_source_relocate_dynamic_routing/readme/DESCRIPTION.rst b/stock_move_source_relocate_dynamic_routing/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..46f8a8f457 --- /dev/null +++ b/stock_move_source_relocate_dynamic_routing/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +Glue module between ``stock_move_source_relocate`` and +``stock_dynamic_routing``. diff --git a/stock_move_source_relocate_dynamic_routing/tests/__init__.py b/stock_move_source_relocate_dynamic_routing/tests/__init__.py new file mode 100644 index 0000000000..f46756f0a3 --- /dev/null +++ b/stock_move_source_relocate_dynamic_routing/tests/__init__.py @@ -0,0 +1 @@ +from . import test_dynamic_relocate diff --git a/stock_move_source_relocate_dynamic_routing/tests/test_dynamic_relocate.py b/stock_move_source_relocate_dynamic_routing/tests/test_dynamic_relocate.py new file mode 100644 index 0000000000..2338048ab2 --- /dev/null +++ b/stock_move_source_relocate_dynamic_routing/tests/test_dynamic_relocate.py @@ -0,0 +1,57 @@ +from odoo.addons.stock_move_source_relocate.tests.test_source_relocate import ( + SourceRelocateCommon, +) + + +class TestSourceRelocate(SourceRelocateCommon): + def test_relocate_with_routing(self): + """Check that routing is applied when a relocation happen""" + # Relocation: for unavailable move in Stock, relocate to Replenish + self._create_relocate_rule( + self.wh.lot_stock_id, self.loc_replenish, self.wh.pick_type_id + ) + # Routing: a move with source location in replenishment is classified + # in picking type Replenish + pick_type_replenish = self.env["stock.picking.type"].create( + { + "name": "Replenish", + "code": "internal", + "sequence_code": "R", + "warehouse_id": self.wh.id, + "use_create_lots": False, + "use_existing_lots": True, + "default_location_src_id": self.loc_replenish.id, + "default_location_dest_id": self.wh.lot_stock_id.id, + } + ) + self.env["stock.routing"].create( + { + "location_id": self.loc_replenish.id, + "picking_type_id": self.wh.pick_type_id.id, + "rule_ids": [ + ( + 0, + 0, + {"method": "pull", "picking_type_id": pick_type_replenish.id}, + ) + ], + } + ) + move = self._create_single_move(self.product, self.wh.pick_type_id) + move._assign_picking() + move._action_assign() + self.assertRecordValues( + move, + [ + { + "state": "confirmed", + "product_qty": 10.0, + "reserved_availability": 0.0, + # routing changed the picking type + "picking_type_id": pick_type_replenish.id, + "location_id": self.loc_replenish.id, + } + ], + ) + # routing created a new move + self.assertTrue(move.move_dest_ids) diff --git a/stock_move_source_relocate_dynamic_routing/views/stock_routing_views.xml b/stock_move_source_relocate_dynamic_routing/views/stock_routing_views.xml new file mode 100644 index 0000000000..72f4880fc3 --- /dev/null +++ b/stock_move_source_relocate_dynamic_routing/views/stock_routing_views.xml @@ -0,0 +1,19 @@ + + + + stock.routing.form + stock.routing + + +
+
+
+
+
diff --git a/stock_move_source_relocate_dynamic_routing/views/stock_source_relocate_views.xml b/stock_move_source_relocate_dynamic_routing/views/stock_source_relocate_views.xml new file mode 100644 index 0000000000..3b316ea0c7 --- /dev/null +++ b/stock_move_source_relocate_dynamic_routing/views/stock_source_relocate_views.xml @@ -0,0 +1,22 @@ + + + + stock.source.relocate.form + stock.source.relocate + + +
+
+
+
+