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,
|
* 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
|
but perhaps stock.move _rec_name could be improved to better show what
|
||||||
the product of that move is.
|
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
|
Bug Tracker
|
||||||
===========
|
===========
|
||||||
@@ -165,6 +168,7 @@ Contributors
|
|||||||
|
|
||||||
* Chafique Delli <chafique.delli@akretion.com>
|
* Chafique Delli <chafique.delli@akretion.com>
|
||||||
* Giovanni Serra - Ooops <giovanni@ooops404.com>
|
* Giovanni Serra - Ooops <giovanni@ooops404.com>
|
||||||
|
* Michael Tietz (MT Software) <mtietz@mt-software.de>
|
||||||
|
|
||||||
Maintainers
|
Maintainers
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Return Merchandise Authorization Management",
|
"name": "Return Merchandise Authorization Management",
|
||||||
"summary": "Return Merchandise Authorization (RMA)",
|
"summary": "Return Merchandise Authorization (RMA)",
|
||||||
"version": "16.0.1.3.0",
|
"version": "16.0.1.4.0",
|
||||||
"development_status": "Production/Stable",
|
"development_status": "Production/Stable",
|
||||||
"category": "RMA",
|
"category": "RMA",
|
||||||
"website": "https://github.com/OCA/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 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).
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from odoo import SUPERUSER_ID, api
|
from odoo import SUPERUSER_ID, api
|
||||||
@@ -24,7 +25,9 @@ def post_init_hook(cr, registry):
|
|||||||
def create_rma_locations(warehouse):
|
def create_rma_locations(warehouse):
|
||||||
stock_location = env["stock.location"]
|
stock_location = env["stock.location"]
|
||||||
if not warehouse.rma_loc_id:
|
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 = (
|
warehouse.rma_loc_id = (
|
||||||
stock_location.with_context(active_test=False)
|
stock_location.with_context(active_test=False)
|
||||||
.create(rma_location_vals)
|
.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_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
|
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
|
# Create rma locations and picking types
|
||||||
warehouses = env["stock.warehouse"].search([])
|
warehouses = env["stock.warehouse"].search([])
|
||||||
for warehouse in warehouses:
|
for warehouse in warehouses:
|
||||||
create_rma_locations(warehouse)
|
create_rma_locations(warehouse)
|
||||||
create_rma_picking_types(warehouse)
|
create_rma_picking_types(warehouse)
|
||||||
|
create_rma_routes(warehouses)
|
||||||
# Create rma sequence per company
|
# Create rma sequence per company
|
||||||
for company in env["res.company"].search([]):
|
for company in env["res.company"].search([]):
|
||||||
company.create_rma_index()
|
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 2020 Tecnativa - Ernesto Tejeda
|
||||||
# Copyright 2023 Tecnativa - Pedro M. Baeza
|
# 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).
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
import logging
|
import logging
|
||||||
from collections import Counter
|
from collections import defaultdict
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
from odoo import _, api, fields, models
|
||||||
from odoo.exceptions import AccessError, ValidationError
|
from odoo.exceptions import AccessError, ValidationError
|
||||||
from odoo.tests import Form
|
|
||||||
from odoo.tools import html2plaintext
|
from odoo.tools import html2plaintext
|
||||||
|
|
||||||
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
|
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
|
||||||
@@ -317,18 +318,8 @@ class Rma(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _compute_delivery_picking_count(self):
|
def _compute_delivery_picking_count(self):
|
||||||
# It is enough to count the moves to know how many pickings
|
for rma in self:
|
||||||
# there are because there will be a unique move linked to the
|
rma.delivery_picking_count = len(rma.delivery_move_ids.picking_id)
|
||||||
# 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)
|
|
||||||
|
|
||||||
@api.depends(
|
@api.depends(
|
||||||
"delivery_move_ids",
|
"delivery_move_ids",
|
||||||
@@ -670,21 +661,85 @@ class Rma(models.Model):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _add_message_subscribe_partner(self):
|
def _add_message_subscribe_partner(self):
|
||||||
|
self.ensure_one()
|
||||||
if self.partner_id and self.partner_id not in self.message_partner_ids:
|
if self.partner_id and self.partner_id not in self.message_partner_ids:
|
||||||
self.message_subscribe([self.partner_id.id])
|
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):
|
def action_confirm(self):
|
||||||
"""Invoked when 'Confirm' button in rma form view is clicked."""
|
"""Invoked when 'Confirm' button in rma form view is clicked."""
|
||||||
self.ensure_one()
|
|
||||||
self._ensure_required_fields()
|
self._ensure_required_fields()
|
||||||
if self.state == "draft":
|
self = self.filtered(lambda rma: rma.state == "draft")
|
||||||
if self.picking_id:
|
if not self:
|
||||||
reception_move = self._create_receptions_from_picking()
|
return
|
||||||
else:
|
procurements = self._prepare_reception_procurements()
|
||||||
reception_move = self._create_receptions_from_product()
|
if procurements:
|
||||||
self.write({"reception_move_id": reception_move.id, "state": "confirmed"})
|
self.env["procurement.group"].run(procurements)
|
||||||
self._add_message_subscribe_partner()
|
self.reception_move_id.picking_id.action_assign()
|
||||||
self._send_confirmation_email()
|
self.write({"state": "confirmed"})
|
||||||
|
for rma in self:
|
||||||
|
rma._add_message_subscribe_partner()
|
||||||
|
self._send_confirmation_email()
|
||||||
|
|
||||||
def action_refund(self):
|
def action_refund(self):
|
||||||
"""Invoked when 'Refund' button in rma form view is clicked
|
"""Invoked when 'Refund' button in rma form view is clicked
|
||||||
@@ -723,11 +778,8 @@ class Rma(models.Model):
|
|||||||
self._ensure_can_be_replaced()
|
self._ensure_can_be_replaced()
|
||||||
# Force active_id to avoid issues when coming from smart buttons
|
# Force active_id to avoid issues when coming from smart buttons
|
||||||
# in other models
|
# in other models
|
||||||
action = (
|
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||||
self.env.ref("rma.rma_delivery_wizard_action")
|
"rma.rma_delivery_wizard_action"
|
||||||
.sudo()
|
|
||||||
.with_context(active_id=self.id)
|
|
||||||
.read()[0]
|
|
||||||
)
|
)
|
||||||
action["name"] = "Replace product(s)"
|
action["name"] = "Replace product(s)"
|
||||||
action["context"] = dict(self.env.context)
|
action["context"] = dict(self.env.context)
|
||||||
@@ -746,11 +798,8 @@ class Rma(models.Model):
|
|||||||
self._ensure_can_be_returned()
|
self._ensure_can_be_returned()
|
||||||
# Force active_id to avoid issues when coming from smart buttons
|
# Force active_id to avoid issues when coming from smart buttons
|
||||||
# in other models
|
# in other models
|
||||||
action = (
|
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||||
self.env.ref("rma.rma_delivery_wizard_action")
|
"rma.rma_delivery_wizard_action"
|
||||||
.sudo()
|
|
||||||
.with_context(active_id=self.id)
|
|
||||||
.read()[0]
|
|
||||||
)
|
)
|
||||||
action["context"] = dict(self.env.context)
|
action["context"] = dict(self.env.context)
|
||||||
action["context"].update(
|
action["context"].update(
|
||||||
@@ -766,11 +815,8 @@ class Rma(models.Model):
|
|||||||
self._ensure_can_be_split()
|
self._ensure_can_be_split()
|
||||||
# Force active_id to avoid issues when coming from smart buttons
|
# Force active_id to avoid issues when coming from smart buttons
|
||||||
# in other models
|
# in other models
|
||||||
action = (
|
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||||
self.env.ref("rma.rma_split_wizard_action")
|
"rma.rma_split_wizard_action"
|
||||||
.sudo()
|
|
||||||
.with_context(active_id=self.id)
|
|
||||||
.read()[0]
|
|
||||||
)
|
)
|
||||||
action["context"] = dict(self.env.context)
|
action["context"] = dict(self.env.context)
|
||||||
action["context"].update(active_id=self.id, active_ids=self.ids)
|
action["context"].update(active_id=self.id, active_ids=self.ids)
|
||||||
@@ -782,11 +828,8 @@ class Rma(models.Model):
|
|||||||
self._ensure_can_be_returned()
|
self._ensure_can_be_returned()
|
||||||
# Force active_id to avoid issues when coming from smart buttons
|
# Force active_id to avoid issues when coming from smart buttons
|
||||||
# in other models
|
# in other models
|
||||||
action = (
|
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||||
self.env.ref("rma.rma_finalization_wizard_action")
|
"rma.rma_finalization_wizard_action"
|
||||||
.sudo()
|
|
||||||
.with_context(active_id=self.id)
|
|
||||||
.read()[0]
|
|
||||||
)
|
)
|
||||||
action["context"] = dict(self.env.context)
|
action["context"] = dict(self.env.context)
|
||||||
action["context"].update(active_id=self.id, active_ids=self.ids)
|
action["context"].update(active_id=self.id, active_ids=self.ids)
|
||||||
@@ -794,7 +837,7 @@ class Rma(models.Model):
|
|||||||
|
|
||||||
def action_cancel(self):
|
def action_cancel(self):
|
||||||
"""Invoked when 'Cancel' button in rma form view is clicked."""
|
"""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"})
|
self.write({"state": "cancelled"})
|
||||||
|
|
||||||
def action_draft(self):
|
def action_draft(self):
|
||||||
@@ -819,25 +862,28 @@ class Rma(models.Model):
|
|||||||
"url": self.get_portal_url(),
|
"url": self.get_portal_url(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def action_view_receipt(self):
|
def _action_view_pickings(self, pickings):
|
||||||
"""Invoked when 'Receipt' smart button in rma form view is clicked."""
|
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
# Force active_id to avoid issues when coming from smart buttons
|
# Force active_id to avoid issues when coming from smart buttons
|
||||||
# in other models
|
# in other models
|
||||||
action = (
|
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||||
self.env.ref("stock.action_picking_tree_all")
|
"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,
|
|
||||||
)
|
)
|
||||||
|
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
|
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):
|
def action_view_refund(self):
|
||||||
"""Invoked when 'Refund' smart button in rma form view is clicked."""
|
"""Invoked when 'Refund' smart button in rma form view is clicked."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
@@ -853,23 +899,7 @@ class Rma(models.Model):
|
|||||||
|
|
||||||
def action_view_delivery(self):
|
def action_view_delivery(self):
|
||||||
"""Invoked when 'Delivery' smart button in rma form view is clicked."""
|
"""Invoked when 'Delivery' smart button in rma form view is clicked."""
|
||||||
action = (
|
return self._action_view_pickings(self.mapped("delivery_move_ids.picking_id"))
|
||||||
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
|
|
||||||
|
|
||||||
# Validation business methods
|
# Validation business methods
|
||||||
def _ensure_required_fields(self):
|
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
|
# Extract business methods
|
||||||
def extract_quantity(self, qty, uom):
|
def extract_quantity(self, qty, uom):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
@@ -1133,44 +1088,100 @@ class Rma(models.Model):
|
|||||||
"rma_id": self.id,
|
"rma_id": self.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Returning business methods
|
def _delivery_should_be_grouped(self):
|
||||||
def create_return(self, scheduled_date, qty=None, uom=None):
|
"""Checks if the rmas should be grouped for the delivery process"""
|
||||||
"""Intended to be invoked by the delivery wizard"""
|
|
||||||
group_returns = self.env.company.rma_return_grouping
|
group_returns = self.env.company.rma_return_grouping
|
||||||
if "rma_return_grouping" in self.env.context:
|
if "rma_return_grouping" in self.env.context:
|
||||||
group_returns = self.env.context.get("rma_return_grouping")
|
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_can_be_returned()
|
||||||
self._ensure_qty_to_return(qty, uom)
|
self._ensure_qty_to_return(qty, uom)
|
||||||
group_dict = {}
|
rmas_to_return = self.filtered(
|
||||||
rmas_to_return = self.filtered("can_be_returned")
|
lambda rma: rma.can_be_returned and rma._product_is_storable()
|
||||||
for record in rmas_to_return:
|
)
|
||||||
key = (
|
procurements = rmas_to_return._prepare_delivery_procurements(
|
||||||
record.partner_shipping_id.id,
|
scheduled_date, qty, uom
|
||||||
record.company_id.id,
|
)
|
||||||
record.warehouse_id,
|
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"])
|
for picking, rmas in pickings.items():
|
||||||
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})
|
|
||||||
)
|
|
||||||
picking.action_confirm()
|
picking.action_confirm()
|
||||||
picking.action_assign()
|
picking.action_assign()
|
||||||
picking.message_post_with_view(
|
picking.message_post_with_view(
|
||||||
@@ -1180,42 +1191,53 @@ class Rma(models.Model):
|
|||||||
)
|
)
|
||||||
rmas_to_return.write({"state": "waiting_return"})
|
rmas_to_return.write({"state": "waiting_return"})
|
||||||
|
|
||||||
def _prepare_returning_picking_vals(self, origin=None):
|
def _prepare_replace_procurement_vals(self, warehouse=None, scheduled_date=None):
|
||||||
self.ensure_one()
|
"""This method is used only for Replace (not Delivery). We do not use any
|
||||||
return {
|
specific route here."""
|
||||||
"picking_type_id": self.warehouse_id.rma_out_type_id.id,
|
vals = self._prepare_common_procurement_vals(warehouse, scheduled_date)
|
||||||
"location_id": self.location_id.id,
|
vals["rma_id"] = self.id
|
||||||
"location_dest_id": self.reception_move_id.location_id.id,
|
return vals
|
||||||
"origin": origin or self.name,
|
|
||||||
"partner_id": self.partner_shipping_id.id,
|
|
||||||
"company_id": self.company_id.id,
|
|
||||||
"move_ids": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
def _prepare_returning_move_vals(self, scheduled_date, quantity=None, uom=None):
|
def _prepare_replace_procurements(
|
||||||
self.ensure_one()
|
self, warehouse, scheduled_date, product, qty, uom
|
||||||
return {
|
):
|
||||||
"product_id": self.product_id.id,
|
procurements = []
|
||||||
"name": self.product_id.with_context(
|
group_model = self.env["procurement.group"]
|
||||||
lang=self.partner_shipping_id.lang or "en_US"
|
for rma in self:
|
||||||
).display_name,
|
if not rma._product_is_storable(product):
|
||||||
"product_uom_qty": quantity or self.product_uom_qty,
|
continue
|
||||||
"product_uom": uom and uom.id or self.product_uom.id,
|
|
||||||
"location_id": self.location_id.id,
|
if not rma.procurement_group_id:
|
||||||
"location_dest_id": self.reception_move_id.location_id.id,
|
rma.procurement_group_id = group_model.create(
|
||||||
"date": scheduled_date,
|
rma._prepare_procurement_group_vals()
|
||||||
"rma_id": self.id,
|
)
|
||||||
"move_orig_ids": [(4, self.reception_move_id.id)],
|
|
||||||
"company_id": self.company_id.id,
|
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
|
# Replacing business methods
|
||||||
def create_replace(self, scheduled_date, warehouse, product, qty, uom):
|
def create_replace(self, scheduled_date, warehouse, product, qty, uom):
|
||||||
"""Intended to be invoked by the delivery wizard"""
|
"""Intended to be invoked by the delivery wizard"""
|
||||||
self.ensure_one()
|
|
||||||
self._ensure_can_be_replaced()
|
self._ensure_can_be_replaced()
|
||||||
moves_before = self.delivery_move_ids
|
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
|
new_moves = self.delivery_move_ids - moves_before
|
||||||
body = ""
|
body = ""
|
||||||
# The product replacement could explode into several moves like in the case of
|
# The product replacement could explode into several moves like in the case of
|
||||||
@@ -1239,6 +1261,12 @@ class Rma(models.Model):
|
|||||||
)
|
)
|
||||||
+ "\n"
|
+ "\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(
|
self.message_post(
|
||||||
body=body
|
body=body
|
||||||
or _(
|
or _(
|
||||||
@@ -1251,74 +1279,13 @@ class Rma(models.Model):
|
|||||||
)
|
)
|
||||||
% (
|
% (
|
||||||
{
|
{
|
||||||
"id": product.id,
|
"id": self.id,
|
||||||
"name": product.display_name,
|
"name": self.display_name,
|
||||||
"qty": qty,
|
"qty": qty,
|
||||||
"uom": uom.name,
|
"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
|
# Mail business methods
|
||||||
def _creation_subtype(self):
|
def _creation_subtype(self):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
# 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).
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
from odoo import _, api, fields, models
|
||||||
@@ -22,7 +23,7 @@ class StockMove(models.Model):
|
|||||||
string="RMA receivers",
|
string="RMA receivers",
|
||||||
copy=False,
|
copy=False,
|
||||||
)
|
)
|
||||||
# RMA that create the delivery movement to the customer
|
# RMA that creates the out move
|
||||||
rma_id = fields.Many2one(
|
rma_id = fields.Many2one(
|
||||||
comodel_name="rma",
|
comodel_name="rma",
|
||||||
string="RMA return",
|
string="RMA return",
|
||||||
@@ -32,8 +33,8 @@ class StockMove(models.Model):
|
|||||||
def unlink(self):
|
def unlink(self):
|
||||||
# A stock user could have no RMA permissions, so the ids wouldn't
|
# A stock user could have no RMA permissions, so the ids wouldn't
|
||||||
# be accessible due to record rules.
|
# be accessible due to record rules.
|
||||||
rma_receiver = self.sudo().mapped("rma_receiver_ids")
|
rma_receiver = self.sudo().rma_receiver_ids
|
||||||
rma = self.sudo().mapped("rma_id")
|
rma = self.sudo().rma_id
|
||||||
res = super().unlink()
|
res = super().unlink()
|
||||||
rma_receiver.filtered(lambda x: x.state != "cancelled").write(
|
rma_receiver.filtered(lambda x: x.state != "cancelled").write(
|
||||||
{"state": "draft"}
|
{"state": "draft"}
|
||||||
@@ -103,38 +104,22 @@ class StockMove(models.Model):
|
|||||||
res["rma_id"] = self.sudo().rma_id.id
|
res["rma_id"] = self.sudo().rma_id.id
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _prepare_return_rma_vals(self, original_picking):
|
def _prepare_procurement_values(self):
|
||||||
"""hook method for preparing an RMA from the 'return picking wizard'."""
|
res = super()._prepare_procurement_values()
|
||||||
self.ensure_one()
|
if self.rma_id:
|
||||||
partner = original_picking.partner_id
|
res["rma_id"] = self.rma_id.id
|
||||||
if hasattr(original_picking, "sale_id") and original_picking.sale_id:
|
return res
|
||||||
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",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class StockRule(models.Model):
|
class StockRule(models.Model):
|
||||||
_inherit = "stock.rule"
|
_inherit = "stock.rule"
|
||||||
|
|
||||||
def _get_custom_move_fields(self):
|
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):
|
def action_view_rma(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
action = self.sudo().env.ref("rma.rma_action").read()[0]
|
action = self.env["ir.actions.act_window"]._for_xml_id("rma.rma_action")
|
||||||
rma = self.move_ids.mapped("rma_ids")
|
rma = self.move_lines.mapped("rma_ids")
|
||||||
if len(rma) == 1:
|
if len(rma) == 1:
|
||||||
action.update(
|
action.update(
|
||||||
res_id=rma.id,
|
res_id=rma.id,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
# 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).
|
# 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):
|
class StockWarehouse(models.Model):
|
||||||
@@ -27,35 +28,39 @@ class StockWarehouse(models.Model):
|
|||||||
comodel_name="stock.location",
|
comodel_name="stock.location",
|
||||||
string="RMA 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 _get_rma_location_values(self, vals, code=False):
|
||||||
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):
|
|
||||||
"""this method is intended to be used by 'create' method
|
"""this method is intended to be used by 'create' method
|
||||||
to create a new RMA location to be linked to a new warehouse.
|
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 {
|
return {
|
||||||
"name": self.view_location_id.name,
|
"name": view_location.name,
|
||||||
"active": True,
|
"active": True,
|
||||||
"return_location": True,
|
"return_location": True,
|
||||||
"usage": "internal",
|
"usage": "internal",
|
||||||
"company_id": self.company_id.id,
|
"company_id": company_id,
|
||||||
"location_id": self.env.ref("rma.stock_location_rma").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):
|
def _get_sequence_values(self, name=False, code=False):
|
||||||
values = super()._get_sequence_values(name=name, code=code)
|
values = super()._get_sequence_values(name=name, code=code)
|
||||||
values.update(
|
values.update(
|
||||||
@@ -77,12 +82,14 @@ class StockWarehouse(models.Model):
|
|||||||
return values
|
return values
|
||||||
|
|
||||||
def _update_name_and_code(self, new_name=False, new_code=False):
|
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:
|
for warehouse in self:
|
||||||
sequence_data = warehouse._get_sequence_values()
|
sequence_data = warehouse._get_sequence_values()
|
||||||
warehouse.rma_in_type_id.sequence_id.write(sequence_data["rma_in_type_id"])
|
warehouse.rma_in_type_id.sequence_id.write(sequence_data["rma_in_type_id"])
|
||||||
warehouse.rma_out_type_id.sequence_id.write(
|
warehouse.rma_out_type_id.sequence_id.write(
|
||||||
sequence_data["rma_out_type_id"]
|
sequence_data["rma_out_type_id"]
|
||||||
)
|
)
|
||||||
|
return res
|
||||||
|
|
||||||
def _get_picking_type_create_values(self, max_sequence):
|
def _get_picking_type_create_values(self, max_sequence):
|
||||||
data, next_sequence = super()._get_picking_type_create_values(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_picking_type_id": data.get("rma_out_type_id", False)}
|
||||||
)
|
)
|
||||||
return data
|
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>
|
* Chafique Delli <chafique.delli@akretion.com>
|
||||||
* Giovanni Serra - Ooops <giovanni@ooops404.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,
|
* 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
|
but perhaps stock.move _rec_name could be improved to better show what
|
||||||
the product of that move is.
|
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,
|
<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
|
but perhaps stock.move _rec_name could be improved to better show what
|
||||||
the product of that move is.</li>
|
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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" id="bug-tracker">
|
<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>
|
||||||
<li>Chafique Delli <<a class="reference external" href="mailto:chafique.delli@akretion.com">chafique.delli@akretion.com</a>></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>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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" id="maintainers">
|
<div class="section" id="maintainers">
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
# 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).
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from odoo.exceptions import UserError, ValidationError
|
from odoo.exceptions import UserError, ValidationError
|
||||||
from odoo.tests import Form, TransactionCase, new_test_user, users
|
from odoo.tests import Form, TransactionCase, new_test_user, users
|
||||||
|
|
||||||
|
from .. import hooks
|
||||||
|
|
||||||
|
|
||||||
class TestRma(TransactionCase):
|
class TestRma(TransactionCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -72,6 +75,8 @@ class TestRma(TransactionCase):
|
|||||||
{"name": "[Test] It's out of warranty. To be scrapped"}
|
{"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("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
|
# Ensure grouping
|
||||||
cls.env.company.rma_return_grouping = True
|
cls.env.company.rma_return_grouping = True
|
||||||
|
|
||||||
@@ -130,6 +135,49 @@ class TestRma(TransactionCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestRmaCase(TestRma):
|
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):
|
def test_computed(self):
|
||||||
# If partner changes, the invoice address is set
|
# If partner changes, the invoice address is set
|
||||||
rma = self.env["rma"].new()
|
rma = self.env["rma"].new()
|
||||||
@@ -331,7 +379,7 @@ class TestRmaCase(TestRma):
|
|||||||
# line of refund_1
|
# line of refund_1
|
||||||
self.assertEqual(len(refund_1.invoice_line_ids), 3)
|
self.assertEqual(len(refund_1.invoice_line_ids), 3)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
refund_1.invoice_line_ids.mapped("rma_id"),
|
refund_1.invoice_line_ids.rma_id,
|
||||||
(rma_1 | rma_2 | rma_3),
|
(rma_1 | rma_2 | rma_3),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -538,6 +586,13 @@ class TestRmaCase(TestRma):
|
|||||||
all_rmas = rma_1 | rma_2 | rma_3 | rma_4
|
all_rmas = rma_1 | rma_2 | rma_3 | rma_4
|
||||||
self.assertEqual(all_rmas.mapped("state"), ["received"] * 4)
|
self.assertEqual(all_rmas.mapped("state"), ["received"] * 4)
|
||||||
self.assertEqual(all_rmas.mapped("can_be_returned"), [True] * 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
|
# Mass return of those four RMAs
|
||||||
delivery_wizard = (
|
delivery_wizard = (
|
||||||
self.env["rma.delivery.wizard"]
|
self.env["rma.delivery.wizard"]
|
||||||
@@ -548,6 +603,10 @@ class TestRmaCase(TestRma):
|
|||||||
# Two pickings were created
|
# Two pickings were created
|
||||||
pick_1 = (rma_1 | rma_2 | rma_3).mapped("delivery_move_ids.picking_id")
|
pick_1 = (rma_1 | rma_2 | rma_3).mapped("delivery_move_ids.picking_id")
|
||||||
pick_2 = rma_4.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_1), 1)
|
||||||
self.assertEqual(len(pick_2), 1)
|
self.assertEqual(len(pick_2), 1)
|
||||||
self.assertNotEqual(pick_1, pick_2)
|
self.assertNotEqual(pick_1, pick_2)
|
||||||
@@ -563,7 +622,7 @@ class TestRmaCase(TestRma):
|
|||||||
# line of picking_1
|
# line of picking_1
|
||||||
self.assertEqual(len(pick_1.move_ids), 3)
|
self.assertEqual(len(pick_1.move_ids), 3)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
pick_1.move_ids.mapped("rma_id"),
|
pick_1.move_ids.rma_id,
|
||||||
(rma_1 | rma_2 | rma_3),
|
(rma_1 | rma_2 | rma_3),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -634,14 +693,14 @@ class TestRmaCase(TestRma):
|
|||||||
origin_moves = origin_delivery.move_ids
|
origin_moves = origin_delivery.move_ids
|
||||||
self.assertTrue(origin_moves[0].rma_ids)
|
self.assertTrue(origin_moves[0].rma_ids)
|
||||||
self.assertTrue(origin_moves[1].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)
|
self.assertEqual(rmas.mapped("state"), ["confirmed"] * 2)
|
||||||
# Each reception move is linked one of the generated RMAs
|
# Each reception move is linked one of the generated RMAs
|
||||||
reception = self.env["stock.picking"].browse(picking_action["res_id"])
|
reception = self.env["stock.picking"].browse(picking_action["res_id"])
|
||||||
reception_moves = reception.move_ids
|
reception_moves = reception.move_ids
|
||||||
self.assertTrue(reception_moves[0].rma_receiver_ids)
|
self.assertTrue(reception_moves[0].rma_receiver_ids)
|
||||||
self.assertTrue(reception_moves[1].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
|
# Validate the reception picking to set rmas to 'received' state
|
||||||
reception_moves[0].quantity_done = reception_moves[0].product_uom_qty
|
reception_moves[0].quantity_done = reception_moves[0].product_uom_qty
|
||||||
reception_moves[1].quantity_done = reception_moves[1].product_uom_qty
|
reception_moves[1].quantity_done = reception_moves[1].product_uom_qty
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
# Copyright 2020 Tecnativa - Ernesto Tejeda
|
# 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).
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
from odoo import _, api, fields, models
|
||||||
from odoo.exceptions import ValidationError
|
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):
|
class ReturnPicking(models.TransientModel):
|
||||||
@@ -48,6 +65,52 @@ class ReturnPicking(models.TransientModel):
|
|||||||
location_id = return_picking_type.default_location_dest_id.id
|
location_id = return_picking_type.default_location_dest_id.id
|
||||||
self.location_id = location_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):
|
def create_returns(self):
|
||||||
"""Override create_returns method for creating one or more
|
"""Override create_returns method for creating one or more
|
||||||
'confirmed' RMAs after return a delivery picking in case
|
'confirmed' RMAs after return a delivery picking in case
|
||||||
@@ -57,10 +120,6 @@ class ReturnPicking(models.TransientModel):
|
|||||||
as the 'Receipt'.
|
as the 'Receipt'.
|
||||||
"""
|
"""
|
||||||
if self.create_rma:
|
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:
|
if not self.picking_id.partner_id:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_(
|
_(
|
||||||
@@ -68,12 +127,30 @@ class ReturnPicking(models.TransientModel):
|
|||||||
"'Stock Picking' from which RMAs will be created"
|
"'Stock Picking' from which RMAs will be created"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
returned_picking = self.env["stock.picking"].browse(res["res_id"])
|
vals_list = self._prepare_rma_vals_list()
|
||||||
vals_list = [
|
rmas = self.env["rma"].create(vals_list)
|
||||||
move._prepare_return_rma_vals(self.picking_id)
|
rmas.action_confirm()
|
||||||
for move in returned_picking.move_ids
|
picking = rmas.reception_move_id.picking_id
|
||||||
]
|
picking = picking and picking[0] or picking
|
||||||
self.env["rma"].create(vals_list)
|
ctx = dict(self.env.context)
|
||||||
return res
|
ctx.update(
|
||||||
else:
|
{
|
||||||
return super().create_returns()
|
"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 res_config_settings
|
||||||
from . import rma
|
from . import rma
|
||||||
from . import sale
|
from . import sale
|
||||||
from . import stock_move
|
|
||||||
|
|||||||
@@ -163,3 +163,15 @@ class Rma(models.Model):
|
|||||||
):
|
):
|
||||||
vals["sale_line_ids"] = [(4, line.id)]
|
vals["sale_line_ids"] = [(4, line.id)]
|
||||||
return vals
|
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 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).
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
from odoo import _, api, fields, models
|
||||||
@@ -118,10 +119,23 @@ class SaleOrderLine(models.Model):
|
|||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
# Method helper to filter chained moves
|
# Method helper to filter chained moves
|
||||||
|
|
||||||
def destination_moves(_move):
|
def _get_chained_moves(_moves, done_moves=None):
|
||||||
return _move.mapped("move_dest_ids").filtered(
|
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"]
|
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
|
product = self.product_id
|
||||||
if self.product_id.type not in ["product", "consu"]:
|
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
|
# to return. When a product is re-delivered it should be
|
||||||
# allowed to open an RMA again on it.
|
# allowed to open an RMA again on it.
|
||||||
qty = move.product_uom_qty
|
qty = move.product_uom_qty
|
||||||
qty_returned = 0
|
for _move in _get_chained_moves(move):
|
||||||
move_dest = destination_moves(move)
|
factor = 1
|
||||||
# With the return of the return of the return we could have an
|
if _move.location_dest_id.usage != "customer":
|
||||||
# infinite loop, so we should avoid it dropping already explored
|
factor = -1
|
||||||
# move_dest_ids
|
qty += factor * _move.product_uom_qty
|
||||||
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
|
|
||||||
# If by chance we get a negative qty we should ignore it
|
# 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(
|
data.append(
|
||||||
{
|
{
|
||||||
"product": move.product_id,
|
"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).
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from . import sale_order_rma_wizard
|
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