mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
stock_location_package_restriction: add nopackage
Allow to configure that you don't want a package on the location. Refactor code.
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
# Copyright 2023 Raumschmiede (http://www.raumschmiede.de)
|
||||
# Copyright 2023 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
{
|
||||
"name": "Stock Location Package Restriction",
|
||||
"summary": """
|
||||
Control if the location can contain products not in a package""",
|
||||
Control if the location can contain products in a package""",
|
||||
"version": "14.0.1.2.1",
|
||||
"category": "Warehouse Management",
|
||||
"author": "Raumschmiede.de, BCIM, Odoo Community Association (OCA)",
|
||||
"maintainters": ["jbaudoux"],
|
||||
"website": "https://github.com/OCA/stock-logistics-warehouse",
|
||||
"license": "AGPL-3",
|
||||
"depends": ["stock"],
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
# Copyright 2023 Raumschmiede (http://www.raumschmiede.de)
|
||||
# Copyright 2024 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
NOPACKAGE = "nopackage"
|
||||
SINGLEPACKAGE = "singlepackage"
|
||||
MULTIPACKAGE = "multiplepackage"
|
||||
|
||||
@@ -16,21 +19,96 @@ class StockLocation(models.Model):
|
||||
Control if the location can contain products not in a package.
|
||||
|
||||
Options:
|
||||
* False (not set): Not mandatory, the location can contain products
|
||||
not part of a package
|
||||
* False (not set): No restriction, the location can contain products
|
||||
with and without package
|
||||
* Forbidden: The location cannot have products part of a package
|
||||
* Mandatory and unique: The location cannot have products not
|
||||
part of a package and you cannot have more than 1 package on
|
||||
the location
|
||||
* Mandatory and not unique: The location cannot have products
|
||||
not part of a package and you may have store multiple packages
|
||||
not part of a package and you may store multiple packages
|
||||
on the location
|
||||
""",
|
||||
store=True,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _selection_package_restriction(self):
|
||||
return [
|
||||
(NOPACKAGE, "Forbidden"),
|
||||
(MULTIPACKAGE, "Mandatory"),
|
||||
(SINGLEPACKAGE, "Mandatory and unique"),
|
||||
]
|
||||
|
||||
def _check_package_restriction(self, move_lines=None):
|
||||
"""Check if the location respect the package restrictions
|
||||
|
||||
:param move_lines: Optional planned move_line to validate its destination location
|
||||
:raises ValidationError: if the restriction is not respected
|
||||
"""
|
||||
error_msgs = []
|
||||
move_lines = move_lines or self.env["stock.move.line"]
|
||||
for location in self:
|
||||
if not location.package_restriction:
|
||||
continue
|
||||
invalid_products = False
|
||||
if location.package_restriction == NOPACKAGE:
|
||||
if move_lines:
|
||||
move_lines_with_package = move_lines.filtered("result_package_id")
|
||||
if move_lines_with_package:
|
||||
invalid_products = move_lines_with_package.product_id
|
||||
else:
|
||||
quants_with_package = location.quant_ids.filtered(
|
||||
lambda q: q.package_id and q.quantity
|
||||
)
|
||||
if quants_with_package:
|
||||
invalid_products = quants_with_package.product_id
|
||||
if invalid_products:
|
||||
error_msgs.append(
|
||||
_(
|
||||
"A package is not allowed on the location {location}."
|
||||
"You cannot move the product(s) {product} with a package."
|
||||
).format(
|
||||
location=location.display_name,
|
||||
product=", ".join(invalid_products.mapped("display_name")),
|
||||
)
|
||||
)
|
||||
continue
|
||||
# SINGLE or MULTI
|
||||
if move_lines:
|
||||
move_lines_without_package = move_lines.filtered(
|
||||
lambda ml: not ml.result_package_id
|
||||
)
|
||||
if move_lines_without_package:
|
||||
invalid_products = move_lines_without_package.product_id
|
||||
else:
|
||||
quants_without_package = location.quant_ids.filtered(
|
||||
lambda q: not q.package_id and q.quantity
|
||||
)
|
||||
if quants_without_package:
|
||||
invalid_products = quants_without_package.product_id
|
||||
if invalid_products:
|
||||
error_msgs.append(
|
||||
_(
|
||||
"A package is mandatory on the location {location}.\n"
|
||||
"You cannot move the product(s) {product} without a package."
|
||||
).format(
|
||||
location=location.display_name,
|
||||
product=", ".join(invalid_products.mapped("display_name")),
|
||||
)
|
||||
)
|
||||
continue
|
||||
if location.package_restriction == MULTIPACKAGE:
|
||||
continue
|
||||
# SINGLE
|
||||
packages = (
|
||||
move_lines.result_package_id
|
||||
| location.quant_ids.filtered("quantity").package_id
|
||||
)
|
||||
if len(packages) > 1:
|
||||
error_msgs.append(
|
||||
_("Only one package is allowed on the location {location}.").format(
|
||||
location=location.display_name
|
||||
)
|
||||
)
|
||||
if error_msgs:
|
||||
raise ValidationError("\n".join(error_msgs))
|
||||
|
||||
@@ -1,112 +1,21 @@
|
||||
# Copyright 2023 Raumschmiede (http://www.raumschmiede.de)
|
||||
# Copyright 2024 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
|
||||
from odoo import _, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import groupby
|
||||
|
||||
from .stock_location import SINGLEPACKAGE
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = "stock.move"
|
||||
|
||||
def _check_location_package_restriction(
|
||||
self, new_location=None, only_qty_done=True
|
||||
):
|
||||
"""Check if the moves can be done regarding potential package restrictions
|
||||
|
||||
By default will only check move lines with a quantity done and their set
|
||||
destination location.
|
||||
|
||||
If `new_location` is set it will be use as destination location.
|
||||
If `only_qty_done` is False the check is executed on all
|
||||
related move lines.
|
||||
|
||||
"""
|
||||
Package = self.env["stock.quant.package"]
|
||||
error_msgs = []
|
||||
dest_location = new_location or self.move_line_ids.location_dest_id
|
||||
quants_grouped = self.env["stock.quant"].read_group(
|
||||
[
|
||||
("location_id", "in", dest_location.ids),
|
||||
("location_id.package_restriction", "!=", False),
|
||||
("quantity", ">", 0),
|
||||
],
|
||||
["location_id", "package_id:array_agg"],
|
||||
"location_id",
|
||||
)
|
||||
location_packages = {
|
||||
g["location_id"][0]: set(g["package_id"]) for g in quants_grouped
|
||||
}
|
||||
lines_being_processed = (
|
||||
self.move_line_ids.filtered(lambda line: line.qty_done)
|
||||
if only_qty_done
|
||||
else self.move_line_ids
|
||||
)
|
||||
for location, move_lines in groupby(
|
||||
lines_being_processed, lambda m: m.location_dest_id
|
||||
):
|
||||
if not location.package_restriction:
|
||||
continue
|
||||
|
||||
if new_location:
|
||||
location = new_location
|
||||
existing_package_ids = location_packages.get(location.id, set())
|
||||
existing_packages = Package.browse(existing_package_ids).exists()
|
||||
new_packages = Package.browse()
|
||||
for move_line in move_lines:
|
||||
package = move_line.result_package_id
|
||||
# CASE: Package restiction exists but there is no package in move
|
||||
if not package:
|
||||
error_msgs.append(
|
||||
_(
|
||||
"A package is mandatory on the location {location}. "
|
||||
"You cannot move the product {product} without a package."
|
||||
).format(
|
||||
location=location.display_name,
|
||||
product=move_line.product_id.display_name,
|
||||
)
|
||||
)
|
||||
continue
|
||||
if location.package_restriction != SINGLEPACKAGE:
|
||||
continue
|
||||
# CASE: Package on location, new package is different
|
||||
if existing_package_ids and package.id not in existing_package_ids:
|
||||
error_msgs.append(
|
||||
_(
|
||||
"Only one package is allowed on the location {location}."
|
||||
"You cannot add the {package}, there is already "
|
||||
"{existing_packages}."
|
||||
).format(
|
||||
location=location.display_name,
|
||||
existing_packages=", ".join(
|
||||
existing_packages.mapped("name")
|
||||
),
|
||||
package=package.display_name,
|
||||
)
|
||||
)
|
||||
continue
|
||||
# CASE: Multiple packages in Move but single location
|
||||
# with singlepackage Restriction
|
||||
if new_packages and package not in new_packages:
|
||||
error_msgs.append(
|
||||
_(
|
||||
"Only one package is allowed on the location {location}."
|
||||
"You cannot move multiple packages to it:"
|
||||
"{packages}"
|
||||
).format(
|
||||
location=location.display_name,
|
||||
packages=", ".join(new_packages.mapped("display_name")),
|
||||
)
|
||||
)
|
||||
continue
|
||||
new_packages |= package
|
||||
|
||||
if error_msgs:
|
||||
raise ValidationError("\n".join(error_msgs))
|
||||
def _check_location_package_restriction(self):
|
||||
"""Check if the moves can be done regarding potential package restrictions"""
|
||||
self.filtered(
|
||||
lambda m: m.state == "done"
|
||||
).location_dest_id._check_package_restriction()
|
||||
|
||||
def _action_done(self, cancel_backorder=False):
|
||||
res = super()._action_done(cancel_backorder=cancel_backorder)
|
||||
self._check_location_package_restriction()
|
||||
return super()._action_done(cancel_backorder=cancel_backorder)
|
||||
return res
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
* Raumschmiede <Raumschmiede.de>
|
||||
* Jacques-Etienne Baudoux <je@bcim.be>
|
||||
* Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
||||
* Juan Miguel Sánchez Arce <juan.sanchez@camptocamp.com>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
Configure on the location if the contained products must be part of a package
|
||||
and if you allow multiple package. When the module is installed, by default
|
||||
there is no restriction on the locations.
|
||||
Configure on the location the package restriction.
|
||||
|
||||
The options are:
|
||||
|
||||
* False (not set): No restriction, the location can contain products with and
|
||||
without package
|
||||
* Forbidden: The location cannot have products part of a package
|
||||
* Mandatory and unique: The location cannot have products not part of a
|
||||
package and you cannot have more than 1 package on the location
|
||||
* Mandatory and not unique: The location cannot have products not part of a
|
||||
package and you may have store multiple packages on the location
|
||||
* Not mandatory: The location can contain products not part of a package
|
||||
* Mandatory and not unique: The location cannot have products not part of a
|
||||
package and you may store multiple packages on the location
|
||||
|
||||
When the module is installed, by default there is no restriction on the locations.
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
from . import test_stock_inventory
|
||||
from . import test_stock_move
|
||||
from . import test_stock_move_line
|
||||
|
||||
142
stock_location_package_restriction/tests/common.py
Normal file
142
stock_location_package_restriction/tests/common.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# Copyright 2023 Raumschmiede (http://www.raumschmiede.de)
|
||||
# Copyright 2024 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from odoo.tests.common import SavepointCase
|
||||
|
||||
ShortMoveInfo = namedtuple(
|
||||
"ShortMoveInfo", ["product", "location_dest", "qty", "package_id"]
|
||||
)
|
||||
|
||||
|
||||
class TestLocationPackageRestrictionCommon(SavepointCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Data:
|
||||
2 products: product_1, product_2
|
||||
2 packages: pack_1, pack_2
|
||||
1 new warehouse: warehouse1
|
||||
2 new locations: location1 and location2 are children of
|
||||
warehouse1's stock location and without
|
||||
restriction
|
||||
stock:
|
||||
* 50 product_1 in location_1
|
||||
* 0 product_2 in stock
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
|
||||
cls.uom_unit = cls.env.ref("uom.product_uom_unit")
|
||||
Product = cls.env["product.product"]
|
||||
cls.product_1 = Product.create(
|
||||
{"name": "Wood", "type": "product", "uom_id": cls.uom_unit.id}
|
||||
)
|
||||
cls.product_2 = Product.create(
|
||||
{"name": "Stone", "type": "product", "uom_id": cls.uom_unit.id}
|
||||
)
|
||||
|
||||
# Warehouses
|
||||
cls.warehouse_1 = cls.env["stock.warehouse"].create(
|
||||
{
|
||||
"name": "Base Warehouse",
|
||||
"reception_steps": "one_step",
|
||||
"delivery_steps": "ship_only",
|
||||
"code": "BWH",
|
||||
}
|
||||
)
|
||||
|
||||
# Locations
|
||||
cls.location_1 = cls.env["stock.location"].create(
|
||||
{
|
||||
"name": "TestLocation1",
|
||||
"posx": 3,
|
||||
"location_id": cls.warehouse_1.lot_stock_id.id,
|
||||
}
|
||||
)
|
||||
|
||||
cls.location_2 = cls.env["stock.location"].create(
|
||||
{
|
||||
"name": "TestLocation2",
|
||||
"posx": 4,
|
||||
"location_id": cls.warehouse_1.lot_stock_id.id,
|
||||
}
|
||||
)
|
||||
|
||||
# Packages:
|
||||
Package = cls.env["stock.quant.package"]
|
||||
cls.pack_1 = Package.create({"name": "Package 1"})
|
||||
cls.pack_2 = Package.create({"name": "Package 2"})
|
||||
|
||||
cls.supplier_location = cls.env.ref("stock.stock_location_suppliers")
|
||||
|
||||
# partner
|
||||
cls.partner_1 = cls.env["res.partner"].create(
|
||||
{"name": "Raumschmiede.de", "email": "info@raumschmiede.de"}
|
||||
)
|
||||
|
||||
# picking type
|
||||
cls.picking_type_in = cls.env.ref("stock.picking_type_in")
|
||||
|
||||
cls.StockMove = cls.env["stock.move"]
|
||||
cls.StockPicking = cls.env["stock.picking"]
|
||||
|
||||
@classmethod
|
||||
def _change_product_qty(cls, product, location, package, qty):
|
||||
cls.env["stock.quant"].with_context(inventory_mode=True).create(
|
||||
{
|
||||
"product_id": product.id,
|
||||
"package_id": package and package.id,
|
||||
"inventory_quantity": qty,
|
||||
"location_id": location.id,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_package_in_location(cls, location):
|
||||
return (
|
||||
cls.env["stock.quant"]
|
||||
.search([("location_id", "=", location.id)])
|
||||
.mapped("package_id")
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _create_and_assign_picking(cls, short_move_infos, location_dest=None):
|
||||
location_dest = location_dest or cls.location_1
|
||||
picking_in = cls.StockPicking.create(
|
||||
{
|
||||
"partner_id": cls.partner_1.id,
|
||||
"picking_type_id": cls.picking_type_in.id,
|
||||
"location_id": cls.supplier_location.id,
|
||||
"location_dest_id": location_dest.id,
|
||||
}
|
||||
)
|
||||
for move_info in short_move_infos:
|
||||
cls.StockMove.create(
|
||||
{
|
||||
"name": move_info.product.name,
|
||||
"product_id": move_info.product.id,
|
||||
"product_uom_qty": move_info.qty,
|
||||
"product_uom": move_info.product.uom_id.id,
|
||||
"picking_id": picking_in.id,
|
||||
"location_id": cls.supplier_location.id,
|
||||
"location_dest_id": move_info.location_dest.id,
|
||||
}
|
||||
)
|
||||
|
||||
picking_in.action_confirm()
|
||||
for move_info in short_move_infos:
|
||||
line = picking_in.move_line_ids.filtered(
|
||||
lambda x: x.product_id.id == move_info.product.id
|
||||
)
|
||||
if line:
|
||||
line.result_package_id = move_info.package_id
|
||||
return picking_in
|
||||
|
||||
@classmethod
|
||||
def _process_picking(cls, picking):
|
||||
picking.action_assign()
|
||||
for line in picking.move_line_ids:
|
||||
line.qty_done = line.product_qty
|
||||
picking.button_validate()
|
||||
@@ -0,0 +1,79 @@
|
||||
# Copyright 2023 Raumschmiede (http://www.raumschmiede.de)
|
||||
# Copyright 2024 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo.addons.stock_location_package_restriction.models.stock_location import (
|
||||
NOPACKAGE,
|
||||
SINGLEPACKAGE,
|
||||
)
|
||||
|
||||
from .common import TestLocationPackageRestrictionCommon
|
||||
|
||||
|
||||
class TestInventory(TestLocationPackageRestrictionCommon):
|
||||
def test_inventory_no_restriction(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 without package_restriction
|
||||
location_1 with 50 product_1 and pack_1
|
||||
Test case:
|
||||
Add qty of product_2 into location_1 with pack_2
|
||||
Expected result:
|
||||
The location contains the 2 products in 2 different packages
|
||||
"""
|
||||
# Inventory Add product_1 to location_1
|
||||
self._change_product_qty(self.product_1, self.location_1, self.pack_1, 50)
|
||||
self.assertEqual(self.pack_1, self._get_package_in_location(self.location_1))
|
||||
|
||||
self._change_product_qty(self.product_2, self.location_1, self.pack_2, 10)
|
||||
self.assertEqual(
|
||||
self.pack_1 | self.pack_2,
|
||||
self._get_package_in_location(self.location_1),
|
||||
)
|
||||
|
||||
def test_inventory_no_package_success(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 with no package_restriction
|
||||
Test case:
|
||||
Add qty of product_1 into location_1
|
||||
Expected result:
|
||||
The location contains the product
|
||||
"""
|
||||
self.location_1.package_restriction = NOPACKAGE
|
||||
# Inventory Add product_1 to location_1
|
||||
self._change_product_qty(self.product_1, self.location_1, False, 50)
|
||||
|
||||
def test_inventory_no_package_error(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 with no package_restriction
|
||||
Test case:
|
||||
Add qty of product_1 into location_1 with pack_1
|
||||
Expected result:
|
||||
ValidationError
|
||||
"""
|
||||
self.location_1.package_restriction = NOPACKAGE
|
||||
# Inventory Add product_1 to location_1
|
||||
with self.assertRaises(ValidationError):
|
||||
self._change_product_qty(self.product_1, self.location_1, self.pack_1, 50)
|
||||
|
||||
def test_inventory_single_package(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 with single package restriction
|
||||
location_1 with 50 product_1 and pack_1
|
||||
Test case:
|
||||
Add qty of product_2 into location_1 with pack_2
|
||||
Expected result:
|
||||
ValidationError
|
||||
"""
|
||||
# Inventory Add product_1 to location_1
|
||||
self._change_product_qty(self.product_1, self.location_1, self.pack_1, 50)
|
||||
self.assertEqual(self.pack_1, self._get_package_in_location(self.location_1))
|
||||
self.location_1.package_restriction = SINGLEPACKAGE
|
||||
with self.assertRaises(ValidationError):
|
||||
self._change_product_qty(self.product_2, self.location_1, self.pack_2, 10)
|
||||
@@ -1,190 +1,24 @@
|
||||
# Copyright 2023 Raumschmiede (http://www.raumschmiede.de)
|
||||
# Copyright 2024 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import SavepointCase
|
||||
|
||||
from odoo.addons.stock_location_package_restriction.models.stock_location import (
|
||||
MULTIPACKAGE,
|
||||
NOPACKAGE,
|
||||
SINGLEPACKAGE,
|
||||
)
|
||||
|
||||
ShortMoveInfo = namedtuple(
|
||||
"ShortMoveInfo", ["product", "location_dest", "qty", "package_id"]
|
||||
)
|
||||
from .common import ShortMoveInfo, TestLocationPackageRestrictionCommon
|
||||
|
||||
|
||||
class TestStockMove(SavepointCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
class TestStockMove(TestLocationPackageRestrictionCommon):
|
||||
def test_picking_multi_package(self):
|
||||
"""
|
||||
Data:
|
||||
2 products: product_1, product_2
|
||||
2 packages: pack_1, pack_2
|
||||
1 new warehouse: warehouse1
|
||||
2 new locations: location1 and location2 are children of
|
||||
warehouse1's stock location and without
|
||||
restriction
|
||||
stock:
|
||||
* 50 product_1 in location_1
|
||||
* 0 product_2 in stock
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
|
||||
cls.uom_unit = cls.env.ref("uom.product_uom_unit")
|
||||
Product = cls.env["product.product"]
|
||||
cls.product_1 = Product.create(
|
||||
{"name": "Wood", "type": "product", "uom_id": cls.uom_unit.id}
|
||||
)
|
||||
cls.product_2 = Product.create(
|
||||
{"name": "Stone", "type": "product", "uom_id": cls.uom_unit.id}
|
||||
)
|
||||
|
||||
# Warehouses
|
||||
cls.warehouse_1 = cls.env["stock.warehouse"].create(
|
||||
{
|
||||
"name": "Base Warehouse",
|
||||
"reception_steps": "one_step",
|
||||
"delivery_steps": "ship_only",
|
||||
"code": "BWH",
|
||||
}
|
||||
)
|
||||
|
||||
# Locations
|
||||
cls.location_1 = cls.env["stock.location"].create(
|
||||
{
|
||||
"name": "TestLocation1",
|
||||
"posx": 3,
|
||||
"location_id": cls.warehouse_1.lot_stock_id.id,
|
||||
}
|
||||
)
|
||||
|
||||
cls.location_2 = cls.env["stock.location"].create(
|
||||
{
|
||||
"name": "TestLocation2",
|
||||
"posx": 4,
|
||||
"location_id": cls.warehouse_1.lot_stock_id.id,
|
||||
}
|
||||
)
|
||||
|
||||
# Packages:
|
||||
Package = cls.env["stock.quant.package"]
|
||||
cls.pack_1 = Package.create({"name": "Package 1"})
|
||||
cls.pack_2 = Package.create({"name": "Package 2"})
|
||||
|
||||
cls.supplier_location = cls.env.ref("stock.stock_location_suppliers")
|
||||
|
||||
# partner
|
||||
cls.partner_1 = cls.env["res.partner"].create(
|
||||
{"name": "Raumschmiede.de", "email": "info@raumschmiede.de"}
|
||||
)
|
||||
|
||||
# picking type
|
||||
cls.picking_type_in = cls.env.ref("stock.picking_type_in")
|
||||
|
||||
cls.StockMove = cls.env["stock.move"]
|
||||
cls.StockPicking = cls.env["stock.picking"]
|
||||
|
||||
@classmethod
|
||||
def _change_product_qty(cls, product, location, package, qty):
|
||||
cls.env["stock.quant"].with_context(inventory_mode=True).create(
|
||||
{
|
||||
"product_id": product.id,
|
||||
"package_id": package.id,
|
||||
"inventory_quantity": qty,
|
||||
"location_id": location.id,
|
||||
}
|
||||
)
|
||||
|
||||
def _get_package_in_location(self, location):
|
||||
return (
|
||||
self.env["stock.quant"]
|
||||
.search([("location_id", "=", location.id)])
|
||||
.mapped("package_id")
|
||||
)
|
||||
|
||||
def _create_and_assign_picking(self, short_move_infos, location_dest=None):
|
||||
location_dest = location_dest or self.location_1
|
||||
picking_in = self.StockPicking.create(
|
||||
{
|
||||
"partner_id": self.partner_1.id,
|
||||
"picking_type_id": self.picking_type_in.id,
|
||||
"location_id": self.supplier_location.id,
|
||||
"location_dest_id": location_dest.id,
|
||||
}
|
||||
)
|
||||
for move_info in short_move_infos:
|
||||
self.StockMove.create(
|
||||
{
|
||||
"name": move_info.product.name,
|
||||
"product_id": move_info.product.id,
|
||||
"product_uom_qty": move_info.qty,
|
||||
"product_uom": move_info.product.uom_id.id,
|
||||
"picking_id": picking_in.id,
|
||||
"location_id": self.supplier_location.id,
|
||||
"location_dest_id": move_info.location_dest.id,
|
||||
}
|
||||
)
|
||||
|
||||
picking_in.action_confirm()
|
||||
for move_info in short_move_infos:
|
||||
line = picking_in.move_line_ids.filtered(
|
||||
lambda x: x.product_id.id == move_info.product.id
|
||||
)
|
||||
if line:
|
||||
line.result_package_id = move_info.package_id
|
||||
return picking_in
|
||||
|
||||
def _process_picking(self, picking):
|
||||
picking.action_assign()
|
||||
for line in picking.move_line_ids:
|
||||
line.qty_done = line.product_qty
|
||||
picking.button_validate()
|
||||
|
||||
def test_00(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 without package_restriction
|
||||
location_1 with 50 product_1 and pack_1
|
||||
Test case:
|
||||
Add qty of product_2 into location_1 with pack_2
|
||||
Expected result:
|
||||
The location contains the 2 products in 2 different packages
|
||||
"""
|
||||
# Inventory Add product_1 to location_1
|
||||
self._change_product_qty(self.product_1, self.location_1, self.pack_1, 50)
|
||||
self.assertEqual(self.pack_1, self._get_package_in_location(self.location_1))
|
||||
|
||||
self._change_product_qty(self.product_2, self.location_1, self.pack_2, 10)
|
||||
self.assertEqual(
|
||||
self.pack_1 | self.pack_2,
|
||||
self._get_package_in_location(self.location_1),
|
||||
)
|
||||
|
||||
def test_01(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 with single package restriction
|
||||
location_1 with 50 product_1 and pack_1
|
||||
Test case:
|
||||
Add qty of product_2 into location_1 with pack_2
|
||||
Expected result:
|
||||
ValidationError
|
||||
"""
|
||||
# Inventory Add product_1 to location_1
|
||||
self._change_product_qty(self.product_1, self.location_1, self.pack_1, 50)
|
||||
self.assertEqual(self.pack_1, self._get_package_in_location(self.location_1))
|
||||
self.location_1.package_restriction = SINGLEPACKAGE
|
||||
with self.assertRaises(ValidationError):
|
||||
self._change_product_qty(self.product_2, self.location_1, self.pack_2, 10)
|
||||
|
||||
def test_02(self):
|
||||
"""
|
||||
Data:
|
||||
location_2 without product nor product restriction
|
||||
location_2 without product nor package restriction
|
||||
a picking with two move with destination location_2
|
||||
Test case:
|
||||
Process the picking
|
||||
@@ -215,7 +49,56 @@ class TestStockMove(SavepointCase):
|
||||
self._get_package_in_location(self.location_2),
|
||||
)
|
||||
|
||||
def test_03(self):
|
||||
def test_picking_no_package_success(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 with package restriction = 'no package'
|
||||
a picking with destination location_1
|
||||
Test case:
|
||||
Process the picking without result package
|
||||
Expected result:
|
||||
Picking processed
|
||||
"""
|
||||
self.location_1.package_restriction = NOPACKAGE
|
||||
picking = self._create_and_assign_picking(
|
||||
[
|
||||
ShortMoveInfo(
|
||||
product=self.product_1,
|
||||
location_dest=self.location_1,
|
||||
qty=2,
|
||||
package_id=False,
|
||||
),
|
||||
],
|
||||
location_dest=self.location_1,
|
||||
)
|
||||
self._process_picking(picking)
|
||||
|
||||
def test_picking_no_package_error(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 with package restriction = 'no package'
|
||||
a picking with destination location_1
|
||||
Test case:
|
||||
Process the picking with result package
|
||||
Expected result:
|
||||
ValidationError
|
||||
"""
|
||||
self.location_1.package_restriction = NOPACKAGE
|
||||
picking = self._create_and_assign_picking(
|
||||
[
|
||||
ShortMoveInfo(
|
||||
product=self.product_1,
|
||||
location_dest=self.location_1,
|
||||
qty=2,
|
||||
package_id=self.pack_1,
|
||||
),
|
||||
],
|
||||
location_dest=self.location_1,
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
self._process_picking(picking)
|
||||
|
||||
def test_picking_single_package_location_empty(self):
|
||||
"""
|
||||
Data:
|
||||
location_2 without package but with package restriction = 'single package'
|
||||
@@ -246,7 +129,7 @@ class TestStockMove(SavepointCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
self._process_picking(picking)
|
||||
|
||||
def test_03_with_backorder(self):
|
||||
def test_picking_single_package_with_backorder(self):
|
||||
"""
|
||||
Data:
|
||||
location_2 without package but with package restriction = 'single package'
|
||||
@@ -289,10 +172,10 @@ class TestStockMove(SavepointCase):
|
||||
wizard.process()
|
||||
self.assertEqual(self.pack_1, self._get_package_in_location(self.location_2))
|
||||
|
||||
def test_04(self):
|
||||
def test_picking_no_restriction(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 with product_1 and without product restriction = 'single package'
|
||||
location_1 with product_1 and without package restriction = 'single package'
|
||||
a picking with two moves:
|
||||
* product_1 -> location_1,
|
||||
* product_2 -> location_1
|
||||
@@ -331,10 +214,10 @@ class TestStockMove(SavepointCase):
|
||||
self._get_package_in_location(self.location_1),
|
||||
)
|
||||
|
||||
def test_05(self):
|
||||
def test_picking_single_package_location_not_empty(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 with product_1 but with product restriction = 'single package'
|
||||
location_1 with product_1 but with package restriction = 'single package'
|
||||
a picking with one move: product_2 -> location_1
|
||||
Test case:
|
||||
Process the picking
|
||||
|
||||
143
stock_location_package_restriction/tests/test_stock_move_line.py
Normal file
143
stock_location_package_restriction/tests/test_stock_move_line.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# Copyright 2024 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo.addons.stock_location_package_restriction.models.stock_location import (
|
||||
MULTIPACKAGE,
|
||||
NOPACKAGE,
|
||||
SINGLEPACKAGE,
|
||||
)
|
||||
|
||||
from .common import ShortMoveInfo, TestLocationPackageRestrictionCommon
|
||||
|
||||
|
||||
class TestMoveLine(TestLocationPackageRestrictionCommon):
|
||||
def test_move_line_multi_package(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 with pack1
|
||||
a move line with destination location_1
|
||||
Test case:
|
||||
Process the picking
|
||||
Expected result:
|
||||
The two packages are on location 2
|
||||
"""
|
||||
self.location_2.package_restriction = MULTIPACKAGE
|
||||
self._change_product_qty(self.product_1, self.location_1, self.pack_1, 50)
|
||||
picking = self._create_and_assign_picking(
|
||||
[
|
||||
ShortMoveInfo(
|
||||
product=self.product_2,
|
||||
location_dest=self.location_1,
|
||||
qty=2,
|
||||
package_id=self.pack_2,
|
||||
),
|
||||
],
|
||||
location_dest=self.location_1,
|
||||
)
|
||||
self.location_1._check_package_restriction(move_lines=picking.move_line_ids)
|
||||
|
||||
def test_move_line_no_package_success(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 with package restriction = 'no package'
|
||||
a move line with destination location_1
|
||||
Test case:
|
||||
Process the picking without result package
|
||||
Expected result:
|
||||
Picking processed
|
||||
"""
|
||||
self.location_1.package_restriction = NOPACKAGE
|
||||
picking = self._create_and_assign_picking(
|
||||
[
|
||||
ShortMoveInfo(
|
||||
product=self.product_1,
|
||||
location_dest=self.location_1,
|
||||
qty=2,
|
||||
package_id=False,
|
||||
),
|
||||
],
|
||||
location_dest=self.location_1,
|
||||
)
|
||||
self.location_1._check_package_restriction(move_lines=picking.move_line_ids)
|
||||
|
||||
def test_move_line_no_package_error(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 with package restriction = 'no package'
|
||||
a move line with destination location_1
|
||||
Test case:
|
||||
Process the picking with result package
|
||||
Expected result:
|
||||
ValidationError
|
||||
"""
|
||||
self.location_1.package_restriction = NOPACKAGE
|
||||
picking = self._create_and_assign_picking(
|
||||
[
|
||||
ShortMoveInfo(
|
||||
product=self.product_1,
|
||||
location_dest=self.location_1,
|
||||
qty=2,
|
||||
package_id=self.pack_1,
|
||||
),
|
||||
],
|
||||
location_dest=self.location_1,
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
self.location_1._check_package_restriction(move_lines=picking.move_line_ids)
|
||||
|
||||
def test_move_line_single_package_location_empty(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 without package but with package restriction = 'single package'
|
||||
a move line with two move with destination location_2
|
||||
Test case:
|
||||
Process the picking
|
||||
Expected result:
|
||||
ValidationError
|
||||
"""
|
||||
self.location_1.package_restriction = SINGLEPACKAGE
|
||||
picking = self._create_and_assign_picking(
|
||||
[
|
||||
ShortMoveInfo(
|
||||
product=self.product_1,
|
||||
location_dest=self.location_1,
|
||||
qty=2,
|
||||
package_id=self.pack_1,
|
||||
),
|
||||
],
|
||||
location_dest=self.location_1,
|
||||
)
|
||||
self.location_1._check_package_restriction(move_lines=picking.move_line_ids)
|
||||
|
||||
def test_move_line_single_package_location_not_empty(self):
|
||||
"""
|
||||
Data:
|
||||
location_1 with product_1 but with package restriction = 'single package'
|
||||
a picking with one move: product_2 -> location_1
|
||||
Test case:
|
||||
Process the picking
|
||||
Expected result:
|
||||
ValidationError
|
||||
"""
|
||||
self._change_product_qty(self.product_1, self.location_1, self.pack_1, 50)
|
||||
self.assertEqual(
|
||||
self.pack_1,
|
||||
self._get_package_in_location(self.location_1),
|
||||
)
|
||||
self.location_1.package_restriction = SINGLEPACKAGE
|
||||
picking = self._create_and_assign_picking(
|
||||
[
|
||||
ShortMoveInfo(
|
||||
product=self.product_2,
|
||||
location_dest=self.location_1,
|
||||
qty=2,
|
||||
package_id=self.pack_2,
|
||||
),
|
||||
],
|
||||
location_dest=self.location_1,
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
self.location_1._check_package_restriction(move_lines=picking.move_line_ids)
|
||||
Reference in New Issue
Block a user