[14.0][FIX] rma, add support to handle package used on customer moves

This commit is contained in:
Christopher Ormaza
2024-01-31 15:32:36 -05:00
parent a4f43cc4dd
commit ea4386f51e
9 changed files with 475 additions and 20 deletions

View File

@@ -11,3 +11,4 @@ from . import stock_rule
from . import res_partner
from . import res_company
from . import res_config_settings
from . import stock_package_level

View File

@@ -523,6 +523,10 @@ class RmaOrderLine(models.Model):
string="Under Warranty?", readonly=True, states={"draft": [("readonly", False)]}
)
def _get_stock_move_reference(self):
self.ensure_one()
return self.reference_move_id
def _prepare_rma_line_from_stock_move(self, sm, lot=False):
if not self.type:
self.type = self._get_default_type()

View File

@@ -110,3 +110,16 @@ class StockMove(models.Model):
res = super(StockMove, self)._prepare_procurement_values()
res["rma_line_id"] = self.rma_line_id.id
return res
class StockMoveLine(models.Model):
_inherit = "stock.move.line"
def _should_bypass_reservation(self, location):
res = super(StockMoveLine, self)._should_bypass_reservation(location)
if self.env.context.get(
"force_no_bypass_reservation"
) and location.usage not in ("customer", "supplier"):
return False
return res

View File

@@ -0,0 +1,20 @@
from odoo import models
class StockPackageLevel(models.Model):
_inherit = "stock.package_level"
def write(self, values):
ctx = self.env.context.copy()
if (
len(self) == 1
and "location_dest_id" in values
and self.location_dest_id.id == values.get("location_dest_id")
):
ctx.update(
{
"bypass_reservation_update": True,
}
)
return super(StockPackageLevel, self.with_context(**ctx)).write(values)

View File

@@ -16,6 +16,8 @@ class TestRma(common.SavepointCase):
cls.make_supplier_rma = cls.env["rma.order.line.make.supplier.rma"]
cls.rma_add_stock_move = cls.env["rma_add_stock_move"]
cls.product_ctg_model = cls.env["product.category"]
cls.lot_obj = cls.env["stock.production.lot"]
cls.package_obj = cls.env["stock.quant.package"]
cls.stockpicking = cls.env["stock.picking"]
cls.rma = cls.env["rma.order"]
cls.rma_line = cls.env["rma.order.line"]
@@ -34,6 +36,8 @@ class TestRma(common.SavepointCase):
)
cls.product_id = cls._create_product("PT0")
cls.product_1 = cls._create_product("PT1")
cls.product_1_serial = cls._create_product("PT1 Serial", "serial")
cls.product_1_lot = cls._create_product("PT1 Lot", "lot")
cls.product_2 = cls._create_product("PT2")
cls.product_3 = cls._create_product("PT3")
cls.uom_unit = cls.env.ref("uom.product_uom_unit")
@@ -151,9 +155,14 @@ class TestRma(common.SavepointCase):
)
@classmethod
def _create_product(cls, name):
def _create_product(cls, name, tracking="none"):
return cls.product_product_model.create(
{"name": name, "categ_id": cls.category.id, "type": "product"}
{
"name": name,
"categ_id": cls.category.id,
"type": "product",
"tracking": tracking,
}
)
@classmethod
@@ -180,7 +189,7 @@ class TestRma(common.SavepointCase):
picking.button_validate()
@classmethod
def _create_inventory(cls, product, qty, location):
def _create_inventory(cls, product, qty, location, lot_id=False, package_id=False):
"""
Creates inventory of a product on a specific location, this will be used
eventually to create a inventory at specific cost, that will be received in
@@ -198,6 +207,8 @@ class TestRma(common.SavepointCase):
"product_uom_id": product.uom_id.id,
"product_qty": qty,
"location_id": location.id,
"prod_lot_id": lot_id,
"package_id": package_id,
},
)
],
@@ -245,13 +256,21 @@ class TestRma(common.SavepointCase):
for item in products2move:
product = item[0]
product_qty = item[1]
cls._create_inventory(product, product_qty, cls.stock_location)
lot_id = len(item) >= 3 and item[2] or False
origin_package_id = len(item) >= 4 and item[3] or False
destination_package_id = len(item) >= 5 and item[4] or False
cls._create_inventory(
product, product_qty, cls.stock_location, lot_id, origin_package_id
)
move_values = cls._prepare_move(
product,
product_qty,
cls.stock_location,
cls.customer_location,
picking,
lot_id,
origin_package_id=origin_package_id,
destination_package_id=destination_package_id,
)
moves.append(cls.env["stock.move"].create(move_values))
else:
@@ -262,13 +281,21 @@ class TestRma(common.SavepointCase):
for item in products2move:
product = item[0]
product_qty = item[1]
cls._create_inventory(product, product_qty, cls.stock_location)
lot_id = len(item) >= 3 and item[2] or False
origin_package_id = len(item) >= 4 and item[3] or False
destination_package_id = len(item) >= 5 and item[4] or False
cls._create_inventory(
product, product_qty, cls.stock_location, lot_id, origin_package_id
)
move_values = cls._prepare_move(
product,
product_qty,
cls.supplier_location,
cls.stock_rma_location,
picking,
lot_id,
origin_package_id=origin_package_id,
destination_package_id=destination_package_id,
)
moves.append(cls.env["stock.move"].create(move_values))
# Process the picking
@@ -304,7 +331,9 @@ class TestRma(common.SavepointCase):
data = (
wizard.with_user(cls.rma_basic_user)
.with_context(customer=1)
._prepare_rma_line_from_stock_move(move)
._prepare_rma_line_from_stock_move(
move, lot=len(move.lot_ids) == 1 and move.lot_ids[0] or False
)
)
else:
@@ -346,10 +375,20 @@ class TestRma(common.SavepointCase):
return rma_id
@classmethod
def _prepare_move(cls, product, qty, src, dest, picking_in):
def _prepare_move(
cls,
product,
qty,
src,
dest,
picking_in,
lot_id=False,
origin_package_id=False,
destination_package_id=False,
):
location_id = src.id
return {
res = {
"name": product.name,
"partner_id": picking_in.partner_id.id,
"origin": picking_in.name,
@@ -363,6 +402,29 @@ class TestRma(common.SavepointCase):
"picking_id": picking_in.id,
"price_unit": product.standard_price,
}
if lot_id or origin_package_id or destination_package_id:
res.update(
{
"move_line_ids": [
(
0,
0,
{
"picking_id": picking_in.id,
"product_id": product.id,
"product_uom_id": product.uom_id.id,
"qty_done": qty,
"lot_id": lot_id,
"package_id": origin_package_id,
"result_package_id": destination_package_id,
"location_id": location_id,
"location_dest_id": dest.id,
},
)
]
}
)
return res
def _check_equal_quantity(self, qty1, qty2, msg):
self.assertEqual(qty1, qty2, msg)
@@ -1167,3 +1229,135 @@ class TestRma(common.SavepointCase):
self.assertTrue(partner, "Partner is not defined or False")
moves = picking.move_lines
self.assertEqual(len(moves), 1, "Incorrect number of moves created")
def test_10_customer_rma_tracking_lot(self):
lot = self.lot_obj.create(
{
"product_id": self.product_1_lot.id,
}
)
origin_package = self.package_obj.create({})
destination_package = self.package_obj.create({})
products2move = [
(self.product_1_lot, 5, lot.id, origin_package.id, destination_package.id)
]
rma_customer_id = self._create_rma_from_move(
products2move,
"customer",
self.env.ref("base.res_partner_2"),
dropship=False,
)
rma = rma_customer_id.rma_line_ids
rma.action_rma_to_approve()
wizard = self.rma_make_picking.with_context(
{
"active_ids": rma.ids,
"active_model": "rma.order.line",
"picking_type": "incoming",
"active_id": rma.ids[0],
}
).create({})
wizard.action_create_picking()
res = rma.action_view_in_shipments()
self.assertTrue("res_id" in res, "Incorrect number of pickings" "created")
picking = self.env["stock.picking"].browse(res["res_id"])
self.assertEqual(len(picking), 1, "Incorrect number of pickings created")
moves = picking.move_lines
self.assertEqual(
destination_package,
moves.mapped("move_line_ids.package_id"),
"Should have same package assigned",
)
self.assertFalse(
bool(moves.mapped("move_line_ids.result_package_id")),
"Destination package should not be assigned",
)
picking.action_assign()
for mv in picking.move_lines:
mv.quantity_done = mv.product_uom_qty
picking._action_done()
wizard = self.rma_make_picking.with_context(
{
"active_id": rma.ids[0],
"active_ids": rma.ids,
"active_model": "rma.order.line",
"picking_type": "outgoing",
}
).create({})
wizard.action_create_picking()
res = rma.action_view_out_shipments()
picking = self.env["stock.picking"].browse(res["res_id"])
picking.action_assign()
for mv in picking.move_lines:
mv.quantity_done = mv.product_uom_qty
picking._action_done()
self.assertEqual(picking.state, "done", "Final picking should has done state")
def test_11_customer_rma_tracking_serial(self):
lot = self.lot_obj.create(
{
"product_id": self.product_1_serial.id,
}
)
origin_package = self.package_obj.create({})
destination_package = self.package_obj.create({})
products2move = [
(
self.product_1_serial,
1,
lot.id,
origin_package.id,
destination_package.id,
)
]
rma_customer_id = self._create_rma_from_move(
products2move,
"customer",
self.env.ref("base.res_partner_2"),
dropship=False,
)
rma = rma_customer_id.rma_line_ids
rma.action_rma_to_approve()
wizard = self.rma_make_picking.with_context(
{
"active_ids": rma.ids,
"active_model": "rma.order.line",
"picking_type": "incoming",
"active_id": rma.ids[0],
}
).create({})
wizard.action_create_picking()
res = rma.action_view_in_shipments()
self.assertTrue("res_id" in res, "Incorrect number of pickings" "created")
picking = self.env["stock.picking"].browse(res["res_id"])
self.assertEqual(len(picking), 1, "Incorrect number of pickings created")
moves = picking.move_lines
self.assertEqual(
destination_package,
moves.mapped("move_line_ids.package_id"),
"Should have same package assigned",
)
self.assertFalse(
bool(moves.mapped("move_line_ids.result_package_id")),
"Destination package should not be assigned",
)
picking.action_assign()
for mv in picking.move_lines:
mv.quantity_done = mv.product_uom_qty
picking._action_done()
wizard = self.rma_make_picking.with_context(
{
"active_id": rma.ids[0],
"active_ids": rma.ids,
"active_model": "rma.order.line",
"picking_type": "outgoing",
}
).create({})
wizard.action_create_picking()
res = rma.action_view_out_shipments()
picking = self.env["stock.picking"].browse(res["res_id"])
picking.action_assign()
for mv in picking.move_lines:
mv.quantity_done = mv.product_uom_qty
picking._action_done()
self.assertEqual(picking.state, "done", "Final picking should has done state")

View File

@@ -196,6 +196,21 @@ class RmaMakePicking(models.TransientModel):
procurements.extend(procurement)
return procurements
def _is_final_step(self, move):
"""This function helps to know if wizard is called to finish process of rma,
customer is delivery return, and supplier is receipt return"""
if (
move.rma_line_id.type == "customer"
and self.env.context.get("picking_type") == "outgoing"
):
return True
if (
move.rma_line_id.type == "supplier"
and self.env.context.get("picking_type") == "incoming"
):
return True
return False
def action_create_picking(self):
self._create_picking()
move_line_model = self.env["stock.move.line"]
@@ -215,23 +230,68 @@ class RmaMakePicking(models.TransientModel):
and x.rma_line_id.lot_id
):
# Force the reservation of the RMA specific lot for incoming shipments.
is_final_step = self._is_final_step(move)
move.move_line_ids.unlink()
reference_moves = (
not is_final_step
and move.rma_line_id._get_stock_move_reference()
or self.env["stock.move"]
)
package = reference_moves.mapped("move_line_ids.result_package_id")
quants = self.env["stock.quant"]._gather(
move.product_id,
move.location_id,
lot_id=move.rma_line_id.lot_id,
package_id=len(package) == 1 and package or False,
)
move_line_data = move._prepare_move_line_vals(
reserved_quant=(len(quants) == 1) and quants or False
)
move_line_data.update(
{
"qty_done": 0,
}
)
if move.rma_line_id.lot_id and not quants:
# CHECK ME: force al least has lot assigned if quant is not found
move_line_data.update(
{
"lot_id": move.rma_line_id.lot_id.id,
}
)
if move.product_id.tracking == "serial":
move.write(
{
"lot_ids": [(6, 0, move.rma_line_id.lot_id.ids)],
"lot_ids": move.rma_line_id.lot_id.ids,
}
)
quants = self.env["stock.quant"]._gather(
move.product_id, move.location_id, lot_id=move.rma_line_id.lot_id
)
move.move_line_ids.write(
move_line_data.update(
{
"product_uom_qty": 1 if picking_type == "incoming" else 0,
"qty_done": 0,
"package_id": len(quants) == 1 and quants.package_id.id,
"product_uom_qty": 1.0,
}
)
if move.move_line_ids:
move.move_line_ids.with_context(
bypass_reservation_update=True
).write(
{
"lot_id": move_line_data.get("lot_id"),
"package_id": move_line_data.get("package_id"),
"result_package_id": move_line_data.get(
"result_package_id", False
),
"product_uom_qty": 1.0,
}
)
if (
len(quants) == 1
and quants.reserved_quantity == 0
and quants.quantity == 1
and quants.location_id.usage not in ("customer", "supplier")
):
quants.sudo().write(
{"reserved_quantity": quants.reserved_quantity + 1}
)
elif move.product_id.tracking == "lot":
if picking_type == "incoming":
qty = self.item_ids.filtered(
@@ -241,15 +301,12 @@ class RmaMakePicking(models.TransientModel):
qty = self.item_ids.filtered(
lambda x: x.line_id.id == move.rma_line_id.id
).qty_to_deliver
move_line_data = move._prepare_move_line_vals()
move_line_data.update(
{
"lot_id": move.rma_line_id.lot_id.id,
"product_uom_id": move.product_id.uom_id.id,
"qty_done": 0,
"product_uom_qty": qty if picking_type == "incoming" else 0,
}
)
if not move.move_line_ids:
move_line_model.create(move_line_data)
pickings.with_context(force_no_bypass_reservation=True).action_assign()
return action

View File

@@ -89,6 +89,18 @@ class RmaOrderLine(models.Model):
comodel_name="sale.order.line", compute="_compute_sale_line_domain"
)
def _get_stock_move_reference(self):
self.ensure_one()
move = self.reference_move_id
if self.sale_line_id:
# CHECK ME: backorder cases can be more than one move
sale_moves = self.sale_line_id.move_ids.filtered(
lambda x: x.location_dest_id.usage == "customer" and x.state == "done"
)
if sale_moves:
return sale_moves
return move
@api.depends("product_id", "partner_id")
def _compute_sale_line_domain(self):
line_model = self.env["sale.order.line"]

View File

@@ -1,2 +1,3 @@
from . import test_rma_sale
from . import test_rma_stock_account_sale
from . import test_rma_sale_tracking

View File

@@ -0,0 +1,153 @@
from odoo.addons.rma.tests.test_rma import TestRma
class TestRmaSaleTracking(TestRma):
@classmethod
def setUpClass(cls):
super(TestRmaSaleTracking, cls).setUpClass()
cls.rma_obj = cls.env["rma.order"]
cls.rma_line_obj = cls.env["rma.order.line"]
cls.rma_op_obj = cls.env["rma.operation"]
cls.rma_add_sale_wiz = cls.env["rma_add_sale"]
cls.rma_make_sale_wiz = cls.env["rma.order.line.make.sale.order"]
cls.so_obj = cls.env["sale.order"]
cls.sol_obj = cls.env["sale.order.line"]
cls.product_obj = cls.env["product.product"]
cls.partner_obj = cls.env["res.partner"]
cls.rma_route_cust = cls.env.ref("rma.route_rma_customer")
cls.customer1 = cls.partner_obj.create({"name": "Customer 1"})
cls.product_lot_2 = cls._create_product("PT2 Lot", "lot")
cls.product_serial_2 = cls._create_product("PT2 Serial", "serial")
cls.so = cls.so_obj.create(
{
"partner_id": cls.customer1.id,
"partner_invoice_id": cls.customer1.id,
"partner_shipping_id": cls.customer1.id,
"order_line": [
(
0,
0,
{
"name": cls.product_serial_2.name,
"product_id": cls.product_serial_2.id,
"product_uom_qty": 1.0,
"product_uom": cls.product_serial_2.uom_id.id,
"price_unit": cls.product_serial_2.list_price,
},
),
(
0,
0,
{
"name": cls.product_lot_2.name,
"product_id": cls.product_lot_2.id,
"product_uom_qty": 18.0,
"product_uom": cls.product_lot_2.uom_id.id,
"price_unit": cls.product_lot_2.list_price,
},
),
],
"pricelist_id": cls.env.ref("product.list0").id,
}
)
cls.so.action_confirm()
cls.serial = cls.lot_obj.create(
{
"product_id": cls.product_serial_2.id,
}
)
cls.lot = cls.lot_obj.create(
{
"product_id": cls.product_lot_2.id,
}
)
cls.package_1 = cls.package_obj.create({})
cls.package_2 = cls.package_obj.create({})
cls.package_3 = cls.package_obj.create({})
cls.package_4 = cls.package_obj.create({})
cls._create_inventory(
cls.product_serial_2, 1, cls.stock_location, cls.serial.id, cls.package_1.id
)
cls._create_inventory(
cls.product_lot_2, 1, cls.stock_location, cls.lot.id, cls.package_3.id
)
picking = cls.so.picking_ids
picking.action_assign()
for move in picking.move_lines:
if move.product_id.id == cls.product_serial_2.id:
move.move_line_ids.write({"result_package_id": cls.package_2.id})
if move.product_id.id == cls.product_lot_2.id:
move.move_line_ids.write({"result_package_id": cls.package_4.id})
cls._do_picking(picking)
# Create RMA group and operation:
cls.rma_group = cls.rma_obj.create({"partner_id": cls.customer1.id})
cls.operation_1 = cls.rma_op_obj.create(
{
"code": "TEST",
"name": "Sale afer receive",
"type": "customer",
"receipt_policy": "ordered",
"sale_policy": "received",
"in_route_id": cls.rma_route_cust.id,
"out_route_id": cls.rma_route_cust.id,
}
)
add_sale = cls.rma_add_sale_wiz.with_context(
{
"customer": True,
"active_ids": cls.rma_group.id,
"active_model": "rma.order",
}
).create(
{"sale_id": cls.so.id, "sale_line_ids": [(6, 0, cls.so.order_line.ids)]}
)
add_sale.add_lines()
def test_01_customer_rma_tracking(self):
rma_serial = self.rma_group.rma_line_ids.filtered(
lambda r: r.product_id == self.product_serial_2
)
rma_lot = self.rma_group.rma_line_ids.filtered(
lambda r: r.product_id == self.product_lot_2
)
for rma in rma_serial + rma_lot:
wizard = self.rma_make_picking.with_context(
{
"active_ids": rma.ids,
"active_model": "rma.order.line",
"picking_type": "incoming",
"active_id": rma.ids[0],
}
).create({})
wizard.action_create_picking()
res = rma.action_view_in_shipments()
self.assertTrue("res_id" in res, "Incorrect number of pickings" "created")
picking = self.env["stock.picking"].browse(res["res_id"])
self.assertEqual(len(picking), 1, "Incorrect number of pickings created")
moves = picking.move_lines
self.asserTrue(
bool(moves.mapped("move_line_ids.package_id")),
"Should have same package assigned",
)
self.assertFalse(
bool(moves.mapped("move_line_ids.result_package_id")),
"Destination package should not be assigned",
)
picking.action_assign()
for mv in picking.move_lines:
mv.quantity_done = mv.product_uom_qty
picking._action_done()
self.assertEqual(
picking.state, "done", "Final picking should has done state"
)