Skip to content

Commit

Permalink
[IMP] rma_sale : Always create rma line from sale line using stock mo…
Browse files Browse the repository at this point in the history
…ve lines by default

This allow to manage phantom bom products, by creating rma lines for the components instead of the kit if the option is activated.
  • Loading branch information
florian-dacosta committed May 23, 2024
1 parent 334b86f commit 2dce769
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 37 deletions.
5 changes: 4 additions & 1 deletion rma_sale/tests/test_rma_sale.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ def setUpClass(cls):
"pricelist_id": cls.env.ref("product.list0").id,
}
)

cls.so.action_confirm()
for move in cls.so.picking_ids.move_ids:
move.write({"quantity_done": move.product_uom_qty})
cls.so.picking_ids._action_done()
# Create RMA group and operation:
cls.rma_group = cls.rma_obj.create({"partner_id": customer1.id})
cls.operation_1 = cls.rma_op_obj.create(
Expand Down
112 changes: 77 additions & 35 deletions rma_sale/wizards/rma_add_sale.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def select_all(self):
"target": "new",
}

def _prepare_rma_line_from_sale_order_line(self, line, lot=None):
def _prepare_rma_line(self, line, product, quantity, uom_id=False, lot=None):
operation = self.rma_id.operation_default_id
if not operation:
operation = line.product_id.rma_customer_operation_id
Expand Down Expand Up @@ -121,33 +121,26 @@ def _prepare_rma_line_from_sale_order_line(self, line, lot=None):
or operation.in_warehouse_id.lot_rma_id
or warehouse.lot_rma_id
)
product_qty = line.product_uom_qty
if line.product_id.tracking == "serial":
product_qty = 1
elif line.product_id.tracking == "lot":
product_qty = sum(
line.mapped("move_ids.move_line_ids")
.filtered(lambda x: x.lot_id.id == lot.id)
.mapped("qty_done")
)
data = {
"partner_id": self.partner_id.id,
"description": self.rma_id.description,
"sale_line_id": line.id,
"product_id": line.product_id.id,
"product_id": product.id,
"lot_id": lot and lot.id or False,
"origin": line.order_id.name,
"uom_id": line.product_uom.id,
"uom_id": uom_id or product.uom_id.id,
"operation_id": operation.id,
"product_qty": product_qty,
"product_qty": quantity,
"delivery_address_id": self.sale_id.partner_shipping_id.id,
"invoice_address_id": self.sale_id.partner_invoice_id.id,
"price_unit": line.currency_id._convert(
"price_unit": line.product_id == product
and line.currency_id._convert(
line.price_unit,
line.currency_id,
line.company_id,
line.order_id.date_order,
),
)
or product.lst_price,
"rma_id": self.rma_id.id,
"in_route_id": operation.in_route_id.id or route.id,
"out_route_id": operation.out_route_id.id or route.id,
Expand All @@ -172,35 +165,84 @@ def _get_existing_sale_lines(self):
existing_sale_lines.append(rma_line.sale_line_id)
return existing_sale_lines

def _should_create_rma_line(self, line, existing_sale_line, lot=False):
if not lot and line in existing_sale_line:
return False
if lot and (
lot.id not in self.lot_ids.ids
or lot.id in self.rma_id.rma_line_ids.mapped("lot_id").ids
):
return False
return True

def _create_from_move_line(self, line):
return True

def _get_lot_quantity_from_move_lines(self, sale_line):
outgoing_lines = self.env["stock.move.line"]
incoming_lines = self.env["stock.move.line"]
sent_moves = sale_line.move_ids.filtered(
lambda m: m.state == "done" and not m.scrapped
)
for move in sent_moves:
if move.location_dest_id.usage == "customer" and (
not move.origin_returned_move_id
or (move.origin_returned_move_id and move.to_refund)
):
outgoing_lines |= move.move_line_ids
elif move.location_dest_id.usage != "customer" and move.to_refund:
incoming_lines |= move.move_line_ids
sent_product_data = {}
for line in outgoing_lines:
key = (line.product_id, line.product_uom_id, line.lot_id)
if key not in sent_product_data:
sent_product_data[key] = 0.0
sent_product_data[key] += line.qty_done
for line in incoming_lines:
key = (line.product_id, line.product_uom_id, line.lot_id)
if key not in sent_product_data:
sent_product_data[key] = 0.0
sent_product_data[key] -= line.qty_done
return sent_product_data

def add_lines(self):
rma_line_obj = self.env["rma.order.line"]
existing_sale_lines = self._get_existing_sale_lines()
existing_sale_line = self._get_existing_sale_lines()
for line in self.sale_line_ids:
tracking_move = line.product_id.tracking in ("serial", "lot")
# Load a PO line only once
if line not in existing_sale_lines or tracking_move:
if not tracking_move:
data = self._prepare_rma_line_from_sale_order_line(line)
if self._create_from_move_line(line):
sent_produt_data = self._get_lot_quantity_from_move_lines(line)
for (product, uom, lot), qty in sent_produt_data.items():
if not self._should_create_rma_line(
line, existing_sale_line, lot=lot
):
continue
data = self._prepare_rma_line(
line, product, qty, uom_id=uom.id, lot=lot
)
rec = rma_line_obj.create(data)
# Ensure that configuration on the operation is applied
# TODO MIG: in v16 the usage of such onchange can be removed in
# favor of (pre)computed stored editable fields for all policies
# and configuration in the RMA operation.
rec._onchange_operation_id()
else:
for lot in line.mapped("move_ids.move_line_ids.lot_id").filtered(
lambda x: x.id in self.lot_ids.ids
):
if lot.id in self.rma_id.rma_line_ids.mapped("lot_id").ids:
continue
data = self._prepare_rma_line_from_sale_order_line(line, lot)
rec = rma_line_obj.create(data)
# Ensure that configuration on the operation is applied
# TODO MIG: in v16 the usage of such onchange can be removed in
# favor of (pre)computed stored editable fields for all policies
# and configuration in the RMA operation.
rec._onchange_operation_id()
rec.price_unit = rec._get_price_unit()
else:
if not self._should_create_rma_line(line, existing_sale_line):
continue
# we can't have lot management based on sale order line
data = self._prepare_rma_line(
line,
line.product_id,
line.product_uom_qty,
uom_id=line.product_uom.id,
lot=False,
)
rec = rma_line_obj.create(data)
# Ensure that configuration on the operation is applied
# TODO MIG: in v16 the usage of such onchange can be removed in
# favor of (pre)computed stored editable fields for all policies
# and configuration in the RMA operation.
rec._onchange_operation_id()
rec.price_unit = rec._get_price_unit()
rma = self.rma_id
data_rma = self._get_rma_data()
rma.write(data_rma)
Expand Down
1 change: 1 addition & 0 deletions rma_sale_mrp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Copyright 2023 ForgeFlow S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html)
from . import models
from . import wizards
2 changes: 1 addition & 1 deletion rma_sale_mrp/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
"author": "ForgeFlow",
"website": "https://github.com/ForgeFlow/stock-rma",
"depends": ["rma_sale", "sale_mrp"],
"data": [],
"data": ["views/res_config_settings_views.xml"],
"installable": True,
}
2 changes: 2 additions & 0 deletions rma_sale_mrp/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Copyright 2023 ForgeFlow S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html)
from . import rma_order_line
from . import res_company
from . import res_config_settings
9 changes: 9 additions & 0 deletions rma_sale_mrp/models/res_company.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html)

from odoo import fields, models


class ResCompany(models.Model):
_inherit = "res.company"

add_component_from_sale = fields.Boolean()
14 changes: 14 additions & 0 deletions rma_sale_mrp/models/res_config_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html)

from odoo import fields, models


class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"

add_component_from_sale = fields.Boolean(
related="company_id.add_component_from_sale",
readonly=False,
help="If active, when creating a rma from a sale order, in case the product "
"is a kit, the delivered components will be added instead of the kit.",
)
1 change: 1 addition & 0 deletions rma_sale_mrp/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an parameter at company level to choose if rma lines are created for a kit or for the components, in the case the rma lines are created from a sale order.
35 changes: 35 additions & 0 deletions rma_sale_mrp/tests/test_rma_mrp.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,38 @@ def test_01_kit_return_with_diff_prices(self):
self.assertEqual(
150.0, sum(component_2_sm.mapped("stock_valuation_layer_ids.value"))
)

def test_02_add_kit_from_sale(self):
order_01 = self._make_sale_order(self.kit_product, 2, 30.0)
self._do_picking(order_01.picking_ids, 2.0)
rma = self.env["rma.order"].create({"partner_id": self.customer.id})
add_sale = (
self.env["rma_add_sale"]
.with_context(active_model="rma.order", active_ids=rma.ids)
.create(
{
"sale_id": order_01.id,
"sale_line_ids": [(6, 0, order_01.order_line.ids)],
}
)
)
add_sale.add_lines()
# component config is not set, we should create a rma line for the kit.
self.assertEqual(len(rma.rma_line_ids), 1)
self.assertEqual(rma.rma_line_ids.product_id, self.kit_product)
self.assertEqual(rma.rma_line_ids.product_qty, 2.0)

# test with component config now
rma.rma_line_ids.unlink()
order_01.company_id.write({"add_component_from_sale": True})
add_sale.add_lines()
self.assertEqual(len(rma.rma_line_ids), 2)
line_component_1 = rma.rma_line_ids.filtered(
lambda line: line.product_id == self.component_product_1
)
line_component_2 = rma.rma_line_ids.filtered(
lambda line: line.product_id == self.component_product_2
)
self.assertTrue(line_component_1)
self.assertEqual(line_component_1.product_qty, 2.0)
self.assertTrue(line_component_2)
25 changes: 25 additions & 0 deletions rma_sale_mrp/views/res_config_settings_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>

<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="model">res.config.settings</field>
<field name="priority" eval="10" />
<field name="inherit_id" ref="rma.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[@name='rma_account']" position="after">
<div name="kit_component" class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="add_component_from_sale" />
</div>
<div class="o_setting_right_pane">
<label for="add_component_from_sale" />
<div class="text-muted">
When adding rma lines from sale : add the components in case of a kit.
</div>
</div>
</div>
</xpath>
</field>
</record>

</odoo>
1 change: 1 addition & 0 deletions rma_sale_mrp/wizards/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import rma_add_sale
16 changes: 16 additions & 0 deletions rma_sale_mrp/wizards/rma_add_sale.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2020 ForgeFlow S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html)

from odoo import models


class RmaAddSale(models.TransientModel):
_inherit = "rma_add_sale"

def _create_from_move_line(self, line):
phantom_bom = line.move_ids.bom_line_id.bom_id.filtered(
lambda bom: bom.type == "phantom"
)
if phantom_bom and not line.company_id.add_component_from_sale:
return False
return super()._create_from_move_line(line)

0 comments on commit 2dce769

Please sign in to comment.