mirror of
https://github.com/ForgeFlow/stock-rma.git
synced 2025-01-21 12:57:49 +02:00
[IMP] rma_sale : Always create rma line from sale line using stock move 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.
This commit is contained in:
@@ -65,7 +65,10 @@ class TestRmaSale(common.SingleTransactionCase):
|
||||
"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(
|
||||
|
||||
@@ -82,7 +82,7 @@ class RmaAddSale(models.TransientModel):
|
||||
"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
|
||||
@@ -121,33 +121,26 @@ class RmaAddSale(models.TransientModel):
|
||||
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,
|
||||
@@ -172,35 +165,84 @@ class RmaAddSale(models.TransientModel):
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
rma_sale_mrp/models/res_company.py
Normal file
9
rma_sale_mrp/models/res_company.py
Normal file
@@ -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
rma_sale_mrp/models/res_config_settings.py
Normal file
14
rma_sale_mrp/models/res_config_settings.py
Normal file
@@ -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.",
|
||||
)
|
||||
@@ -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.
|
||||
|
||||
@@ -252,3 +252,38 @@ class TestRmaMrp(TransactionCase):
|
||||
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
rma_sale_mrp/views/res_config_settings_views.xml
Normal file
25
rma_sale_mrp/views/res_config_settings_views.xml
Normal file
@@ -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
rma_sale_mrp/wizards/__init__.py
Normal file
1
rma_sale_mrp/wizards/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import rma_add_sale
|
||||
16
rma_sale_mrp/wizards/rma_add_sale.py
Normal file
16
rma_sale_mrp/wizards/rma_add_sale.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user