-
-
Notifications
You must be signed in to change notification settings - Fork 654
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[IMP] stock_picking_return_lot: Add a lot field to the return wizard
- Add a lot field to the return wizard and create a return line for each lot/product combination. - Use Stock Restrict Lot to track the accepted lot on the return picking.
- Loading branch information
Showing
13 changed files
with
363 additions
and
116 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
from . import wizard | ||
from . import wizards |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,14 @@ | ||
# Copyright 2020 Camptocamp | ||
# Copyright 2024 ACSONE SA/NV | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
{ | ||
"name": "Stock Picking Return Lot", | ||
"summary": "Propagate SN/lots from origin picking to return picking.", | ||
"version": "16.0.1.0.0", | ||
"license": "AGPL-3", | ||
"author": "Camptocamp, Odoo Community Association (OCA)", | ||
"author": "Camptocamp, ACSONE SA/NV, Odoo Community Association (OCA)", | ||
"website": "https://github.com/OCA/stock-logistics-workflow", | ||
"depends": ["stock", "stock_account"], | ||
"depends": ["stock", "stock_restrict_lot"], | ||
"data": ["wizards/stock_return_picking.xml"], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,10 @@ | ||
This module propagates serial numbers or lots from initial picking to return picking. | ||
When a product is tracked by lot or serial number and is returned by a customer, | ||
it’s crucial to clearly indicate to the user which lot or serial number can be | ||
accepted. This way, we prevent user from receiving a product with a lot or | ||
serial number different from the original delivery. | ||
|
||
This module enhances the return process by creating a separate return line for | ||
each product/lot and automatically pre-filling it with the lot from the original delivery. | ||
It relies on the `Stock Restrict Lot <https://github.com/OCA/stock-logistics-workflow/tree/16.0/stock_restrict_lot>`__ | ||
module to enforce accurate tracking, ensuring that the reception order reflects | ||
the correct lot or serial number that should be received. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
252 changes: 174 additions & 78 deletions
252
stock_picking_return_lot/tests/test_stock_picking_return_lot.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,107 +1,203 @@ | ||
# Copyright 2020 Iryna Vyshnevska Camptocamp | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). | ||
|
||
from odoo.tests import common | ||
from odoo import Command | ||
from odoo.tests.common import TransactionCase | ||
|
||
|
||
class StockPickingReturnLotTest(common.SavepointCase): | ||
def setUp(self): | ||
super().setUp() | ||
self.picking_obj = self.env["stock.picking"] | ||
partner = self.env["res.partner"].create({"name": "Test"}) | ||
product = self.env["product.product"].create( | ||
{"name": "test_product", "type": "product", "tracking": "serial"} | ||
) | ||
lot_1 = self.env["stock.lot"].create( | ||
{"name": "000001", "product_id": product.id} | ||
) | ||
lot_2 = self.env["stock.lot"].create( | ||
{"name": "000002", "product_id": product.id} | ||
) | ||
picking_type_out = self.env.ref("stock.picking_type_out") | ||
stock_location = self.env.ref("stock.stock_location_stock") | ||
customer_location = self.env.ref("stock.stock_location_customers") | ||
self.env["stock.quant"]._update_available_quantity( | ||
product, stock_location, 1, lot_id=lot_1 | ||
class StockPickingReturnLotTest(TransactionCase): | ||
@classmethod | ||
def setUpClass(cls): | ||
super().setUpClass() | ||
cls.picking_obj = cls.env["stock.picking"] | ||
cls.partner = cls.env["res.partner"].create({"name": "Test"}) | ||
cls.product = cls.env["product.product"].create( | ||
{"name": "test_product", "type": "product", "tracking": "lot"} | ||
) | ||
self.env["stock.quant"]._update_available_quantity( | ||
product, stock_location, 1, lot_id=lot_2 | ||
cls.lot_1 = cls.env["stock.lot"].create( | ||
{"name": "000001", "product_id": cls.product.id} | ||
) | ||
cls.lot_2 = cls.env["stock.lot"].create( | ||
{"name": "000002", "product_id": cls.product.id} | ||
) | ||
cls.picking_type_out = cls.env.ref("stock.picking_type_out") | ||
cls.stock_location = cls.env.ref("stock.stock_location_stock") | ||
cls.customer_location = cls.env.ref("stock.stock_location_customers") | ||
cls.env["stock.quant"]._update_available_quantity( | ||
cls.product, cls.stock_location, 1, lot_id=cls.lot_1 | ||
) | ||
self.picking = self.picking_obj.create( | ||
cls.env["stock.quant"]._update_available_quantity( | ||
cls.product, cls.stock_location, 2, lot_id=cls.lot_2 | ||
) | ||
cls.picking = cls.picking_obj.create( | ||
{ | ||
"partner_id": partner.id, | ||
"picking_type_id": picking_type_out.id, | ||
"location_id": stock_location.id, | ||
"location_dest_id": customer_location.id, | ||
"move_lines": [ | ||
( | ||
0, | ||
0, | ||
"partner_id": cls.partner.id, | ||
"picking_type_id": cls.picking_type_out.id, | ||
"location_id": cls.stock_location.id, | ||
"location_dest_id": cls.customer_location.id, | ||
"move_ids": [ | ||
Command.create( | ||
{ | ||
"name": product.name, | ||
"product_id": product.id, | ||
"product_uom_qty": 2, | ||
"product_uom": product.uom_id.id, | ||
"location_id": stock_location.id, | ||
"location_dest_id": customer_location.id, | ||
"name": cls.product.name, | ||
"product_id": cls.product.id, | ||
"product_uom_qty": 3, | ||
"product_uom": cls.product.uom_id.id, | ||
"location_id": cls.stock_location.id, | ||
"location_dest_id": cls.customer_location.id, | ||
}, | ||
) | ||
], | ||
} | ||
) | ||
self.picking.move_lines[0].write( | ||
cls.picking.action_confirm() | ||
cls.picking.action_assign() | ||
cls.picking.action_set_quantities_to_reservation() | ||
cls.picking._action_done() | ||
|
||
@classmethod | ||
def create_return_wiz(cls, picking): | ||
return ( | ||
cls.env["stock.return.picking"] | ||
.with_context(active_id=picking.id, active_model="stock.picking") | ||
.create({}) | ||
) | ||
|
||
def _create_validate_picking(self): | ||
picking = self.picking_obj.create( | ||
{ | ||
"move_line_ids": [ | ||
( | ||
0, | ||
0, | ||
{ | ||
"product_id": product.id, | ||
"qty_done": 1.0, | ||
"lot_id": lot_1.id, | ||
"product_uom_id": product.uom_id.id, | ||
"location_id": stock_location.id, | ||
"location_dest_id": customer_location.id, | ||
}, | ||
), | ||
( | ||
0, | ||
0, | ||
"partner_id": self.partner.id, | ||
"picking_type_id": self.picking_type_out.id, | ||
"location_id": self.stock_location.id, | ||
"location_dest_id": self.customer_location.id, | ||
"move_ids": [ | ||
Command.create( | ||
{ | ||
"product_id": product.id, | ||
"qty_done": 1.0, | ||
"lot_id": lot_2.id, | ||
"product_uom_id": product.uom_id.id, | ||
"location_id": stock_location.id, | ||
"location_dest_id": customer_location.id, | ||
"picking_id": self.picking.id, | ||
"name": self.product.name, | ||
"product_id": self.product.id, | ||
"product_uom_qty": 1, | ||
"product_uom": self.product.uom_id.id, | ||
"location_id": self.stock_location.id, | ||
"location_dest_id": self.customer_location.id, | ||
}, | ||
), | ||
] | ||
], | ||
} | ||
) | ||
self.env["stock.move"].create( | ||
{ | ||
"picking_id": picking.id, | ||
"name": self.product.name, | ||
"product_id": self.product.id, | ||
"product_uom_qty": 1, | ||
"product_uom": self.product.uom_id.id, | ||
"location_id": self.stock_location.id, | ||
"location_dest_id": self.customer_location.id, | ||
} | ||
) | ||
self.picking.action_confirm() | ||
self.picking.action_assign() | ||
self.picking.move_lines._action_done() | ||
picking.action_confirm() | ||
picking.action_assign() | ||
picking.action_set_quantities_to_reservation() | ||
picking._action_done() | ||
return picking | ||
|
||
def create_return_wiz(self): | ||
return ( | ||
self.env["stock.return.picking"] | ||
.with_context(active_id=self.picking.id, active_model="stock.picking") | ||
.create({}) | ||
def test_partial_return(self): | ||
wiz = self.create_return_wiz(self.picking) | ||
wiz._onchange_picking_id() | ||
self.assertEqual(len(wiz.product_return_moves), 2) | ||
return_line_1 = wiz.product_return_moves.filtered( | ||
lambda m, lot=self.lot_1: m.lot_id == lot | ||
) | ||
return_line_2 = wiz.product_return_moves.filtered( | ||
lambda m, lot=self.lot_2: m.lot_id == lot | ||
) | ||
self.assertEqual(return_line_1.quantity, 1) | ||
self.assertEqual(return_line_2.quantity, 2) | ||
return_line_2.quantity = 1 | ||
picking_returned_id = wiz._create_returns()[0] | ||
picking_returned = self.picking_obj.browse(picking_returned_id) | ||
move_1 = picking_returned.move_ids.filtered( | ||
lambda m, lot=self.lot_1: m.restrict_lot_id == lot | ||
) | ||
move_2 = picking_returned.move_ids.filtered( | ||
lambda m, lot=self.lot_2: m.restrict_lot_id == lot | ||
) | ||
self.assertEqual(move_1.move_line_ids.lot_id, self.lot_1) | ||
self.assertEqual(move_2.move_line_ids.lot_id, self.lot_2) | ||
self.assertEqual(move_2.product_qty, 1) | ||
|
||
def test_return(self): | ||
wiz = self.create_return_wiz() | ||
def test_full_return_after_partial_return(self): | ||
self.test_partial_return() | ||
wiz = self.create_return_wiz(self.picking) | ||
wiz._onchange_picking_id() | ||
self.assertEqual(len(wiz.product_return_moves), 1) | ||
self.assertEqual(len(wiz.product_return_moves), 2) | ||
|
||
return_line_1 = wiz.product_return_moves.filtered( | ||
lambda m, lot=self.lot_1: m.lot_id == lot | ||
) | ||
return_line_2 = wiz.product_return_moves.filtered( | ||
lambda m, lot=self.lot_2: m.lot_id == lot | ||
) | ||
self.assertEqual(return_line_1.quantity, 0) | ||
self.assertEqual(return_line_2.quantity, 1) | ||
picking_returned_id = wiz._create_returns()[0] | ||
picking_returned = self.picking_obj.browse(picking_returned_id) | ||
move_1 = picking_returned.move_ids.filtered( | ||
lambda m, lot=self.lot_1: m.restrict_lot_id == lot | ||
) | ||
move_2 = picking_returned.move_ids.filtered( | ||
lambda m, lot=self.lot_2: m.restrict_lot_id == lot | ||
) | ||
self.assertFalse(move_1) | ||
self.assertEqual(move_2.move_line_ids.lot_id, self.lot_2) | ||
self.assertEqual(move_2.product_qty, 1) | ||
|
||
def test_multiple_move_same_product_different_lot(self): | ||
self.env["stock.quant"]._update_available_quantity( | ||
self.product, self.stock_location, 1, lot_id=self.lot_1 | ||
) | ||
self.env["stock.quant"]._update_available_quantity( | ||
self.product, self.stock_location, 1, lot_id=self.lot_2 | ||
) | ||
picking = self._create_validate_picking() | ||
wiz = self.create_return_wiz(picking) | ||
wiz._onchange_picking_id() | ||
self.assertEqual(len(wiz.product_return_moves), 2) | ||
return_line_1 = wiz.product_return_moves.filtered( | ||
lambda m, lot=self.lot_1: m.lot_id == lot | ||
) | ||
return_line_2 = wiz.product_return_moves.filtered( | ||
lambda m, lot=self.lot_2: m.lot_id == lot | ||
) | ||
self.assertEqual(return_line_1.quantity, 1) | ||
self.assertEqual(return_line_2.quantity, 1) | ||
picking_returned_id = wiz._create_returns()[0] | ||
picking_returned = self.picking_obj.browse(picking_returned_id) | ||
move_1 = picking_returned.move_ids.filtered( | ||
lambda m, lot=self.lot_1: m.restrict_lot_id == lot | ||
) | ||
move_2 = picking_returned.move_ids.filtered( | ||
lambda m, lot=self.lot_2: m.restrict_lot_id == lot | ||
) | ||
self.assertEqual(move_1.move_line_ids.lot_id, self.lot_1) | ||
self.assertEqual(move_2.move_line_ids.lot_id, self.lot_2) | ||
self.assertEqual(move_2.product_qty, 1) | ||
|
||
self.assertEqual(len(picking_returned.move_line_ids), 2) | ||
self.assertTrue( | ||
picking_returned.move_line_ids.filtered(lambda l: l.lot_id.name == "000002") | ||
def test_multiple_move_same_product_same_lot(self): | ||
self.env["stock.quant"]._update_available_quantity( | ||
self.product, self.stock_location, 2, lot_id=self.lot_1 | ||
) | ||
picking = self._create_validate_picking() | ||
wiz = self.create_return_wiz(picking) | ||
wiz._onchange_picking_id() | ||
self.assertEqual(len(wiz.product_return_moves), 1) | ||
return_line_1 = wiz.product_return_moves.filtered( | ||
lambda m, lot=self.lot_1: m.lot_id == lot | ||
) | ||
self.assertTrue( | ||
picking_returned.move_line_ids.filtered(lambda l: l.lot_id.name == "000001") | ||
self.assertEqual(return_line_1.quantity, 2) | ||
picking_returned_id = wiz._create_returns()[0] | ||
picking_returned = self.picking_obj.browse(picking_returned_id) | ||
move_1 = picking_returned.move_ids.filtered( | ||
lambda m, lot=self.lot_1: m.restrict_lot_id == lot | ||
) | ||
self.assertEqual(move_1.move_line_ids.lot_id, self.lot_1) | ||
self.assertEqual(move_1.product_qty, 2) |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.