Skip to content

Commit

Permalink
[IMP] stock_picking_return_lot: Add a lot field to the return wizard
Browse files Browse the repository at this point in the history
- 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
sbejaoui committed Sep 2, 2024
1 parent 4393a37 commit 4f8c3f5
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 116 deletions.
16 changes: 15 additions & 1 deletion stock_picking_return_lot/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,16 @@ Stock Picking Return Lot

|badge1| |badge2| |badge3| |badge4| |badge5|

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.

**Table of contents**

Expand All @@ -52,6 +61,7 @@ Authors
~~~~~~~

* Camptocamp
* ACSONE SA/NV

Contributors
~~~~~~~~~~~~
Expand All @@ -67,6 +77,10 @@ Contributors

* Foresti Francesco <francesco.foresti@ooops404.com>

* `ACSONE SA/NV <https://www.acsone.eu>`__:

* Souheil Bejaoui <souheil.bejaoui@acsone.eu>

Maintainers
~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion stock_picking_return_lot/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from . import wizard
from . import wizards
6 changes: 4 additions & 2 deletions stock_picking_return_lot/__manifest__.py
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"],
}
4 changes: 4 additions & 0 deletions stock_picking_return_lot/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@
* `Ooops404 <https://www.ooops404.com>`__:

* Foresti Francesco <francesco.foresti@ooops404.com>

* `ACSONE SA/NV <https://www.acsone.eu>`__:

* Souheil Bejaoui <souheil.bejaoui@acsone.eu>
11 changes: 10 additions & 1 deletion stock_picking_return_lot/readme/DESCRIPTION.rst
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.
15 changes: 14 additions & 1 deletion stock_picking_return_lot/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,15 @@ <h1 class="title">Stock Picking Return Lot</h1>
!! source digest: sha256:a039386ff4a4425200e457d180c9f74b09f1f2c552b1c685f531a1d26c1d4740
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/stock-logistics-workflow/tree/16.0/stock_picking_return_lot"><img alt="OCA/stock-logistics-workflow" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/stock-logistics-workflow-16-0/stock-logistics-workflow-16-0-stock_picking_return_lot"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-workflow&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module propagates serial numbers or lots from initial picking to return picking.</p>
<p>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.</p>
<p>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 <a class="reference external" href="https://github.com/OCA/stock-logistics-workflow/tree/16.0/stock_restrict_lot">Stock Restrict Lot</a>
module to enforce accurate tracking, ensuring that the reception order reflects
the correct lot or serial number that should be received.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
Expand All @@ -396,6 +404,7 @@ <h1><a class="toc-backref" href="#toc-entry-2">Credits</a></h1>
<h2><a class="toc-backref" href="#toc-entry-3">Authors</a></h2>
<ul class="simple">
<li>Camptocamp</li>
<li>ACSONE SA/NV</li>
</ul>
</div>
<div class="section" id="contributors">
Expand All @@ -411,6 +420,10 @@ <h2><a class="toc-backref" href="#toc-entry-4">Contributors</a></h2>
<li>Foresti Francesco &lt;<a class="reference external" href="mailto:francesco.foresti&#64;ooops404.com">francesco.foresti&#64;ooops404.com</a>&gt;</li>
</ul>
</li>
<li><a class="reference external" href="https://www.acsone.eu">ACSONE SA/NV</a>:<ul>
<li>Souheil Bejaoui &lt;<a class="reference external" href="mailto:souheil.bejaoui&#64;acsone.eu">souheil.bejaoui&#64;acsone.eu</a>&gt;</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
Expand Down
252 changes: 174 additions & 78 deletions stock_picking_return_lot/tests/test_stock_picking_return_lot.py
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)
1 change: 0 additions & 1 deletion stock_picking_return_lot/wizard/__init__.py

This file was deleted.

Loading

0 comments on commit 4f8c3f5

Please sign in to comment.