From 1ca7aa26d56dfd12dd66366ed111255254459908 Mon Sep 17 00:00:00 2001 From: geomer198 Date: Thu, 9 Mar 2023 23:22:14 +0300 Subject: [PATCH] [ADD] stock_picking_product_interchangeable: Added module. --- .../stock_picking_product_interchangeable | 1 + .../setup.py | 6 + .../README.rst | 0 .../__init__.py | 1 + .../__manifest__.py | 21 ++ .../demo/product_demo.xml | 55 ++++ .../demo/stock_demo.xml | 50 ++++ .../models/__init__.py | 4 + .../models/product.py | 66 +++++ .../models/stock_move.py | 108 ++++++++ .../models/stock_picking.py | 16 ++ .../models/stock_picking_type.py | 11 + .../readme/CONFIGURATION.rst | 11 + .../readme/DESCRIPTION.rst | 5 + .../readme/USAGE.rst | 3 + .../tests/__init__.py | 2 + .../tests/common.py | 31 +++ .../tests/test_product.py | 44 ++++ .../tests/test_stock_picking.py | 238 ++++++++++++++++++ .../views/product_views.xml | 34 +++ .../views/stock_picking_type_views.xml | 17 ++ .../views/stock_picking_views.xml | 19 ++ 22 files changed, 743 insertions(+) create mode 120000 setup/stock_picking_product_interchangeable/odoo/addons/stock_picking_product_interchangeable create mode 100644 setup/stock_picking_product_interchangeable/setup.py create mode 100644 stock_picking_product_interchangeable/README.rst create mode 100644 stock_picking_product_interchangeable/__init__.py create mode 100644 stock_picking_product_interchangeable/__manifest__.py create mode 100644 stock_picking_product_interchangeable/demo/product_demo.xml create mode 100644 stock_picking_product_interchangeable/demo/stock_demo.xml create mode 100644 stock_picking_product_interchangeable/models/__init__.py create mode 100644 stock_picking_product_interchangeable/models/product.py create mode 100644 stock_picking_product_interchangeable/models/stock_move.py create mode 100644 stock_picking_product_interchangeable/models/stock_picking.py create mode 100644 stock_picking_product_interchangeable/models/stock_picking_type.py create mode 100644 stock_picking_product_interchangeable/readme/CONFIGURATION.rst create mode 100644 stock_picking_product_interchangeable/readme/DESCRIPTION.rst create mode 100644 stock_picking_product_interchangeable/readme/USAGE.rst create mode 100644 stock_picking_product_interchangeable/tests/__init__.py create mode 100644 stock_picking_product_interchangeable/tests/common.py create mode 100644 stock_picking_product_interchangeable/tests/test_product.py create mode 100644 stock_picking_product_interchangeable/tests/test_stock_picking.py create mode 100644 stock_picking_product_interchangeable/views/product_views.xml create mode 100644 stock_picking_product_interchangeable/views/stock_picking_type_views.xml create mode 100644 stock_picking_product_interchangeable/views/stock_picking_views.xml diff --git a/setup/stock_picking_product_interchangeable/odoo/addons/stock_picking_product_interchangeable b/setup/stock_picking_product_interchangeable/odoo/addons/stock_picking_product_interchangeable new file mode 120000 index 000000000..5b9eb48c9 --- /dev/null +++ b/setup/stock_picking_product_interchangeable/odoo/addons/stock_picking_product_interchangeable @@ -0,0 +1 @@ +../../../../stock_picking_product_interchangeable \ No newline at end of file diff --git a/setup/stock_picking_product_interchangeable/setup.py b/setup/stock_picking_product_interchangeable/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/stock_picking_product_interchangeable/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_picking_product_interchangeable/README.rst b/stock_picking_product_interchangeable/README.rst new file mode 100644 index 000000000..e69de29bb diff --git a/stock_picking_product_interchangeable/__init__.py b/stock_picking_product_interchangeable/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/stock_picking_product_interchangeable/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_picking_product_interchangeable/__manifest__.py b/stock_picking_product_interchangeable/__manifest__.py new file mode 100644 index 000000000..a000911d7 --- /dev/null +++ b/stock_picking_product_interchangeable/__manifest__.py @@ -0,0 +1,21 @@ +{ + "name": "Stock Picking Product Interchangeable", + "summary": """Stock Picking Product Interchangeable""", + "author": "Cetmix, Odoo Community Association (OCA)", + "version": "16.0.1.0.0", + "category": "Inventory/Inventory", + "website": "https://github.com/OCA/stock-logistics-warehouse", + "maintainers": ["geomer198", "CetmixGitDrone"], + "depends": ["stock_available"], + "data": [ + "views/product_views.xml", + "views/stock_picking_type_views.xml", + "views/stock_picking_views.xml", + ], + "demo": [ + "demo/product_demo.xml", + "demo/stock_demo.xml", + ], + "license": "AGPL-3", + "installable": True, +} diff --git a/stock_picking_product_interchangeable/demo/product_demo.xml b/stock_picking_product_interchangeable/demo/product_demo.xml new file mode 100644 index 000000000..53e612c54 --- /dev/null +++ b/stock_picking_product_interchangeable/demo/product_demo.xml @@ -0,0 +1,55 @@ + + + + + Bob + bob@example.com + + + + Plate + product + + + + Napkin + product + + + + + + + + + + Chopsticks + product + + + + Fork + product + + + + Spoon + product + + + + Knife + product + + + + diff --git a/stock_picking_product_interchangeable/demo/stock_demo.xml b/stock_picking_product_interchangeable/demo/stock_demo.xml new file mode 100644 index 000000000..bc45467e4 --- /dev/null +++ b/stock_picking_product_interchangeable/demo/stock_demo.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + 20.0 + + + + + + 8.0 + + + + + + 3.0 + + + + + + + + diff --git a/stock_picking_product_interchangeable/models/__init__.py b/stock_picking_product_interchangeable/models/__init__.py new file mode 100644 index 000000000..9bd77247d --- /dev/null +++ b/stock_picking_product_interchangeable/models/__init__.py @@ -0,0 +1,4 @@ +from . import product +from . import stock_move +from . import stock_picking +from . import stock_picking_type diff --git a/stock_picking_product_interchangeable/models/product.py b/stock_picking_product_interchangeable/models/product.py new file mode 100644 index 000000000..59f3b19b8 --- /dev/null +++ b/stock_picking_product_interchangeable/models/product.py @@ -0,0 +1,66 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + product_tmpl_interchangeable_ids = fields.Many2many( + comodel_name="product.product", + compute="_compute_product_tmpl_interchangeable_ids", + inverse="_inverse_product_tmpl_interchangeable_ids", + ) + + def _compute_product_tmpl_interchangeable_ids(self): + """Compute interchangeable products""" + for rec in self: + rec.product_tmpl_interchangeable_ids = ( + rec.product_variant_ids.product_interchangeable_ids + ) + + def _inverse_product_tmpl_interchangeable_ids(self): + """Set new interchangeable product""" + for rec in self: + rec.product_variant_id.product_replaces_ids = ( + rec.product_tmpl_interchangeable_ids + ) + + +class Product(models.Model): + _inherit = "product.product" + + product_interchangeable_ids = fields.Many2many( + comodel_name="product.product", + string="Replaces", + help="Products that can be substituted by current product", + inverse="_inverse_product_interchangeable_ids", + compute="_compute_product_interchangeable_ids", + ) + + product_replaces_ids = fields.Many2many( + comodel_name="product.product", + string="Replaces", + relation="product_substitute_rel", + column1="product_id", + column2="product_replaced_id", + help="Products that can be substituted by current product", + ) + product_replaced_by_ids = fields.Many2many( + comodel_name="product.product", + string="Replaces", + relation="product_substitute_rel", + column1="product_replaced_id", + column2="product_id", + help="Products that can substitute current current product", + ) + + def _compute_product_interchangeable_ids(self): + """Compute interchangeable products""" + for rec in self: + rec.product_interchangeable_ids = ( + rec.product_replaces_ids | rec.product_replaced_by_ids + ) - rec + + def _inverse_product_interchangeable_ids(self): + """Set new interchangeable product""" + for rec in self: + rec.product_replaces_ids = rec.product_interchangeable_ids diff --git a/stock_picking_product_interchangeable/models/stock_move.py b/stock_picking_product_interchangeable/models/stock_move.py new file mode 100644 index 000000000..c1a8de27d --- /dev/null +++ b/stock_picking_product_interchangeable/models/stock_move.py @@ -0,0 +1,108 @@ +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _prepare_interchangeable_products(self, mode, for_qty): + """ + Preparing products for current product replacement + :param mode: stock.picking.type substitute_products_mode value + :param for_qty: product qty for replacement + :return: list of tuple [(product_obj, count), ...] + """ + qty = for_qty + if qty >= 0: + return False + products_info = [] + for product in self.product_id.product_interchangeable_ids: + available_qty = product.immediately_usable_qty + if available_qty > 0 > qty: + product_qty = abs(qty) if available_qty + qty >= 0 else available_qty + products_info.append((product, product_qty)) + qty += product_qty + if (mode == "all" and qty == 0) or mode == "any": + return products_info + return False + + def _create_stock_move_interchangeable_products(self, products_info): + """ + Creates stock.move records for replacement product + :param products_info: struct list of tuple [(product_obj, count), ...] + :return: Stock Move object + """ + stock_move_obj = self.env["stock.move"] + if not products_info: + return stock_move_obj + return stock_move_obj.create( + [ + { + "picking_id": self.picking_id.id, + "name": product.display_name, + "product_id": product.id, + "product_uom_qty": qty, + "location_id": self.location_id.id, + "location_dest_id": self.location_dest_id.id, + "company_id": self.company_id.id, + } + for product, qty in products_info + ] + ) + + def _interchangeable_stock_move_filter(self): + """ + Filter for applying interchangeable behavior for stock.move + :return: True/False + """ + type_ = self.picking_type_id + mode = type_.substitute_products_mode + skip_behavior = not (mode and type_.code == "outgoing") + return not (skip_behavior or self.picking_id.pass_interchangeable) + + def _add_note_interchangeable_picking_note(self, products_info, qty): + """ + Add note for product with interchangeable products + :param list products_info: struct list of tuple [(product_obj, count), ...] + """ + self.ensure_one() + product = self.product_id + qty = abs(qty) + note = rf"{product.display_name} missing qty {qty} was replaced with:" # noqa + lines = [ + f"
  • {product.display_name} {qty}
  • " + for product, qty in products_info + ] + note += f"
    " + picking = self.picking_id + if not picking.note: + picking.note = note + else: + picking.note += note + + def _action_confirm(self, merge=True, merge_into=False): + moves = super(StockMove, self)._action_confirm( + merge=merge, merge_into=merge_into + ) + inter_moves = moves.filtered( + lambda move: move._interchangeable_stock_move_filter() + ) + if not inter_moves: + return moves + other_moves = moves - inter_moves + move_ids = inter_moves.filtered( + lambda m: m.product_id.product_interchangeable_ids + ) + new_moves = self.env["stock.move"] + for move in move_ids: + mode = move.picking_type_id.substitute_products_mode + qty = move.product_id.immediately_usable_qty + products_info = move._prepare_interchangeable_products(mode, qty) + if products_info: + products_qty = sum(map(lambda item: item[1], products_info)) + new_moves = move._create_stock_move_interchangeable_products( + products_info + ) + new_moves |= new_moves._action_confirm(merge, merge_into) + move.product_uom_qty -= products_qty + move._add_note_interchangeable_picking_note(products_info, qty) + return inter_moves | new_moves | other_moves diff --git a/stock_picking_product_interchangeable/models/stock_picking.py b/stock_picking_product_interchangeable/models/stock_picking.py new file mode 100644 index 000000000..537f841ce --- /dev/null +++ b/stock_picking_product_interchangeable/models/stock_picking.py @@ -0,0 +1,16 @@ +from odoo import api, fields, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + pass_interchangeable = fields.Boolean() + available_pass_interchangeable = fields.Boolean() + + @api.onchange("picking_type_id") + def _onchange_available_pass_interchangeable(self): + """Compute available to showing pass_interchangeable field""" + type_ = self.picking_type_id + self.available_pass_interchangeable = ( + type_.substitute_products_mode and type_.code == "outgoing" + ) diff --git a/stock_picking_product_interchangeable/models/stock_picking_type.py b/stock_picking_product_interchangeable/models/stock_picking_type.py new file mode 100644 index 000000000..1d289c842 --- /dev/null +++ b/stock_picking_product_interchangeable/models/stock_picking_type.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class StockPickingType(models.Model): + _inherit = "stock.picking.type" + + substitute_products_mode = fields.Selection( + selection=[("all", "If all available"), ("any", "If any available")], + string="Substitute Products", + required=False, + ) diff --git a/stock_picking_product_interchangeable/readme/CONFIGURATION.rst b/stock_picking_product_interchangeable/readme/CONFIGURATION.rst new file mode 100644 index 000000000..43c600e2c --- /dev/null +++ b/stock_picking_product_interchangeable/readme/CONFIGURATION.rst @@ -0,0 +1,11 @@ +Open **Inventory** -> **Configuration** -> **Operations types** and select an Operation Type you would like to use interchangeable products in. +Only Operation Types of type **Delivery** can use this feature. + +Select **Substitute Products** mode: + +- "If all available". Interchangeable products will be added products will be added only if it is possible to reserve all requested amount. +- "If any available". Interchangeable products will be added if there is at least a single one available. + +In Product form open **Interchangeable** tab and add products that can be used as a substitute. + +**Important**: if you are using product variants Interchangeable products must be defined for each variant separately. diff --git a/stock_picking_product_interchangeable/readme/DESCRIPTION.rst b/stock_picking_product_interchangeable/readme/DESCRIPTION.rst new file mode 100644 index 000000000..405080adf --- /dev/null +++ b/stock_picking_product_interchangeable/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +This module implements "interchangeable" products. If an interchangeable product is not available in stock in requested quantity its substitute products will be added to picking to complete the order. + +Eg there is an "English Breakfast" tea and "Breakfast in England" tea which are absolutely the same inside and have the same price. Only package labels are different. And our customers don't care which of them will be actually delivered. + +NB: Interchangeable products substitute each other. Eg if "English Breakfast" can substitutes "Breakfast in England" then "Breakfast in England" can substitute "English Breakfast". diff --git a/stock_picking_product_interchangeable/readme/USAGE.rst b/stock_picking_product_interchangeable/readme/USAGE.rst new file mode 100644 index 000000000..46608bcb6 --- /dev/null +++ b/stock_picking_product_interchangeable/readme/USAGE.rst @@ -0,0 +1,3 @@ +Click **Mark todo** or "Check availability" button in Stock Picking form. + +If **Substitute Products** option is activated for picking operation type and there is not enough stock of an interchangeable product substitute products will be added to picking. diff --git a/stock_picking_product_interchangeable/tests/__init__.py b/stock_picking_product_interchangeable/tests/__init__.py new file mode 100644 index 000000000..9158adedf --- /dev/null +++ b/stock_picking_product_interchangeable/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_product +from . import test_stock_picking diff --git a/stock_picking_product_interchangeable/tests/common.py b/stock_picking_product_interchangeable/tests/common.py new file mode 100644 index 000000000..d7b2be18d --- /dev/null +++ b/stock_picking_product_interchangeable/tests/common.py @@ -0,0 +1,31 @@ +from odoo.tests import TransactionCase + + +class StockPickingProductInterchangeableCommon(TransactionCase): + @classmethod + def setUpClass(cls): + super(StockPickingProductInterchangeableCommon, cls).setUpClass() + cls.product_fork = cls.env.ref( + "stock_picking_product_interchangeable.product_product_fork" + ) + cls.product_spoon = cls.env.ref( + "stock_picking_product_interchangeable.product_product_spoon" + ) + cls.product_knife = cls.env.ref( + "stock_picking_product_interchangeable.product_product_knife" + ) + cls.product_chopsticks = cls.env.ref( + "stock_picking_product_interchangeable.product_product_chopsticks" + ) + cls.product_tmpl_napkin = cls.env.ref( + "stock_picking_product_interchangeable.product_template_napkin" + ) + cls.product_tmpl_plate = cls.env.ref( + "stock_picking_product_interchangeable.product_template_plate" + ) + cls.product_napkin = cls.product_tmpl_napkin.product_variant_id + cls.picking_type_in = cls.env.ref("stock.picking_type_in") + cls.picking_type_out = cls.env.ref("stock.picking_type_out") + cls.res_partner_bob = cls.env.ref( + "stock_picking_product_interchangeable.res_partner_bob" + ) diff --git a/stock_picking_product_interchangeable/tests/test_product.py b/stock_picking_product_interchangeable/tests/test_product.py new file mode 100644 index 000000000..14e8ca8e6 --- /dev/null +++ b/stock_picking_product_interchangeable/tests/test_product.py @@ -0,0 +1,44 @@ +from odoo.tests import Form + +from .common import StockPickingProductInterchangeableCommon + + +class TestProduct(StockPickingProductInterchangeableCommon): + def test_invisible_interchangeable_products(self): + """ + Test flow to hiding interchangeable table + for product templates with 2 and more product variants + """ + self.assertEqual( + len(self.product_tmpl_napkin.product_variant_ids), + 2, + msg="Product variants count must be equal to 2", + ) + with self.assertRaises(AssertionError): + with Form( + self.product_tmpl_napkin, + view="stock_picking_product_interchangeable.product_template_interchangeable_form_view", # noqa + ) as form: + form.product_tmpl_interchangeable_ids.add(self.product_tmpl_plate) + + def test_visible_interchangeable_products(self): + """ + Test flow to visible interchangeable table + for product templates with 1 product variant + """ + self.assertEqual(len(self.product_tmpl_plate.product_variant_ids), 1) + with Form( + self.product_tmpl_plate, + view="stock_picking_product_interchangeable.product_template_interchangeable_form_view", # noqa + ) as form: + form.product_tmpl_interchangeable_ids.add(self.product_chopsticks) + self.assertEqual( + len(self.product_tmpl_plate.product_tmpl_interchangeable_ids), + 1, + msg="Interchangeable products count must be equal to 1", + ) + self.assertEqual( + self.product_tmpl_plate.product_tmpl_interchangeable_ids, + self.product_chopsticks, + msg="Products must be the same", + ) diff --git a/stock_picking_product_interchangeable/tests/test_stock_picking.py b/stock_picking_product_interchangeable/tests/test_stock_picking.py new file mode 100644 index 000000000..2bad2145c --- /dev/null +++ b/stock_picking_product_interchangeable/tests/test_stock_picking.py @@ -0,0 +1,238 @@ +from odoo.tests import Form + +from .common import StockPickingProductInterchangeableCommon + + +class TestStockPicking(StockPickingProductInterchangeableCommon): + def test_access_to_pass_interchangeable_field(self): + """Test flow to show/hide pass_interchangeable field""" + with self.assertRaises(AssertionError): + with Form(self.env["stock.picking"]) as form: + form.pass_interchangeable = True + with self.assertRaises(AssertionError): + with Form(self.env["stock.picking"]) as form: + form.picking_type_id = self.picking_type_in + form.pass_interchangeable = True + with Form(self.env["stock.picking"]) as form: + form.picking_type_id = self.picking_type_out + form.pass_interchangeable = True + self.assertTrue( + form.pass_interchangeable, + msg="'pass_interchangeable' field must be visible", + ) + + def _create_stock_picking(self, product_qty, pass_interchangeable=False): + """ + Create stock picking + :param int product_qty: Product Qty + :param bool pass_interchangeable: Pass Interchangeable flag + :return stock.picking: stock.picking record + """ + form = Form(self.env["stock.picking"]) + form.partner_id = self.res_partner_bob + form.picking_type_id = self.picking_type_out + form.pass_interchangeable = pass_interchangeable + with form.move_ids_without_package.new() as line: + line.product_id = self.product_knife + line.product_uom_qty = product_qty + return form.save() + + def test_picking_note_for_interchangeable_products(self): + """Test flow to create pickings note for interchangeable products""" + expected_string = f"{self.product_knife.display_name} missing qty 27.0 was replaced with:
    " # noqa + fork_str = f"
  • {self.product_fork.display_name} 20.0
  • " + spoon_str = f"
  • {self.product_spoon.display_name} 7.0
  • " + expected_string += f"
    " + picking = self._create_stock_picking(30) + picking.action_confirm() + self.assertEqual( + str(picking.note), expected_string, msg="Strings must be the same" + ) + + def test_create_many_pickings(self): + """Test flow to create and confirm pickings with different picking type""" + picking_1 = self._create_stock_picking(30) + form = Form(self.env["stock.picking"]) + form.picking_type_id = self.picking_type_in + with form.move_ids_without_package.new() as line: + line.product_id = self.product_napkin + line.product_uom_qty = 10 + picking_2 = form.save() + pickings = picking_1 | picking_2 + pickings.action_confirm() + self.assertEqual( + len(pickings.move_ids), 4, msg="Total stock moves count must be equal to 4" + ) + self.assertEqual( + len(picking_1.move_ids), + 3, + msg="First picking moves count must be equal to 3", + ) + self.assertEqual( + len(picking_2.move_ids), + 1, + msg="Second picking moves count must be equal to 1", + ) + + def test_create_delivery_stock_picking_with_pass_interchangeable(self): + """Test flow to skip interchangeable behavior for delivery stock.picking record""" + record = self._create_stock_picking(30, True) + self.assertEqual(len(record.move_ids), 1, msg="Moves count must be equal to 1") + knife_move = record.move_ids + self.assertEqual( + knife_move.product_id, + self.product_knife, + msg="Move product must be equal to 'Knife'", + ) + self.assertEqual( + knife_move.forecast_availability, + -27, + msg="Forecast Availability must be equal to -27", + ) + self.assertEqual( + knife_move.product_id.immediately_usable_qty, + 3, + msg="Products count on hand must be equal to 3", + ) + record.action_confirm() + self.assertEqual( + len(record.move_ids), 1, msg="Move lines count must be equal to 1" + ) + knife_move = record.move_ids + + self.assertEqual( + knife_move.product_id, + self.product_knife, + msg="Move product must be equal to 'Knife'", + ) + self.assertEqual( + knife_move.product_uom_qty, 30, msg="Move products Qty must be equal to 30" + ) + + def test_create_delivery_stock_picking_with_substitute_products_all_01(self): + """Test flow to stock picking with substitute products 'all'""" + record = self._create_stock_picking(30) + self.assertEqual(len(record.move_ids), 1, msg="Moves count must be equal to 1") + knife_move = record.move_ids + self.assertEqual( + knife_move.product_id, + self.product_knife, + msg="Move product must be equal to 'Knife'", + ) + self.assertEqual( + knife_move.forecast_availability, + -27, + msg="Forecast Availability must be equal to -27", + ) + self.assertEqual( + knife_move.product_id.immediately_usable_qty, + 3, + msg="Products count on hand must be equal to 3", + ) + record.action_confirm() + self.assertEqual(len(record.move_ids), 3, msg="Moves count must be equal to 3") + knife_move, fork_move, spoon_move = record.move_ids + + self.assertEqual( + knife_move.product_id, + self.product_knife, + msg="Move product must be equal to 'Knife'", + ) + self.assertEqual( + knife_move.product_uom_qty, 3, msg="Move products Qty must be equal to 3" + ) + + self.assertEqual( + fork_move.product_id, + self.product_fork, + msg="Move product must be equal to 'Fork'", + ) + self.assertEqual( + fork_move.product_uom_qty, 20, msg="Move products Qty must be equal to 20" + ) + + self.assertEqual( + spoon_move.product_id, + self.product_spoon, + msg="Move product must be equal to 'Spoon'", + ) + self.assertEqual( + spoon_move.product_uom_qty, 7, msg="Move products Qty must be equal to 7" + ) + + def test_create_delivery_stock_picking_with_substitute_products_all_02(self): + """Test flow to stock picking with substitute products 'all'""" + record = self._create_stock_picking(32) + self.assertEqual(len(record.move_ids), 1, msg="Moves count must be equal to 1") + knife_move = record.move_ids + self.assertEqual( + knife_move.product_id, + self.product_knife, + msg="Move product must be equal to 'Knife'", + ) + self.assertEqual( + knife_move.forecast_availability, + -29, + msg="Forecast Availability must be equal to -29", + ) + self.assertEqual( + knife_move.product_id.immediately_usable_qty, + 3, + msg="Products count on hand must be equal to 3", + ) + record.action_confirm() + self.assertEqual(len(record.move_ids), 1, msg="Moves count must be equal to 1") + knife_move = record.move_ids + + self.assertEqual( + knife_move.product_id, + self.product_knife, + msg="Move product must be equal to 'Knife'", + ) + self.assertEqual( + knife_move.product_uom_qty, 32, msg="Move products Qty must be equal to 32" + ) + + def test_create_delivery_stock_picking_with_substitute_products_any(self): + """Test flow to stock picking with substitute products 'any'""" + self.picking_type_out.write({"substitute_products_mode": "any"}) + record = self._create_stock_picking(32) + self.assertEqual(len(record.move_ids), 1, msg="Moves count must be equal to 1") + knife_move = record.move_ids + self.assertEqual( + knife_move.product_id, + self.product_knife, + msg="Move product must be equal to 'Knife'", + ) + self.assertEqual( + knife_move.forecast_availability, + -29, + msg="Forecast Availability must be equal to -29", + ) + self.assertEqual( + knife_move.product_id.immediately_usable_qty, + 3, + msg="Products count on hand must be equal to 3", + ) + record.action_confirm() + self.assertEqual(len(record.move_ids), 3, msg="Moves count must be equal to 3") + knife_move, fork_move, spoon_move = record.move_ids + + self.assertEqual( + knife_move.product_id, + self.product_knife, + msg="Move product must be equal to 'Knife'", + ) + self.assertEqual( + knife_move.product_uom_qty, 4, msg="Move products Qty must be equal to 4" + ) + + self.assertEqual(fork_move.product_id, self.product_fork) + self.assertEqual( + fork_move.product_uom_qty, 20, msg="Move products Qty must be equal to 20" + ) + + self.assertEqual(spoon_move.product_id, self.product_spoon) + self.assertEqual( + spoon_move.product_uom_qty, 8, msg="Move products Qty must be equal to 8" + ) diff --git a/stock_picking_product_interchangeable/views/product_views.xml b/stock_picking_product_interchangeable/views/product_views.xml new file mode 100644 index 000000000..14d18bbe8 --- /dev/null +++ b/stock_picking_product_interchangeable/views/product_views.xml @@ -0,0 +1,34 @@ + + + + + product.template.interchangeable.product.form + product.template + + + + + + + + + + + + product.product.interchangeable.form + product.product + + + + + + + + + + + diff --git a/stock_picking_product_interchangeable/views/stock_picking_type_views.xml b/stock_picking_product_interchangeable/views/stock_picking_type_views.xml new file mode 100644 index 000000000..40ef1b69e --- /dev/null +++ b/stock_picking_product_interchangeable/views/stock_picking_type_views.xml @@ -0,0 +1,17 @@ + + + + + stock.picking.type.substitute.products + stock.picking.type + + + + + + + + diff --git a/stock_picking_product_interchangeable/views/stock_picking_views.xml b/stock_picking_product_interchangeable/views/stock_picking_views.xml new file mode 100644 index 000000000..fd5e2860e --- /dev/null +++ b/stock_picking_product_interchangeable/views/stock_picking_views.xml @@ -0,0 +1,19 @@ + + + + + stock.picking.interchangeable.form + stock.picking + + + + + + + + + +