mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
[ADD] stock_picking_product_interchangeable: Added module.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
../../../../stock_picking_product_interchangeable
|
||||
6
setup/stock_picking_product_interchangeable/setup.py
Normal file
6
setup/stock_picking_product_interchangeable/setup.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
||||
0
stock_picking_product_interchangeable/README.rst
Normal file
0
stock_picking_product_interchangeable/README.rst
Normal file
1
stock_picking_product_interchangeable/__init__.py
Normal file
1
stock_picking_product_interchangeable/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
21
stock_picking_product_interchangeable/__manifest__.py
Normal file
21
stock_picking_product_interchangeable/__manifest__.py
Normal file
@@ -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,
|
||||
}
|
||||
55
stock_picking_product_interchangeable/demo/product_demo.xml
Normal file
55
stock_picking_product_interchangeable/demo/product_demo.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="res_partner_bob" model="res.partner">
|
||||
<field name="name">Bob</field>
|
||||
<field name="email">bob@example.com</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_plate" model="product.template">
|
||||
<field name="name">Plate</field>
|
||||
<field name="detailed_type">product</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_napkin" model="product.template">
|
||||
<field name="name">Napkin</field>
|
||||
<field name="detailed_type">product</field>
|
||||
</record>
|
||||
|
||||
<record
|
||||
id="product_template_napkin_attribute_line"
|
||||
model="product.template.attribute.line"
|
||||
>
|
||||
<field name="product_tmpl_id" ref="product_template_napkin" />
|
||||
<field name="attribute_id" ref="product.product_attribute_2" />
|
||||
<field
|
||||
name="value_ids"
|
||||
eval="[(4, ref('product.product_attribute_value_3')), (4, ref('product.product_attribute_value_4'))]"
|
||||
/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_chopsticks" model="product.product">
|
||||
<field name="name">Chopsticks</field>
|
||||
<field name="detailed_type">product</field>
|
||||
</record>
|
||||
|
||||
<record id="product_product_fork" model="product.product">
|
||||
<field name="name">Fork</field>
|
||||
<field name="detailed_type">product</field>
|
||||
</record>
|
||||
|
||||
<record id="product_product_spoon" model="product.product">
|
||||
<field name="name">Spoon</field>
|
||||
<field name="detailed_type">product</field>
|
||||
</record>
|
||||
|
||||
<record id="product_product_knife" model="product.product">
|
||||
<field name="name">Knife</field>
|
||||
<field name="detailed_type">product</field>
|
||||
<field
|
||||
name="product_interchangeable_ids"
|
||||
eval="[(4,ref('product_product_fork')), (4, ref('product_product_spoon'))]"
|
||||
/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
50
stock_picking_product_interchangeable/demo/stock_demo.xml
Normal file
50
stock_picking_product_interchangeable/demo/stock_demo.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<function model="stock.picking.type" name="write">
|
||||
<value eval="[ref('stock.picking_type_out')]" />
|
||||
<value eval="{'substitute_products_mode': 'all'}" />
|
||||
</function>
|
||||
|
||||
<record id="stock_inventory_fork" model="stock.quant">
|
||||
<field name="product_id" ref="product_product_fork" />
|
||||
<field name="inventory_quantity">20.0</field>
|
||||
<field
|
||||
name="location_id"
|
||||
model="stock.location"
|
||||
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"
|
||||
/>
|
||||
</record>
|
||||
|
||||
<record id="stock_inventory_spoon" model="stock.quant">
|
||||
<field name="product_id" ref="product_product_spoon" />
|
||||
<field name="inventory_quantity">8.0</field>
|
||||
<field
|
||||
name="location_id"
|
||||
model="stock.location"
|
||||
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"
|
||||
/>
|
||||
</record>
|
||||
|
||||
<record id="stock_inventory_knife" model="stock.quant">
|
||||
<field name="product_id" ref="product_product_knife" />
|
||||
<field name="inventory_quantity">3.0</field>
|
||||
<field
|
||||
name="location_id"
|
||||
model="stock.location"
|
||||
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"
|
||||
/>
|
||||
</record>
|
||||
|
||||
<function model="stock.quant" name="action_apply_inventory">
|
||||
<function
|
||||
eval="[[('id', 'in', (ref('stock_inventory_fork'),
|
||||
ref('stock_inventory_spoon'),
|
||||
ref('stock_inventory_knife'),
|
||||
))]]"
|
||||
model="stock.quant"
|
||||
name="search"
|
||||
/>
|
||||
</function>
|
||||
|
||||
</odoo>
|
||||
4
stock_picking_product_interchangeable/models/__init__.py
Normal file
4
stock_picking_product_interchangeable/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import product
|
||||
from . import stock_move
|
||||
from . import stock_picking
|
||||
from . import stock_picking_type
|
||||
66
stock_picking_product_interchangeable/models/product.py
Normal file
66
stock_picking_product_interchangeable/models/product.py
Normal file
@@ -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
|
||||
108
stock_picking_product_interchangeable/models/stock_move.py
Normal file
108
stock_picking_product_interchangeable/models/stock_move.py
Normal file
@@ -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"<b>{product.display_name}</b> missing qty <i>{qty}</i> was replaced with:<br\>" # noqa
|
||||
lines = [
|
||||
f"<li><b>{product.display_name}</b> <i>{qty}</i></li>"
|
||||
for product, qty in products_info
|
||||
]
|
||||
note += f"<ul>{''.join(lines)}</ul><br/>"
|
||||
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
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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.
|
||||
@@ -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".
|
||||
3
stock_picking_product_interchangeable/readme/USAGE.rst
Normal file
3
stock_picking_product_interchangeable/readme/USAGE.rst
Normal file
@@ -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.
|
||||
2
stock_picking_product_interchangeable/tests/__init__.py
Normal file
2
stock_picking_product_interchangeable/tests/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import test_product
|
||||
from . import test_stock_picking
|
||||
31
stock_picking_product_interchangeable/tests/common.py
Normal file
31
stock_picking_product_interchangeable/tests/common.py
Normal file
@@ -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"
|
||||
)
|
||||
44
stock_picking_product_interchangeable/tests/test_product.py
Normal file
44
stock_picking_product_interchangeable/tests/test_product.py
Normal file
@@ -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",
|
||||
)
|
||||
@@ -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"<b>{self.product_knife.display_name}</b> missing qty <i>27.0</i> was replaced with:<br>" # noqa
|
||||
fork_str = f"<li><b>{self.product_fork.display_name}</b> <i>20.0</i></li>"
|
||||
spoon_str = f"<li><b>{self.product_spoon.display_name}</b> <i>7.0</i></li>"
|
||||
expected_string += f"<ul>{''.join([fork_str, spoon_str])}</ul><br>"
|
||||
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"
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="product_template_interchangeable_form_view" model="ir.ui.view">
|
||||
<field name="name">product.template.interchangeable.product.form</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_only_form_view" />
|
||||
<field name="arch" type="xml">
|
||||
<notebook position="inside">
|
||||
<page
|
||||
name="interchangeable"
|
||||
string="Interchangeable"
|
||||
attrs="{'invisible': [('product_variant_count', '>', 1)]}"
|
||||
>
|
||||
<field name="product_tmpl_interchangeable_ids" nolabel="1" />
|
||||
</page>
|
||||
</notebook>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_normal_form_view" model="ir.ui.view">
|
||||
<field name="name">product.product.interchangeable.form</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="inherit_id" ref="product.product_normal_form_view" />
|
||||
<field name="arch" type="xml">
|
||||
<notebook position="inside">
|
||||
<page name="interchangeable" string="Interchangeable">
|
||||
<field name="product_interchangeable_ids" nolabel="1" />
|
||||
</page>
|
||||
</notebook>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="stock_picking_type" model="ir.ui.view">
|
||||
<field name="name">stock.picking.type.substitute.products</field>
|
||||
<field name="model">stock.picking.type</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_type_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="code" position="after">
|
||||
<field
|
||||
name="substitute_products_mode"
|
||||
attrs="{'invisible': [('code', '!=', 'outgoing')]}"
|
||||
/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_picking_form" model="ir.ui.view">
|
||||
<field name="name">stock.picking.interchangeable.form</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="picking_type_id" position="after">
|
||||
<field name="available_pass_interchangeable" invisible="1" />
|
||||
<field
|
||||
name="pass_interchangeable"
|
||||
attrs="{'invisible': [('available_pass_interchangeable', '=', False)]}"
|
||||
/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user