Skip to content

Commit 2a0e1c7

Browse files
author
Juan Ignacio Carreras
committed
[FIX] stock_ux: return in the delivered move UoM on "Return All"
The core "Return All" button (action_create_returns_all) copies the delivered quantity into a return line interpreted in the product's reference UoM, while that quantity is expressed in the move UoM. When they differ (e.g. with stock.propagate_uom enabled, which keeps deliveries in the sale/purchase line UoM) the return came out with the raw, unconverted number in the wrong unit. Instead of converting to the reference UoM, make the return inherit the UoM of the move it reverses, backporting the behaviour Odoo 19 already ships: the return line UoM is computed from move_id.product_uom and the return move is created in that unit. This keeps the return document in the same unit as the delivery it reverts, avoids the round-up over-return on products whose reference ratio is not exact, and does not reintroduce the bug reversed once the module is ported to 19. Change note: El botón "Devolver todo" dejaba la devolución en una unidad de medida distinta a la de la entrega cuando el producto se opera en una UdM distinta a la de referencia (con "Propagar UdM" activo). Ahora la devolución queda en la misma unidad que la entrega que revierte y devuelve exactamente lo entregado, sin el redondeo que en algunos productos devolvía de más.
1 parent 9aea90f commit 2a0e1c7

3 files changed

Lines changed: 167 additions & 1 deletion

File tree

stock_ux/models/stock_return_picking.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
# For copyright and license notices, see __manifest__.py file in module root
33
# directory
44
##############################################################################
5-
from odoo import fields, models
5+
from odoo import api, fields, models
6+
from odoo.tools.float_utils import float_round
67

78

89
class StockReturnPicking(models.TransientModel):
@@ -15,3 +16,63 @@ def _create_return(self):
1516
picking = super()._create_return()
1617
picking.write({"note": self.reason})
1718
return picking
19+
20+
@api.model
21+
def _prepare_stock_return_picking_line_vals_from_move(self, stock_move):
22+
# `uom_id` pasa a computarse desde el movimiento (ver StockReturnPickingLine),
23+
# así que no lo forzamos acá a la UdM de referencia del producto.
24+
vals = super()._prepare_stock_return_picking_line_vals_from_move(stock_move)
25+
vals.pop("uom_id", None)
26+
return vals
27+
28+
def action_create_returns_all(self):
29+
# Backport del comportamiento de la 19: "Devolver todo" hereda la UdM del
30+
# movimiento que revierte, en vez de convertir a la UdM de referencia del
31+
# producto. Así el remito de devolución queda en la misma unidad que la
32+
# entrega (no contradice al remito que revierte), no hay redondeo hacia
33+
# arriba (que en productos con ratio no exacto devolvía de más) y no se
34+
# reintroduce el bug al portar a la 19, donde el core ya hace exactamente
35+
# esto. El core de la 18 copiaba `stock_move.quantity` (en la UdM del
36+
# movimiento) dentro de una línea interpretada en la UdM de referencia, por
37+
# eso salía el número crudo sin convertir. Ver ticket 125517.
38+
self.ensure_one()
39+
for return_move in self.product_return_moves:
40+
stock_move = return_move.move_id
41+
if not stock_move or stock_move.state == "cancel" or stock_move.scrapped:
42+
continue
43+
quantity = stock_move.quantity
44+
for move in stock_move.move_dest_ids:
45+
if not move.origin_returned_move_id or move.origin_returned_move_id != stock_move:
46+
continue
47+
quantity -= move.quantity
48+
return_move.quantity = float_round(quantity, precision_rounding=stock_move.product_uom.rounding)
49+
return self.action_create_returns()
50+
51+
52+
class StockReturnPickingLine(models.TransientModel):
53+
_inherit = "stock.return.picking.line"
54+
55+
# En el core de la 18 `uom_id` es related a `product_id.uom_id` (UdM de
56+
# referencia). Lo hacemos computado desde el movimiento para que la línea —y el
57+
# movimiento de devolución que se crea a partir de ella— queden en la misma UdM
58+
# que la entrega original, igual que en la 19. Ver ticket 125517.
59+
uom_id = fields.Many2one(
60+
"uom.uom",
61+
string="Unit of Measure",
62+
related=False,
63+
store=False,
64+
readonly=True,
65+
compute="_compute_uom_id",
66+
)
67+
68+
@api.depends("move_id.product_uom", "product_id.uom_id")
69+
def _compute_uom_id(self):
70+
for line in self:
71+
line.uom_id = line.move_id.product_uom or line.product_id.uom_id
72+
73+
def _prepare_move_default_values(self, new_picking):
74+
# el movimiento de devolución se crea en la UdM de la línea (heredada del
75+
# movimiento original), no en la de referencia del producto.
76+
vals = super()._prepare_move_default_values(new_picking)
77+
vals["product_uom"] = self.uom_id.id
78+
return vals

stock_ux/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from . import test_mto_warehouse_propagation
22
from . import test_stock_orderpoint_multiple_over_max
3+
from . import test_return_all_uom
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from odoo.tests import TransactionCase, tagged
2+
3+
4+
@tagged("post_install", "-at_install")
5+
class TestReturnAllUom(TransactionCase):
6+
""" "Devolver todo" debe devolver en la UdM del movimiento entregado.
7+
8+
Backport del comportamiento de la 19: cuando la entrega está en una UdM
9+
distinta a la de referencia del producto (p.ej. con `stock.propagate_uom`
10+
activo, que deja las entregas en la UdM de la línea), la devolución hereda la
11+
UdM del movimiento y devuelve exactamente lo entregado, sin convertir a la UdM
12+
de referencia (que dejaba el remito en otra unidad y redondeaba de más). Ver
13+
ticket 125517.
14+
"""
15+
16+
@classmethod
17+
def setUpClass(cls):
18+
super().setUpClass()
19+
cls.uom_dozen = cls.env.ref("uom.product_uom_dozen")
20+
cls.uom_unit = cls.env.ref("uom.product_uom_unit")
21+
cls.stock_location = cls.env.ref("stock.stock_location_stock")
22+
cls.customer_location = cls.env.ref("stock.stock_location_customers")
23+
cls.picking_type_out = cls.env.ref("stock.picking_type_out")
24+
25+
def _deliver(self, product, uom, qty):
26+
"""Confirma y valida una entrega de `qty` en `uom`, devuelve el picking hecho."""
27+
picking = self.env["stock.picking"].create(
28+
{
29+
"picking_type_id": self.picking_type_out.id,
30+
"location_id": self.stock_location.id,
31+
"location_dest_id": self.customer_location.id,
32+
"move_ids": [
33+
(
34+
0,
35+
0,
36+
{
37+
"name": product.name,
38+
"product_id": product.id,
39+
"product_uom": uom.id,
40+
"product_uom_qty": qty,
41+
"location_id": self.stock_location.id,
42+
"location_dest_id": self.customer_location.id,
43+
},
44+
)
45+
],
46+
}
47+
)
48+
picking.action_confirm()
49+
move = picking.move_ids
50+
move.quantity = qty
51+
move.picked = True
52+
picking._action_done()
53+
self.assertEqual(move.state, "done")
54+
self.assertEqual(move.product_uom, uom)
55+
return picking
56+
57+
def _return_all(self, picking):
58+
wizard = (
59+
self.env["stock.return.picking"]
60+
.with_context(active_id=picking.id, active_ids=picking.ids, active_model="stock.picking")
61+
.create({})
62+
)
63+
action = wizard.action_create_returns_all()
64+
return self.env["stock.picking"].browse(action["res_id"]).move_ids
65+
66+
def test_return_all_keeps_move_uom(self):
67+
"""Entrega en Unidades de un producto con referencia en Docenas -> la
68+
devolución queda en Unidades (misma UdM que la entrega), no en Docenas.
69+
70+
Sin el fix el core copiaba el número crudo (24) interpretándolo en la UdM
71+
de referencia, así que salían 24 Docenas.
72+
"""
73+
product = self.env["product.product"].create(
74+
{
75+
"name": "Producto con referencia en docenas",
76+
"is_storable": True,
77+
"uom_id": self.uom_dozen.id,
78+
"uom_po_id": self.uom_dozen.id,
79+
}
80+
)
81+
picking = self._deliver(product, self.uom_unit, 24)
82+
return_move = self._return_all(picking)
83+
self.assertEqual(return_move.product_uom, self.uom_unit)
84+
self.assertEqual(return_move.product_uom_qty, 24.0)
85+
86+
def test_return_all_no_overreturn_on_non_exact_ratio(self):
87+
"""Ratio no exacto: devolver 1 Unidad de un producto con referencia en
88+
Docenas no debe devolver de más.
89+
90+
Convertir a Docenas redondeaba 1/12 hacia arriba (0,09 Docenas = 1,08
91+
Unidades). Heredando la UdM del movimiento la devolución es 1 Unidad exacta.
92+
"""
93+
product = self.env["product.product"].create(
94+
{
95+
"name": "Producto docenas ratio no exacto",
96+
"is_storable": True,
97+
"uom_id": self.uom_dozen.id,
98+
"uom_po_id": self.uom_dozen.id,
99+
}
100+
)
101+
picking = self._deliver(product, self.uom_unit, 1)
102+
return_move = self._return_all(picking)
103+
self.assertEqual(return_move.product_uom, self.uom_unit)
104+
self.assertEqual(return_move.product_uom_qty, 1.0)

0 commit comments

Comments
 (0)