Skip to content

Commit

Permalink
Merge pull request #525 from akretion/16-rma-phantom-bom-components-r…
Browse files Browse the repository at this point in the history
…efactore

[16][rma_sale][rma_sale_mrp] Create rma line from stock move line instead of sale order line by default and when possible + manage kit components
  • Loading branch information
AaronHForgeFlow authored Aug 1, 2024
2 parents ec9c1b7 + 0bcd702 commit f83a0de
Show file tree
Hide file tree
Showing 12 changed files with 188 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
114 changes: 79 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,9 @@ def select_all(self):
"target": "new",
}

def _prepare_rma_line_from_sale_order_line(self, line, lot=None):
def _prepare_rma_line_from_sale_order_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 +123,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 +167,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_from_sale_order_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_from_sale_order_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"

rma_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"

rma_add_component_from_sale = fields.Boolean(
related="company_id.rma_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({"rma_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="rma_add_component_from_sale" />
</div>
<div class="o_setting_right_pane">
<label for="rma_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.rma_add_component_from_sale:
return False
return super()._create_from_move_line(line)

0 comments on commit f83a0de

Please sign in to comment.