mirror of
https://github.com/OCA/rma.git
synced 2025-02-16 17:11:47 +02:00
@@ -134,6 +134,9 @@ Known issues / Roadmap
|
||||
* As soon as the picking is selected, the user should select the move,
|
||||
but perhaps stock.move _rec_name could be improved to better show what
|
||||
the product of that move is.
|
||||
* Add RMA reception and/or RMA delivery on several steps - 2 or 3 - like
|
||||
normal receptions/deliveries. It should be a separate option inside the
|
||||
warehouse definition.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
@@ -165,6 +168,7 @@ Contributors
|
||||
|
||||
* Chafique Delli <chafique.delli@akretion.com>
|
||||
* Giovanni Serra - Ooops <giovanni@ooops404.com>
|
||||
* Michael Tietz (MT Software) <mtietz@mt-software.de>
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{
|
||||
"name": "Return Merchandise Authorization Management",
|
||||
"summary": "Return Merchandise Authorization (RMA)",
|
||||
"version": "16.0.1.3.0",
|
||||
"version": "16.0.1.4.0",
|
||||
"development_status": "Production/Stable",
|
||||
"category": "RMA",
|
||||
"website": "https://github.com/OCA/rma",
|
||||
|
||||
13
rma/hooks.py
13
rma/hooks.py
@@ -1,4 +1,5 @@
|
||||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
||||
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import SUPERUSER_ID, api
|
||||
@@ -24,7 +25,9 @@ def post_init_hook(cr, registry):
|
||||
def create_rma_locations(warehouse):
|
||||
stock_location = env["stock.location"]
|
||||
if not warehouse.rma_loc_id:
|
||||
rma_location_vals = warehouse._get_rma_location_values()
|
||||
rma_location_vals = warehouse._get_rma_location_values(
|
||||
{"company_id": warehouse.company_id.id}, warehouse.code
|
||||
)
|
||||
warehouse.rma_loc_id = (
|
||||
stock_location.with_context(active_test=False)
|
||||
.create(rma_location_vals)
|
||||
@@ -61,11 +64,19 @@ def post_init_hook(cr, registry):
|
||||
whs.rma_in_type_id.return_picking_type_id = whs.rma_out_type_id.id
|
||||
whs.rma_out_type_id.return_picking_type_id = whs.rma_in_type_id.id
|
||||
|
||||
def create_rma_routes(warehouses):
|
||||
"""Create initially rma in/out stock.location.routes and stock.rules"""
|
||||
warehouses = warehouses.with_context(rma_post_init_hook=True)
|
||||
for wh in warehouses:
|
||||
route_vals = wh._create_or_update_route()
|
||||
wh.write(route_vals)
|
||||
|
||||
# Create rma locations and picking types
|
||||
warehouses = env["stock.warehouse"].search([])
|
||||
for warehouse in warehouses:
|
||||
create_rma_locations(warehouse)
|
||||
create_rma_picking_types(warehouse)
|
||||
create_rma_routes(warehouses)
|
||||
# Create rma sequence per company
|
||||
for company in env["res.company"].search([]):
|
||||
company.create_rma_index()
|
||||
|
||||
14
rma/migrations/16.0.1.4.0/post-migration.py
Normal file
14
rma/migrations/16.0.1.4.0/post-migration.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Copyright 2024 Tecnativa - Víctor Martínez
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from openupgradelib import openupgrade
|
||||
|
||||
|
||||
@openupgrade.migrate()
|
||||
def migrate(env, version):
|
||||
"""Similar behavior to create_rma_routes of post_init_hook."""
|
||||
warehouses = env["stock.warehouse"].search([])
|
||||
warehouses = warehouses.with_context(rma_post_init_hook=True)
|
||||
for wh in warehouses:
|
||||
route_vals = wh._create_or_update_route()
|
||||
wh.write(route_vals)
|
||||
@@ -1,12 +1,13 @@
|
||||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
||||
# Copyright 2023 Tecnativa - Pedro M. Baeza
|
||||
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
import logging
|
||||
from collections import Counter
|
||||
from collections import defaultdict
|
||||
from itertools import groupby
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import AccessError, ValidationError
|
||||
from odoo.tests import Form
|
||||
from odoo.tools import html2plaintext
|
||||
|
||||
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
|
||||
@@ -317,18 +318,8 @@ class Rma(models.Model):
|
||||
)
|
||||
|
||||
def _compute_delivery_picking_count(self):
|
||||
# It is enough to count the moves to know how many pickings
|
||||
# there are because there will be a unique move linked to the
|
||||
# same picking and the same rma.
|
||||
rma_data = self.env["stock.move"].read_group(
|
||||
[("rma_id", "in", self.ids)],
|
||||
["rma_id", "picking_id"],
|
||||
["rma_id", "picking_id"],
|
||||
lazy=False,
|
||||
)
|
||||
mapped_data = Counter(map(lambda r: r["rma_id"][0], rma_data))
|
||||
for record in self:
|
||||
record.delivery_picking_count = mapped_data.get(record.id, 0)
|
||||
for rma in self:
|
||||
rma.delivery_picking_count = len(rma.delivery_move_ids.picking_id)
|
||||
|
||||
@api.depends(
|
||||
"delivery_move_ids",
|
||||
@@ -670,21 +661,85 @@ class Rma(models.Model):
|
||||
}
|
||||
|
||||
def _add_message_subscribe_partner(self):
|
||||
self.ensure_one()
|
||||
if self.partner_id and self.partner_id not in self.message_partner_ids:
|
||||
self.message_subscribe([self.partner_id.id])
|
||||
|
||||
def _product_is_storable(self, product=None):
|
||||
product = product or self.product_id
|
||||
return product.type in ["product", "consu"]
|
||||
|
||||
def _prepare_procurement_group_vals(self):
|
||||
return {
|
||||
"move_type": "direct",
|
||||
"partner_id": self and self.partner_shipping_id.id or False,
|
||||
"name": self and ", ".join(self.mapped("name")) or False,
|
||||
}
|
||||
|
||||
def _prepare_common_procurement_vals(
|
||||
self, warehouse=None, scheduled_date=None, group=None
|
||||
):
|
||||
self.ensure_one()
|
||||
group = group or self.procurement_group_id
|
||||
if not group:
|
||||
group = self.env["procurement.group"].create(
|
||||
self._prepare_procurement_group_vals()
|
||||
)
|
||||
return {
|
||||
"company_id": self.company_id,
|
||||
"group_id": group,
|
||||
"date_planned": scheduled_date or fields.Datetime.now(),
|
||||
"warehouse_id": warehouse or self.warehouse_id,
|
||||
"partner_id": group.partner_id.id,
|
||||
"priority": self.priority,
|
||||
}
|
||||
|
||||
def _prepare_reception_procurement_vals(self, group=None):
|
||||
"""This method is used only for reception and a specific RMA IN route."""
|
||||
vals = self._prepare_common_procurement_vals(group=group)
|
||||
vals["route_ids"] = self.warehouse_id.rma_in_route_id
|
||||
vals["rma_receiver_ids"] = [(6, 0, self.ids)]
|
||||
if self.move_id:
|
||||
vals["origin_returned_move_id"] = self.move_id.id
|
||||
return vals
|
||||
|
||||
def _prepare_reception_procurements(self):
|
||||
procurements = []
|
||||
group_model = self.env["procurement.group"]
|
||||
for rma in self:
|
||||
if not rma._product_is_storable():
|
||||
continue
|
||||
group = rma.procurement_group_id
|
||||
if not group:
|
||||
group = group_model.create(rma._prepare_procurement_group_vals())
|
||||
procurements.append(
|
||||
group_model.Procurement(
|
||||
rma.product_id,
|
||||
rma.product_uom_qty,
|
||||
rma.product_uom,
|
||||
rma.location_id,
|
||||
rma.product_id.display_name,
|
||||
group.name,
|
||||
rma.company_id,
|
||||
rma._prepare_reception_procurement_vals(group),
|
||||
)
|
||||
)
|
||||
return procurements
|
||||
|
||||
def action_confirm(self):
|
||||
"""Invoked when 'Confirm' button in rma form view is clicked."""
|
||||
self.ensure_one()
|
||||
self._ensure_required_fields()
|
||||
if self.state == "draft":
|
||||
if self.picking_id:
|
||||
reception_move = self._create_receptions_from_picking()
|
||||
else:
|
||||
reception_move = self._create_receptions_from_product()
|
||||
self.write({"reception_move_id": reception_move.id, "state": "confirmed"})
|
||||
self._add_message_subscribe_partner()
|
||||
self._send_confirmation_email()
|
||||
self = self.filtered(lambda rma: rma.state == "draft")
|
||||
if not self:
|
||||
return
|
||||
procurements = self._prepare_reception_procurements()
|
||||
if procurements:
|
||||
self.env["procurement.group"].run(procurements)
|
||||
self.reception_move_id.picking_id.action_assign()
|
||||
self.write({"state": "confirmed"})
|
||||
for rma in self:
|
||||
rma._add_message_subscribe_partner()
|
||||
self._send_confirmation_email()
|
||||
|
||||
def action_refund(self):
|
||||
"""Invoked when 'Refund' button in rma form view is clicked
|
||||
@@ -723,11 +778,8 @@ class Rma(models.Model):
|
||||
self._ensure_can_be_replaced()
|
||||
# Force active_id to avoid issues when coming from smart buttons
|
||||
# in other models
|
||||
action = (
|
||||
self.env.ref("rma.rma_delivery_wizard_action")
|
||||
.sudo()
|
||||
.with_context(active_id=self.id)
|
||||
.read()[0]
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"rma.rma_delivery_wizard_action"
|
||||
)
|
||||
action["name"] = "Replace product(s)"
|
||||
action["context"] = dict(self.env.context)
|
||||
@@ -746,11 +798,8 @@ class Rma(models.Model):
|
||||
self._ensure_can_be_returned()
|
||||
# Force active_id to avoid issues when coming from smart buttons
|
||||
# in other models
|
||||
action = (
|
||||
self.env.ref("rma.rma_delivery_wizard_action")
|
||||
.sudo()
|
||||
.with_context(active_id=self.id)
|
||||
.read()[0]
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"rma.rma_delivery_wizard_action"
|
||||
)
|
||||
action["context"] = dict(self.env.context)
|
||||
action["context"].update(
|
||||
@@ -766,11 +815,8 @@ class Rma(models.Model):
|
||||
self._ensure_can_be_split()
|
||||
# Force active_id to avoid issues when coming from smart buttons
|
||||
# in other models
|
||||
action = (
|
||||
self.env.ref("rma.rma_split_wizard_action")
|
||||
.sudo()
|
||||
.with_context(active_id=self.id)
|
||||
.read()[0]
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"rma.rma_split_wizard_action"
|
||||
)
|
||||
action["context"] = dict(self.env.context)
|
||||
action["context"].update(active_id=self.id, active_ids=self.ids)
|
||||
@@ -782,11 +828,8 @@ class Rma(models.Model):
|
||||
self._ensure_can_be_returned()
|
||||
# Force active_id to avoid issues when coming from smart buttons
|
||||
# in other models
|
||||
action = (
|
||||
self.env.ref("rma.rma_finalization_wizard_action")
|
||||
.sudo()
|
||||
.with_context(active_id=self.id)
|
||||
.read()[0]
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"rma.rma_finalization_wizard_action"
|
||||
)
|
||||
action["context"] = dict(self.env.context)
|
||||
action["context"].update(active_id=self.id, active_ids=self.ids)
|
||||
@@ -794,7 +837,7 @@ class Rma(models.Model):
|
||||
|
||||
def action_cancel(self):
|
||||
"""Invoked when 'Cancel' button in rma form view is clicked."""
|
||||
self.mapped("reception_move_id")._action_cancel()
|
||||
self.reception_move_id._action_cancel()
|
||||
self.write({"state": "cancelled"})
|
||||
|
||||
def action_draft(self):
|
||||
@@ -819,25 +862,28 @@ class Rma(models.Model):
|
||||
"url": self.get_portal_url(),
|
||||
}
|
||||
|
||||
def action_view_receipt(self):
|
||||
"""Invoked when 'Receipt' smart button in rma form view is clicked."""
|
||||
def _action_view_pickings(self, pickings):
|
||||
self.ensure_one()
|
||||
# Force active_id to avoid issues when coming from smart buttons
|
||||
# in other models
|
||||
action = (
|
||||
self.env.ref("stock.action_picking_tree_all")
|
||||
.sudo()
|
||||
.with_context(active_id=self.id)
|
||||
.read()[0]
|
||||
)
|
||||
action.update(
|
||||
res_id=self.reception_move_id.picking_id.id,
|
||||
view_mode="form",
|
||||
view_id=False,
|
||||
views=False,
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"stock.action_picking_tree_all"
|
||||
)
|
||||
if len(pickings) > 1:
|
||||
action["domain"] = [("id", "in", pickings.ids)]
|
||||
elif pickings:
|
||||
action.update(
|
||||
res_id=pickings.id,
|
||||
view_mode="form",
|
||||
view_id=False,
|
||||
views=False,
|
||||
)
|
||||
return action
|
||||
|
||||
def action_view_receipt(self):
|
||||
"""Invoked when 'Receipt' smart button in rma form view is clicked."""
|
||||
return self._action_view_pickings(self.mapped("reception_move_id.picking_id"))
|
||||
|
||||
def action_view_refund(self):
|
||||
"""Invoked when 'Refund' smart button in rma form view is clicked."""
|
||||
self.ensure_one()
|
||||
@@ -853,23 +899,7 @@ class Rma(models.Model):
|
||||
|
||||
def action_view_delivery(self):
|
||||
"""Invoked when 'Delivery' smart button in rma form view is clicked."""
|
||||
action = (
|
||||
self.env.ref("stock.action_picking_tree_all")
|
||||
.sudo()
|
||||
.with_context(active_id=self.id)
|
||||
.read()[0]
|
||||
)
|
||||
picking = self.delivery_move_ids.mapped("picking_id")
|
||||
if len(picking) > 1:
|
||||
action["domain"] = [("id", "in", picking.ids)]
|
||||
elif picking:
|
||||
action.update(
|
||||
res_id=picking.id,
|
||||
view_mode="form",
|
||||
view_id=False,
|
||||
views=False,
|
||||
)
|
||||
return action
|
||||
return self._action_view_pickings(self.mapped("delivery_move_ids.picking_id"))
|
||||
|
||||
# Validation business methods
|
||||
def _ensure_required_fields(self):
|
||||
@@ -985,81 +1015,6 @@ class Rma(models.Model):
|
||||
)
|
||||
)
|
||||
|
||||
# Reception business methods
|
||||
def _create_receptions_from_picking(self):
|
||||
self.ensure_one()
|
||||
stock_return_picking_form = Form(
|
||||
self.env["stock.return.picking"].with_context(
|
||||
active_ids=self.picking_id.ids,
|
||||
active_id=self.picking_id.id,
|
||||
active_model="stock.picking",
|
||||
)
|
||||
)
|
||||
return_wizard = stock_return_picking_form.save()
|
||||
if self.location_id:
|
||||
return_wizard.location_id = self.location_id
|
||||
return_wizard.product_return_moves.filtered(
|
||||
lambda r: r.move_id != self.move_id
|
||||
).unlink()
|
||||
return_line = return_wizard.product_return_moves
|
||||
return_line.update(
|
||||
{
|
||||
"quantity": self.product_uom_qty,
|
||||
# The to_refund field is now True by default, which isn't right in the
|
||||
# RMA creation context
|
||||
"to_refund": False,
|
||||
}
|
||||
)
|
||||
# set_rma_picking_type is to override the copy() method of stock
|
||||
# picking and change the default picking type to rma picking type.
|
||||
picking_action = return_wizard.with_context(
|
||||
set_rma_picking_type=True
|
||||
).create_returns()
|
||||
picking_id = picking_action["res_id"]
|
||||
picking = self.env["stock.picking"].browse(picking_id)
|
||||
picking.origin = "{} ({})".format(self.name, picking.origin)
|
||||
move = picking.move_ids
|
||||
move.priority = self.priority
|
||||
return move
|
||||
|
||||
def _create_receptions_from_product(self):
|
||||
self.ensure_one()
|
||||
picking = self.env["stock.picking"].create(self._prepare_picking_vals())
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
picking.message_post_with_view(
|
||||
"mail.message_origin_link",
|
||||
values={"self": picking, "origin": self},
|
||||
subtype_id=self.env.ref("mail.mt_note").id,
|
||||
)
|
||||
return picking.move_ids
|
||||
|
||||
def _prepare_picking_vals(self):
|
||||
return {
|
||||
"picking_type_id": self.warehouse_id.rma_in_type_id.id,
|
||||
"origin": self.name,
|
||||
"partner_id": self.partner_shipping_id.id,
|
||||
"location_id": self.partner_shipping_id.property_stock_customer.id,
|
||||
"location_dest_id": self.location_id.id,
|
||||
"move_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product_id.id,
|
||||
# same text as origin move or product text in partner lang
|
||||
"name": self.move_id.name
|
||||
or self.product_id.with_context(
|
||||
lang=self.partner_id.lang or "en_US"
|
||||
).display_name,
|
||||
"location_id": self.partner_shipping_id.property_stock_customer.id,
|
||||
"location_dest_id": self.location_id.id,
|
||||
"product_uom_qty": self.product_uom_qty,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
# Extract business methods
|
||||
def extract_quantity(self, qty, uom):
|
||||
self.ensure_one()
|
||||
@@ -1133,44 +1088,100 @@ class Rma(models.Model):
|
||||
"rma_id": self.id,
|
||||
}
|
||||
|
||||
# Returning business methods
|
||||
def create_return(self, scheduled_date, qty=None, uom=None):
|
||||
"""Intended to be invoked by the delivery wizard"""
|
||||
def _delivery_should_be_grouped(self):
|
||||
"""Checks if the rmas should be grouped for the delivery process"""
|
||||
group_returns = self.env.company.rma_return_grouping
|
||||
if "rma_return_grouping" in self.env.context:
|
||||
group_returns = self.env.context.get("rma_return_grouping")
|
||||
return group_returns
|
||||
|
||||
def _delivery_group_key(self):
|
||||
"""Returns a key by which the rmas should be grouped for the delivery process"""
|
||||
self.ensure_one()
|
||||
return (self.partner_shipping_id.id, self.company_id.id, self.warehouse_id.id)
|
||||
|
||||
def _group_delivery_if_needed(self):
|
||||
"""Groups the given rmas by the returned key from _delivery_group_key
|
||||
by setting the procurement_group_id on the each rma if there is not yet on set"""
|
||||
if not self._delivery_should_be_grouped():
|
||||
return
|
||||
grouped_rmas = groupby(
|
||||
sorted(self, key=lambda rma: rma._delivery_group_key()),
|
||||
key=lambda rma: [rma._delivery_group_key()],
|
||||
)
|
||||
for _group, rmas in grouped_rmas:
|
||||
rmas = (
|
||||
self.browse()
|
||||
.concat(*list(rmas))
|
||||
.filtered(lambda rma: not rma.procurement_group_id)
|
||||
)
|
||||
if not rmas:
|
||||
continue
|
||||
proc_group = self.env["procurement.group"].create(
|
||||
rmas._prepare_procurement_group_vals()
|
||||
)
|
||||
rmas.write({"procurement_group_id": proc_group.id})
|
||||
|
||||
def _prepare_delivery_procurement_vals(self, scheduled_date=None):
|
||||
"""This method is used only for Delivery (not replace). It is important to set
|
||||
RMA Out route."""
|
||||
vals = self._prepare_common_procurement_vals(scheduled_date=scheduled_date)
|
||||
vals["rma_id"] = self.id
|
||||
vals["route_ids"] = self.warehouse_id.rma_out_route_id
|
||||
vals["move_orig_ids"] = [(6, 0, self.reception_move_id.ids)]
|
||||
return vals
|
||||
|
||||
def _prepare_delivery_procurements(self, scheduled_date=None, qty=None, uom=None):
|
||||
self._group_delivery_if_needed()
|
||||
procurements = []
|
||||
group_model = self.env["procurement.group"]
|
||||
for rma in self:
|
||||
if not rma.procurement_group_id:
|
||||
rma.procurement_group_id = group_model.create(
|
||||
rma._prepare_procurement_group_vals()
|
||||
)
|
||||
|
||||
vals = rma._prepare_delivery_procurement_vals(scheduled_date)
|
||||
group = vals.get("group_id")
|
||||
procurements.append(
|
||||
group_model.Procurement(
|
||||
rma.product_id,
|
||||
qty or rma.product_uom_qty,
|
||||
uom or rma.product_uom,
|
||||
rma.partner_shipping_id.property_stock_customer,
|
||||
rma.product_id.display_name,
|
||||
group.name,
|
||||
rma.company_id,
|
||||
vals,
|
||||
)
|
||||
)
|
||||
return procurements
|
||||
|
||||
# Returning business methods
|
||||
def create_return(self, scheduled_date, qty=None, uom=None):
|
||||
"""Intended to be invoked by the delivery wizard"""
|
||||
self._ensure_can_be_returned()
|
||||
self._ensure_qty_to_return(qty, uom)
|
||||
group_dict = {}
|
||||
rmas_to_return = self.filtered("can_be_returned")
|
||||
for record in rmas_to_return:
|
||||
key = (
|
||||
record.partner_shipping_id.id,
|
||||
record.company_id.id,
|
||||
record.warehouse_id,
|
||||
rmas_to_return = self.filtered(
|
||||
lambda rma: rma.can_be_returned and rma._product_is_storable()
|
||||
)
|
||||
procurements = rmas_to_return._prepare_delivery_procurements(
|
||||
scheduled_date, qty, uom
|
||||
)
|
||||
if procurements:
|
||||
self.env["procurement.group"].run(procurements)
|
||||
pickings = defaultdict(lambda: self.browse())
|
||||
for rma in rmas_to_return:
|
||||
picking = rma.delivery_move_ids.picking_id.sorted("id", reverse=True)[0]
|
||||
pickings[picking] |= rma
|
||||
rma.message_post(
|
||||
body=_(
|
||||
'Return: <a href="#" data-oe-model="stock.picking" '
|
||||
'data-oe-id="%(id)d">%(name)s</a> has been created.'
|
||||
)
|
||||
% ({"id": picking.id, "name": picking.name})
|
||||
)
|
||||
group_dict.setdefault(key, self.env["rma"])
|
||||
group_dict[key] |= record
|
||||
if group_returns:
|
||||
grouped_rmas = group_dict.values()
|
||||
else:
|
||||
grouped_rmas = rmas_to_return
|
||||
for rmas in grouped_rmas:
|
||||
origin = ", ".join(rmas.mapped("name"))
|
||||
picking_vals = rmas[0]._prepare_returning_picking_vals(origin)
|
||||
for rma in rmas:
|
||||
picking_vals["move_ids"].append(
|
||||
(0, 0, rma._prepare_returning_move_vals(scheduled_date, qty, uom))
|
||||
)
|
||||
picking = self.env["stock.picking"].create(picking_vals)
|
||||
for rma in rmas:
|
||||
rma.message_post(
|
||||
body=_(
|
||||
'Return: <a href="#" data-oe-model="stock.picking" '
|
||||
'data-oe-id="%(id)d">%(name)s</a> has been created.'
|
||||
)
|
||||
% ({"id": picking.id, "name": picking.name})
|
||||
)
|
||||
for picking, rmas in pickings.items():
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
picking.message_post_with_view(
|
||||
@@ -1180,42 +1191,53 @@ class Rma(models.Model):
|
||||
)
|
||||
rmas_to_return.write({"state": "waiting_return"})
|
||||
|
||||
def _prepare_returning_picking_vals(self, origin=None):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"picking_type_id": self.warehouse_id.rma_out_type_id.id,
|
||||
"location_id": self.location_id.id,
|
||||
"location_dest_id": self.reception_move_id.location_id.id,
|
||||
"origin": origin or self.name,
|
||||
"partner_id": self.partner_shipping_id.id,
|
||||
"company_id": self.company_id.id,
|
||||
"move_ids": [],
|
||||
}
|
||||
def _prepare_replace_procurement_vals(self, warehouse=None, scheduled_date=None):
|
||||
"""This method is used only for Replace (not Delivery). We do not use any
|
||||
specific route here."""
|
||||
vals = self._prepare_common_procurement_vals(warehouse, scheduled_date)
|
||||
vals["rma_id"] = self.id
|
||||
return vals
|
||||
|
||||
def _prepare_returning_move_vals(self, scheduled_date, quantity=None, uom=None):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"product_id": self.product_id.id,
|
||||
"name": self.product_id.with_context(
|
||||
lang=self.partner_shipping_id.lang or "en_US"
|
||||
).display_name,
|
||||
"product_uom_qty": quantity or self.product_uom_qty,
|
||||
"product_uom": uom and uom.id or self.product_uom.id,
|
||||
"location_id": self.location_id.id,
|
||||
"location_dest_id": self.reception_move_id.location_id.id,
|
||||
"date": scheduled_date,
|
||||
"rma_id": self.id,
|
||||
"move_orig_ids": [(4, self.reception_move_id.id)],
|
||||
"company_id": self.company_id.id,
|
||||
}
|
||||
def _prepare_replace_procurements(
|
||||
self, warehouse, scheduled_date, product, qty, uom
|
||||
):
|
||||
procurements = []
|
||||
group_model = self.env["procurement.group"]
|
||||
for rma in self:
|
||||
if not rma._product_is_storable(product):
|
||||
continue
|
||||
|
||||
if not rma.procurement_group_id:
|
||||
rma.procurement_group_id = group_model.create(
|
||||
rma._prepare_procurement_group_vals()
|
||||
)
|
||||
|
||||
vals = rma._prepare_replace_procurement_vals(warehouse, scheduled_date)
|
||||
group = vals.get("group_id")
|
||||
procurements.append(
|
||||
group_model.Procurement(
|
||||
product,
|
||||
qty,
|
||||
uom,
|
||||
rma.partner_shipping_id.property_stock_customer,
|
||||
product.display_name,
|
||||
group.name,
|
||||
rma.company_id,
|
||||
vals,
|
||||
)
|
||||
)
|
||||
return procurements
|
||||
|
||||
# Replacing business methods
|
||||
def create_replace(self, scheduled_date, warehouse, product, qty, uom):
|
||||
"""Intended to be invoked by the delivery wizard"""
|
||||
self.ensure_one()
|
||||
self._ensure_can_be_replaced()
|
||||
moves_before = self.delivery_move_ids
|
||||
self._action_launch_stock_rule(scheduled_date, warehouse, product, qty, uom)
|
||||
procurements = self._prepare_replace_procurements(
|
||||
warehouse, scheduled_date, product, qty, uom
|
||||
)
|
||||
if procurements:
|
||||
self.env["procurement.group"].run(procurements)
|
||||
new_moves = self.delivery_move_ids - moves_before
|
||||
body = ""
|
||||
# The product replacement could explode into several moves like in the case of
|
||||
@@ -1239,6 +1261,12 @@ class Rma(models.Model):
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
for rma in self:
|
||||
rma._add_replace_message(body, qty, uom)
|
||||
self.write({"state": "waiting_replacement"})
|
||||
|
||||
def _add_replace_message(self, body, qty, uom):
|
||||
self.ensure_one()
|
||||
self.message_post(
|
||||
body=body
|
||||
or _(
|
||||
@@ -1251,74 +1279,13 @@ class Rma(models.Model):
|
||||
)
|
||||
% (
|
||||
{
|
||||
"id": product.id,
|
||||
"name": product.display_name,
|
||||
"id": self.id,
|
||||
"name": self.display_name,
|
||||
"qty": qty,
|
||||
"uom": uom.name,
|
||||
}
|
||||
)
|
||||
)
|
||||
if self.state != "waiting_replacement":
|
||||
self.state = "waiting_replacement"
|
||||
|
||||
def _action_launch_stock_rule(
|
||||
self,
|
||||
scheduled_date,
|
||||
warehouse,
|
||||
product,
|
||||
qty,
|
||||
uom,
|
||||
):
|
||||
"""Creates a delivery picking and launch stock rule. It is invoked by:
|
||||
rma.create_replace
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.product_id.type not in ("consu", "product"):
|
||||
return
|
||||
if not self.procurement_group_id:
|
||||
self.procurement_group_id = (
|
||||
self.env["procurement.group"]
|
||||
.create(
|
||||
{
|
||||
"name": self.name,
|
||||
"move_type": "direct",
|
||||
"partner_id": self.partner_shipping_id.id,
|
||||
}
|
||||
)
|
||||
.id
|
||||
)
|
||||
values = self._prepare_procurement_values(
|
||||
self.procurement_group_id, scheduled_date, warehouse
|
||||
)
|
||||
procurement = self.env["procurement.group"].Procurement(
|
||||
product,
|
||||
qty,
|
||||
uom,
|
||||
self.partner_shipping_id.property_stock_customer,
|
||||
self.product_id.display_name,
|
||||
self.procurement_group_id.name,
|
||||
self.company_id,
|
||||
values,
|
||||
)
|
||||
self.env["procurement.group"].run([procurement])
|
||||
return True
|
||||
|
||||
def _prepare_procurement_values(
|
||||
self,
|
||||
group_id,
|
||||
scheduled_date,
|
||||
warehouse,
|
||||
):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"company_id": self.company_id,
|
||||
"group_id": group_id,
|
||||
"date_planned": scheduled_date,
|
||||
"warehouse_id": warehouse,
|
||||
"partner_id": self.partner_shipping_id.id,
|
||||
"rma_id": self.id,
|
||||
"priority": self.priority,
|
||||
}
|
||||
|
||||
# Mail business methods
|
||||
def _creation_subtype(self):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
||||
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
@@ -22,7 +23,7 @@ class StockMove(models.Model):
|
||||
string="RMA receivers",
|
||||
copy=False,
|
||||
)
|
||||
# RMA that create the delivery movement to the customer
|
||||
# RMA that creates the out move
|
||||
rma_id = fields.Many2one(
|
||||
comodel_name="rma",
|
||||
string="RMA return",
|
||||
@@ -32,8 +33,8 @@ class StockMove(models.Model):
|
||||
def unlink(self):
|
||||
# A stock user could have no RMA permissions, so the ids wouldn't
|
||||
# be accessible due to record rules.
|
||||
rma_receiver = self.sudo().mapped("rma_receiver_ids")
|
||||
rma = self.sudo().mapped("rma_id")
|
||||
rma_receiver = self.sudo().rma_receiver_ids
|
||||
rma = self.sudo().rma_id
|
||||
res = super().unlink()
|
||||
rma_receiver.filtered(lambda x: x.state != "cancelled").write(
|
||||
{"state": "draft"}
|
||||
@@ -103,38 +104,22 @@ class StockMove(models.Model):
|
||||
res["rma_id"] = self.sudo().rma_id.id
|
||||
return res
|
||||
|
||||
def _prepare_return_rma_vals(self, original_picking):
|
||||
"""hook method for preparing an RMA from the 'return picking wizard'."""
|
||||
self.ensure_one()
|
||||
partner = original_picking.partner_id
|
||||
if hasattr(original_picking, "sale_id") and original_picking.sale_id:
|
||||
partner_invoice_id = original_picking.sale_id.partner_invoice_id.id
|
||||
partner_shipping_id = original_picking.sale_id.partner_shipping_id.id
|
||||
else:
|
||||
partner_invoice_id = partner.address_get(["invoice"]).get("invoice", False)
|
||||
partner_shipping_id = partner.address_get(["delivery"]).get(
|
||||
"delivery", False
|
||||
)
|
||||
return {
|
||||
"user_id": self.env.user.id,
|
||||
"partner_id": partner.id,
|
||||
"partner_shipping_id": partner_shipping_id,
|
||||
"partner_invoice_id": partner_invoice_id,
|
||||
"origin": original_picking.name,
|
||||
"picking_id": original_picking.id,
|
||||
"move_id": self.origin_returned_move_id.id,
|
||||
"product_id": self.origin_returned_move_id.product_id.id,
|
||||
"product_uom_qty": self.product_uom_qty,
|
||||
"product_uom": self.product_uom.id,
|
||||
"reception_move_id": self.id,
|
||||
"company_id": self.company_id.id,
|
||||
"location_id": self.location_dest_id.id,
|
||||
"state": "confirmed",
|
||||
}
|
||||
def _prepare_procurement_values(self):
|
||||
res = super()._prepare_procurement_values()
|
||||
if self.rma_id:
|
||||
res["rma_id"] = self.rma_id.id
|
||||
return res
|
||||
|
||||
|
||||
class StockRule(models.Model):
|
||||
_inherit = "stock.rule"
|
||||
|
||||
def _get_custom_move_fields(self):
|
||||
return super()._get_custom_move_fields() + ["rma_id"]
|
||||
move_fields = super()._get_custom_move_fields()
|
||||
move_fields += [
|
||||
"rma_id",
|
||||
"origin_returned_move_id",
|
||||
"move_orig_ids",
|
||||
"rma_receiver_ids",
|
||||
]
|
||||
return move_fields
|
||||
|
||||
@@ -30,8 +30,8 @@ class StockPicking(models.Model):
|
||||
|
||||
def action_view_rma(self):
|
||||
self.ensure_one()
|
||||
action = self.sudo().env.ref("rma.rma_action").read()[0]
|
||||
rma = self.move_ids.mapped("rma_ids")
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id("rma.rma_action")
|
||||
rma = self.move_lines.mapped("rma_ids")
|
||||
if len(rma) == 1:
|
||||
action.update(
|
||||
res_id=rma.id,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
||||
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo import _, fields, models
|
||||
|
||||
|
||||
class StockWarehouse(models.Model):
|
||||
@@ -27,35 +28,39 @@ class StockWarehouse(models.Model):
|
||||
comodel_name="stock.location",
|
||||
string="RMA Location",
|
||||
)
|
||||
rma_in_route_id = fields.Many2one("stock.route", "RMA in Route")
|
||||
rma_out_route_id = fields.Many2one("stock.route", "RMA out Route")
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""To create an RMA location and link it with a new warehouse,
|
||||
this method is overridden instead of '_get_locations_values'
|
||||
method because the locations that are created with the
|
||||
values returned by that method are forced to be children
|
||||
of view_location_id, and we don't want that.
|
||||
"""
|
||||
res = super().create(vals_list)
|
||||
stock_location = self.env["stock.location"]
|
||||
for record in res:
|
||||
rma_location_vals = record._get_rma_location_values()
|
||||
record.rma_loc_id = stock_location.create(rma_location_vals).id
|
||||
return res
|
||||
|
||||
def _get_rma_location_values(self):
|
||||
def _get_rma_location_values(self, vals, code=False):
|
||||
"""this method is intended to be used by 'create' method
|
||||
to create a new RMA location to be linked to a new warehouse.
|
||||
"""
|
||||
company_id = vals.get(
|
||||
"company_id", self.default_get(["company_id"])["company_id"]
|
||||
)
|
||||
code = vals.get("code") or code or ""
|
||||
code = code.replace(" ", "").upper()
|
||||
view_location_id = vals.get("view_location_id")
|
||||
view_location = (
|
||||
view_location_id
|
||||
and self.view_location_id.browse(view_location_id)
|
||||
or self.view_location_id
|
||||
)
|
||||
return {
|
||||
"name": self.view_location_id.name,
|
||||
"name": view_location.name,
|
||||
"active": True,
|
||||
"return_location": True,
|
||||
"usage": "internal",
|
||||
"company_id": self.company_id.id,
|
||||
"company_id": company_id,
|
||||
"location_id": self.env.ref("rma.stock_location_rma").id,
|
||||
"barcode": self._valid_barcode(code + "-RMA", company_id),
|
||||
}
|
||||
|
||||
def _get_locations_values(self, vals, code=False):
|
||||
res = super()._get_locations_values(vals, code)
|
||||
res["rma_loc_id"] = self._get_rma_location_values(vals, code)
|
||||
return res
|
||||
|
||||
def _get_sequence_values(self, name=False, code=False):
|
||||
values = super()._get_sequence_values(name=name, code=code)
|
||||
values.update(
|
||||
@@ -77,12 +82,14 @@ class StockWarehouse(models.Model):
|
||||
return values
|
||||
|
||||
def _update_name_and_code(self, new_name=False, new_code=False):
|
||||
res = super()._update_name_and_code(new_name, new_code)
|
||||
for warehouse in self:
|
||||
sequence_data = warehouse._get_sequence_values()
|
||||
warehouse.rma_in_type_id.sequence_id.write(sequence_data["rma_in_type_id"])
|
||||
warehouse.rma_out_type_id.sequence_id.write(
|
||||
sequence_data["rma_out_type_id"]
|
||||
)
|
||||
return res
|
||||
|
||||
def _get_picking_type_create_values(self, max_sequence):
|
||||
data, next_sequence = super()._get_picking_type_create_values(max_sequence)
|
||||
@@ -138,3 +145,70 @@ class StockWarehouse(models.Model):
|
||||
{"return_picking_type_id": data.get("rma_out_type_id", False)}
|
||||
)
|
||||
return data
|
||||
|
||||
def _get_routes_values(self):
|
||||
res = super()._get_routes_values()
|
||||
rma_routes = {
|
||||
"rma_in_route_id": {
|
||||
"routing_key": "rma_in",
|
||||
"depends": ["active"],
|
||||
"route_update_values": {
|
||||
"name": self._format_routename("RMA In"),
|
||||
"active": self.active,
|
||||
},
|
||||
"route_create_values": {
|
||||
"warehouse_selectable": True,
|
||||
"company_id": self.company_id.id,
|
||||
"sequence": 100,
|
||||
},
|
||||
"rules_values": {
|
||||
"active": True,
|
||||
},
|
||||
},
|
||||
"rma_out_route_id": {
|
||||
"routing_key": "rma_out",
|
||||
"depends": ["active"],
|
||||
"route_update_values": {
|
||||
"name": self._format_routename("RMA Out"),
|
||||
"active": self.active,
|
||||
},
|
||||
"route_create_values": {
|
||||
"warehouse_selectable": True,
|
||||
"company_id": self.company_id.id,
|
||||
"sequence": 110,
|
||||
},
|
||||
"rules_values": {
|
||||
"active": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
if self.env.context.get("rma_post_init_hook"):
|
||||
return rma_routes
|
||||
res.update(rma_routes)
|
||||
return res
|
||||
|
||||
def get_rules_dict(self):
|
||||
res = super().get_rules_dict()
|
||||
customer_loc, supplier_loc = self._get_partner_locations()
|
||||
for warehouse in self:
|
||||
res[warehouse.id].update(
|
||||
{
|
||||
"rma_in": [
|
||||
self.Routing(
|
||||
customer_loc,
|
||||
warehouse.rma_loc_id,
|
||||
warehouse.rma_in_type_id,
|
||||
"pull",
|
||||
)
|
||||
],
|
||||
"rma_out": [
|
||||
self.Routing(
|
||||
warehouse.rma_loc_id,
|
||||
customer_loc,
|
||||
warehouse.rma_out_type_id,
|
||||
"pull",
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
return res
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
|
||||
* Chafique Delli <chafique.delli@akretion.com>
|
||||
* Giovanni Serra - Ooops <giovanni@ooops404.com>
|
||||
* Michael Tietz (MT Software) <mtietz@mt-software.de>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
* As soon as the picking is selected, the user should select the move,
|
||||
but perhaps stock.move _rec_name could be improved to better show what
|
||||
the product of that move is.
|
||||
* Add RMA reception and/or RMA delivery on several steps - 2 or 3 - like
|
||||
normal receptions/deliveries. It should be a separate option inside the
|
||||
warehouse definition.
|
||||
|
||||
@@ -488,6 +488,9 @@ will be the default one if no team is set.</li>
|
||||
<li>As soon as the picking is selected, the user should select the move,
|
||||
but perhaps stock.move _rec_name could be improved to better show what
|
||||
the product of that move is.</li>
|
||||
<li>Add RMA reception and/or RMA delivery on several steps - 2 or 3 - like
|
||||
normal receptions/deliveries. It should be a separate option inside the
|
||||
warehouse definition.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
@@ -518,6 +521,7 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
|
||||
</li>
|
||||
<li>Chafique Delli <<a class="reference external" href="mailto:chafique.delli@akretion.com">chafique.delli@akretion.com</a>></li>
|
||||
<li>Giovanni Serra - Ooops <<a class="reference external" href="mailto:giovanni@ooops404.com">giovanni@ooops404.com</a>></li>
|
||||
<li>Michael Tietz (MT Software) <<a class="reference external" href="mailto:mtietz@mt-software.de">mtietz@mt-software.de</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
||||
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tests import Form, TransactionCase, new_test_user, users
|
||||
|
||||
from .. import hooks
|
||||
|
||||
|
||||
class TestRma(TransactionCase):
|
||||
@classmethod
|
||||
@@ -72,6 +75,8 @@ class TestRma(TransactionCase):
|
||||
{"name": "[Test] It's out of warranty. To be scrapped"}
|
||||
)
|
||||
cls.env.ref("rma.group_rma_manual_finalization").users |= cls.env.user
|
||||
cls.env.ref("stock.group_stock_multi_locations").users |= cls.env.user
|
||||
cls.warehouse = cls.env.ref("stock.warehouse0")
|
||||
# Ensure grouping
|
||||
cls.env.company.rma_return_grouping = True
|
||||
|
||||
@@ -130,6 +135,49 @@ class TestRma(TransactionCase):
|
||||
|
||||
|
||||
class TestRmaCase(TestRma):
|
||||
def test_post_init_hook(self):
|
||||
warehouse = self.env["stock.warehouse"].create(
|
||||
{
|
||||
"name": "Test warehouse",
|
||||
"code": "code",
|
||||
"company_id": self.env.company.id,
|
||||
}
|
||||
)
|
||||
hooks.post_init_hook(self.env.cr, self.registry)
|
||||
self.assertTrue(warehouse.rma_in_type_id)
|
||||
self.assertEqual(
|
||||
warehouse.rma_in_type_id.default_location_dest_id, warehouse.rma_loc_id
|
||||
)
|
||||
self.assertEqual(
|
||||
warehouse.rma_out_type_id.default_location_src_id, warehouse.rma_loc_id
|
||||
)
|
||||
self.assertTrue(warehouse.rma_loc_id)
|
||||
self.assertTrue(warehouse.rma_in_route_id)
|
||||
self.assertTrue(warehouse.rma_out_route_id)
|
||||
|
||||
def test_rma_replace_pick_ship(self):
|
||||
self.warehouse.write({"delivery_steps": "pick_ship"})
|
||||
rma = self._create_rma(self.partner, self.product, 1, self.rma_loc)
|
||||
rma.action_confirm()
|
||||
rma.reception_move_id.quantity_done = 1
|
||||
rma.reception_move_id.picking_id._action_done()
|
||||
self.assertEqual(rma.reception_move_id.picking_id.state, "done")
|
||||
self.assertEqual(rma.state, "received")
|
||||
res = rma.action_replace()
|
||||
wizard_form = Form(self.env[res["res_model"]].with_context(**res["context"]))
|
||||
wizard_form.product_id = self.product
|
||||
wizard_form.product_uom_qty = rma.product_uom_qty
|
||||
wizard = wizard_form.save()
|
||||
wizard.action_deliver()
|
||||
self.assertEqual(rma.delivery_picking_count, 2)
|
||||
out_pickings = rma.mapped("delivery_move_ids.picking_id")
|
||||
self.assertIn(
|
||||
self.warehouse.pick_type_id, out_pickings.mapped("picking_type_id")
|
||||
)
|
||||
self.assertIn(
|
||||
self.warehouse.out_type_id, out_pickings.mapped("picking_type_id")
|
||||
)
|
||||
|
||||
def test_computed(self):
|
||||
# If partner changes, the invoice address is set
|
||||
rma = self.env["rma"].new()
|
||||
@@ -331,7 +379,7 @@ class TestRmaCase(TestRma):
|
||||
# line of refund_1
|
||||
self.assertEqual(len(refund_1.invoice_line_ids), 3)
|
||||
self.assertEqual(
|
||||
refund_1.invoice_line_ids.mapped("rma_id"),
|
||||
refund_1.invoice_line_ids.rma_id,
|
||||
(rma_1 | rma_2 | rma_3),
|
||||
)
|
||||
self.assertEqual(
|
||||
@@ -538,6 +586,13 @@ class TestRmaCase(TestRma):
|
||||
all_rmas = rma_1 | rma_2 | rma_3 | rma_4
|
||||
self.assertEqual(all_rmas.mapped("state"), ["received"] * 4)
|
||||
self.assertEqual(all_rmas.mapped("can_be_returned"), [True] * 4)
|
||||
all_in_pickings = all_rmas.mapped("reception_move_id.picking_id")
|
||||
self.assertEqual(
|
||||
all_in_pickings.mapped("picking_type_id"), self.warehouse.rma_in_type_id
|
||||
)
|
||||
self.assertEqual(
|
||||
all_in_pickings.mapped("location_dest_id"), self.warehouse.rma_loc_id
|
||||
)
|
||||
# Mass return of those four RMAs
|
||||
delivery_wizard = (
|
||||
self.env["rma.delivery.wizard"]
|
||||
@@ -548,6 +603,10 @@ class TestRmaCase(TestRma):
|
||||
# Two pickings were created
|
||||
pick_1 = (rma_1 | rma_2 | rma_3).mapped("delivery_move_ids.picking_id")
|
||||
pick_2 = rma_4.delivery_move_ids.picking_id
|
||||
self.assertEqual(pick_1.picking_type_id, self.warehouse.rma_out_type_id)
|
||||
self.assertEqual(pick_1.location_id, self.warehouse.rma_loc_id)
|
||||
self.assertEqual(pick_2.picking_type_id, self.warehouse.rma_out_type_id)
|
||||
self.assertEqual(pick_2.location_id, self.warehouse.rma_loc_id)
|
||||
self.assertEqual(len(pick_1), 1)
|
||||
self.assertEqual(len(pick_2), 1)
|
||||
self.assertNotEqual(pick_1, pick_2)
|
||||
@@ -563,7 +622,7 @@ class TestRmaCase(TestRma):
|
||||
# line of picking_1
|
||||
self.assertEqual(len(pick_1.move_ids), 3)
|
||||
self.assertEqual(
|
||||
pick_1.move_ids.mapped("rma_id"),
|
||||
pick_1.move_ids.rma_id,
|
||||
(rma_1 | rma_2 | rma_3),
|
||||
)
|
||||
self.assertEqual(
|
||||
@@ -634,14 +693,14 @@ class TestRmaCase(TestRma):
|
||||
origin_moves = origin_delivery.move_ids
|
||||
self.assertTrue(origin_moves[0].rma_ids)
|
||||
self.assertTrue(origin_moves[1].rma_ids)
|
||||
rmas = origin_moves.mapped("rma_ids")
|
||||
rmas = origin_moves.rma_ids
|
||||
self.assertEqual(rmas.mapped("state"), ["confirmed"] * 2)
|
||||
# Each reception move is linked one of the generated RMAs
|
||||
reception = self.env["stock.picking"].browse(picking_action["res_id"])
|
||||
reception_moves = reception.move_ids
|
||||
self.assertTrue(reception_moves[0].rma_receiver_ids)
|
||||
self.assertTrue(reception_moves[1].rma_receiver_ids)
|
||||
self.assertEqual(reception_moves.mapped("rma_receiver_ids"), rmas)
|
||||
self.assertEqual(reception_moves.rma_receiver_ids, rmas)
|
||||
# Validate the reception picking to set rmas to 'received' state
|
||||
reception_moves[0].quantity_done = reception_moves[0].product_uom_qty
|
||||
reception_moves[1].quantity_done = reception_moves[1].product_uom_qty
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
||||
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from copy import deepcopy
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import float_compare
|
||||
|
||||
|
||||
class ReturnPickingLine(models.TransientModel):
|
||||
_inherit = "stock.return.picking.line"
|
||||
|
||||
def _prepare_rma_vals(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"move_id": self.move_id.id,
|
||||
"product_id": self.move_id.product_id.id,
|
||||
"product_uom_qty": self.quantity,
|
||||
"product_uom": self.product_id.uom_id.id,
|
||||
"location_id": self.wizard_id.location_id.id or self.move_id.location_id.id,
|
||||
}
|
||||
|
||||
|
||||
class ReturnPicking(models.TransientModel):
|
||||
@@ -48,6 +65,52 @@ class ReturnPicking(models.TransientModel):
|
||||
location_id = return_picking_type.default_location_dest_id.id
|
||||
self.location_id = location_id
|
||||
|
||||
def _prepare_rma_partner_values(self):
|
||||
self.ensure_one()
|
||||
partner = self.picking_id.partner_id
|
||||
partner_address = partner.address_get(["invoice", "delivery"])
|
||||
partner_invoice_id = partner_address.get("invoice", False)
|
||||
partner_shipping_id = partner_address.get("delivery", False)
|
||||
return (
|
||||
partner,
|
||||
partner_invoice_id and partner.browse(partner_invoice_id) or partner,
|
||||
partner_shipping_id and partner.browse(partner_shipping_id) or partner,
|
||||
)
|
||||
|
||||
def _prepare_rma_vals(self):
|
||||
partner, partner_invoice, partner_shipping = self._prepare_rma_partner_values()
|
||||
origin = self.picking_id.name
|
||||
vals = self.env["rma"]._prepare_procurement_group_vals()
|
||||
vals["partner_id"] = partner_shipping.id
|
||||
vals["name"] = origin
|
||||
group = self.env["procurement.group"].create(vals)
|
||||
return {
|
||||
"user_id": self.env.user.id,
|
||||
"partner_id": partner.id,
|
||||
"partner_shipping_id": partner_shipping.id,
|
||||
"partner_invoice_id": partner_invoice.id,
|
||||
"origin": origin,
|
||||
"picking_id": self.picking_id.id,
|
||||
"company_id": self.company_id.id,
|
||||
"procurement_group_id": group.id,
|
||||
}
|
||||
|
||||
def _prepare_rma_vals_list(self):
|
||||
vals_list = []
|
||||
for return_picking in self:
|
||||
global_vals = return_picking._prepare_rma_vals()
|
||||
for line in return_picking.product_return_moves:
|
||||
if (
|
||||
not line.move_id
|
||||
or float_compare(line.quantity, 0, line.product_id.uom_id.rounding)
|
||||
<= 0
|
||||
):
|
||||
continue
|
||||
vals = deepcopy(global_vals)
|
||||
vals.update(line._prepare_rma_vals())
|
||||
vals_list.append(vals)
|
||||
return vals_list
|
||||
|
||||
def create_returns(self):
|
||||
"""Override create_returns method for creating one or more
|
||||
'confirmed' RMAs after return a delivery picking in case
|
||||
@@ -57,10 +120,6 @@ class ReturnPicking(models.TransientModel):
|
||||
as the 'Receipt'.
|
||||
"""
|
||||
if self.create_rma:
|
||||
# set_rma_picking_type is to override the copy() method of stock
|
||||
# picking and change the default picking type to rma picking type
|
||||
self_with_context = self.with_context(set_rma_picking_type=True)
|
||||
res = super(ReturnPicking, self_with_context).create_returns()
|
||||
if not self.picking_id.partner_id:
|
||||
raise ValidationError(
|
||||
_(
|
||||
@@ -68,12 +127,30 @@ class ReturnPicking(models.TransientModel):
|
||||
"'Stock Picking' from which RMAs will be created"
|
||||
)
|
||||
)
|
||||
returned_picking = self.env["stock.picking"].browse(res["res_id"])
|
||||
vals_list = [
|
||||
move._prepare_return_rma_vals(self.picking_id)
|
||||
for move in returned_picking.move_ids
|
||||
]
|
||||
self.env["rma"].create(vals_list)
|
||||
return res
|
||||
else:
|
||||
return super().create_returns()
|
||||
vals_list = self._prepare_rma_vals_list()
|
||||
rmas = self.env["rma"].create(vals_list)
|
||||
rmas.action_confirm()
|
||||
picking = rmas.reception_move_id.picking_id
|
||||
picking = picking and picking[0] or picking
|
||||
ctx = dict(self.env.context)
|
||||
ctx.update(
|
||||
{
|
||||
"default_partner_id": picking.partner_id.id,
|
||||
"search_default_picking_type_id": picking.picking_type_id.id,
|
||||
"search_default_draft": False,
|
||||
"search_default_assigned": False,
|
||||
"search_default_confirmed": False,
|
||||
"search_default_ready": False,
|
||||
"search_default_planning_issues": False,
|
||||
"search_default_available": False,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"name": _("Returned Picking"),
|
||||
"view_mode": "form,tree,calendar",
|
||||
"res_model": "stock.picking",
|
||||
"res_id": picking.id,
|
||||
"type": "ir.actions.act_window",
|
||||
"context": ctx,
|
||||
}
|
||||
return super().create_returns()
|
||||
|
||||
@@ -4,4 +4,3 @@ from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import rma
|
||||
from . import sale
|
||||
from . import stock_move
|
||||
|
||||
@@ -163,3 +163,15 @@ class Rma(models.Model):
|
||||
):
|
||||
vals["sale_line_ids"] = [(4, line.id)]
|
||||
return vals
|
||||
|
||||
def _prepare_procurement_group_vals(self):
|
||||
vals = super()._prepare_procurement_group_vals()
|
||||
if not self.env.context.get("ignore_rma_sale_order") and self.order_id:
|
||||
vals["sale_id"] = self.order_id.id
|
||||
return vals
|
||||
|
||||
def _prepare_delivery_procurements(self, scheduled_date=None, qty=None, uom=None):
|
||||
self = self.with_context(ignore_rma_sale_order=True)
|
||||
return super()._prepare_delivery_procurements(
|
||||
scheduled_date=scheduled_date, qty=qty, uom=uom
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
||||
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
@@ -118,10 +119,23 @@ class SaleOrderLine(models.Model):
|
||||
self.ensure_one()
|
||||
# Method helper to filter chained moves
|
||||
|
||||
def destination_moves(_move):
|
||||
return _move.mapped("move_dest_ids").filtered(
|
||||
def _get_chained_moves(_moves, done_moves=None):
|
||||
moves = _moves.browse()
|
||||
done_moves = done_moves or _moves.browse()
|
||||
for move in _moves:
|
||||
if move.location_dest_id.usage == "customer":
|
||||
moves |= move.returned_move_ids
|
||||
else:
|
||||
moves |= move.move_dest_ids
|
||||
done_moves |= _moves
|
||||
moves = moves.filtered(
|
||||
lambda r: r.state in ["partially_available", "assigned", "done"]
|
||||
)
|
||||
if not moves:
|
||||
return moves
|
||||
moves -= done_moves
|
||||
moves |= _get_chained_moves(moves, done_moves)
|
||||
return moves
|
||||
|
||||
product = self.product_id
|
||||
if self.product_id.type not in ["product", "consu"]:
|
||||
@@ -134,21 +148,13 @@ class SaleOrderLine(models.Model):
|
||||
# to return. When a product is re-delivered it should be
|
||||
# allowed to open an RMA again on it.
|
||||
qty = move.product_uom_qty
|
||||
qty_returned = 0
|
||||
move_dest = destination_moves(move)
|
||||
# With the return of the return of the return we could have an
|
||||
# infinite loop, so we should avoid it dropping already explored
|
||||
# move_dest_ids
|
||||
visited_moves = move + move_dest
|
||||
while move_dest:
|
||||
qty_returned -= sum(move_dest.mapped("product_uom_qty"))
|
||||
move_dest = destination_moves(move_dest) - visited_moves
|
||||
if move_dest:
|
||||
visited_moves += move_dest
|
||||
qty += sum(move_dest.mapped("product_uom_qty"))
|
||||
move_dest = destination_moves(move_dest) - visited_moves
|
||||
for _move in _get_chained_moves(move):
|
||||
factor = 1
|
||||
if _move.location_dest_id.usage != "customer":
|
||||
factor = -1
|
||||
qty += factor * _move.product_uom_qty
|
||||
# If by chance we get a negative qty we should ignore it
|
||||
qty = max(0, sum((qty, qty_returned)))
|
||||
qty = max(0, qty)
|
||||
data.append(
|
||||
{
|
||||
"product": move.product_id,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = "stock.move"
|
||||
|
||||
def _prepare_return_rma_vals(self, original_picking):
|
||||
res = super()._prepare_return_rma_vals(original_picking)
|
||||
res.update(order_id=original_picking.sale_id.id)
|
||||
return res
|
||||
@@ -1,3 +1,4 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import sale_order_rma_wizard
|
||||
from . import stock_picking_return
|
||||
|
||||
29
rma_sale/wizard/stock_picking_return.py
Normal file
29
rma_sale/wizard/stock_picking_return.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ReturnPicking(models.TransientModel):
|
||||
_inherit = "stock.return.picking"
|
||||
|
||||
def _prepare_rma_partner_values(self):
|
||||
sale_order = self.picking_id.sale_id
|
||||
if not sale_order:
|
||||
return super()._prepare_rma_partner_values()
|
||||
return (
|
||||
sale_order.partner_id,
|
||||
sale_order.partner_invoice_id,
|
||||
sale_order.partner_shipping_id,
|
||||
)
|
||||
|
||||
def _prepare_rma_values(self):
|
||||
vals = super()._prepare_rma_values()
|
||||
sale_order = self.picking_id.sale_id
|
||||
if sale_order:
|
||||
vals.update(
|
||||
{
|
||||
"order_id": sale_order.id,
|
||||
}
|
||||
)
|
||||
return vals
|
||||
Reference in New Issue
Block a user