diff --git a/rma/README.rst b/rma/README.rst index ac962781..8c5cfc4b 100644 --- a/rma/README.rst +++ b/rma/README.rst @@ -7,7 +7,7 @@ Return Merchandise Authorization Management !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:ed3081bf9b94660a0e6b35acbc7a874023c57e4bc3a87f3762eabbb1c75ce2f4 + !! source digest: sha256:8f36869aece97a0f6af8aa5d76b446e9cf0bd589d914c1f5e12c628e87317021 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png @@ -140,6 +140,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 =========== @@ -175,6 +178,8 @@ Contributors - Antoni Marroig +- Michael Tietz (MT Software) mtietz@mt-software.de + Maintainers ----------- diff --git a/rma/__manifest__.py b/rma/__manifest__.py index 044f3d6f..087e4e4f 100644 --- a/rma/__manifest__.py +++ b/rma/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Return Merchandise Authorization Management", "summary": "Return Merchandise Authorization (RMA)", - "version": "17.0.1.1.1", + "version": "17.0.2.0.0", "development_status": "Production/Stable", "category": "RMA", "website": "https://github.com/OCA/rma", diff --git a/rma/hooks.py b/rma/hooks.py index 5b46f9aa..f9995950 100644 --- a/rma/hooks.py +++ b/rma/hooks.py @@ -1,4 +1,5 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Michael Tietz (MT Software) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). @@ -20,7 +21,9 @@ def post_init_hook(env): 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) @@ -57,11 +60,19 @@ def post_init_hook(env): 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() diff --git a/rma/i18n/de.po b/rma/i18n/de.po index ee52923e..d56857df 100644 --- a/rma/i18n/de.po +++ b/rma/i18n/de.po @@ -1531,11 +1531,21 @@ msgstr "Automatische RMA-Kundenbenachrichtigungen" msgid "RMA count" msgstr "RMA-Zählung" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_in_route_id +msgid "RMA in Route" +msgstr "" + #. module: rma #: model:mail.message.subtype,description:rma.mt_rma_draft msgid "RMA in draft state" msgstr "RMA im Entwurfszustand" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_out_route_id +msgid "RMA out Route" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_stock_move__rma_receiver_ids msgid "RMA receivers" @@ -1735,6 +1745,11 @@ msgstr "" msgid "Return Picking" msgstr "Inbound Kommissionierung" +#. module: rma +#: model:ir.model,name:rma.model_stock_return_picking_line +msgid "Return Picking Line" +msgstr "" + #. module: rma #: model:ir.actions.act_window,name:rma.rma_delivery_wizard_action #: model:ir.model.fields.selection,name:rma.selection__rma_delivery_wizard__type__return @@ -1758,6 +1773,13 @@ msgstr "" msgid "Returned" msgstr "Ist zurückgekommen" +#. module: rma +#. odoo-python +#: code:addons/rma/wizard/stock_picking_return.py:0 +#, python-format +msgid "Returned Picking" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_rma_tag__rma_ids msgid "Rma" diff --git a/rma/i18n/de_AT.po b/rma/i18n/de_AT.po index 4b9eed1c..9634e927 100644 --- a/rma/i18n/de_AT.po +++ b/rma/i18n/de_AT.po @@ -1445,11 +1445,21 @@ msgstr "" msgid "RMA count" msgstr "" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_in_route_id +msgid "RMA in Route" +msgstr "" + #. module: rma #: model:mail.message.subtype,description:rma.mt_rma_draft msgid "RMA in draft state" msgstr "" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_out_route_id +msgid "RMA out Route" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_stock_move__rma_receiver_ids msgid "RMA receivers" @@ -1641,6 +1651,11 @@ msgstr "" msgid "Return Picking" msgstr "" +#. module: rma +#: model:ir.model,name:rma.model_stock_return_picking_line +msgid "Return Picking Line" +msgstr "" + #. module: rma #: model:ir.actions.act_window,name:rma.rma_delivery_wizard_action #: model:ir.model.fields.selection,name:rma.selection__rma_delivery_wizard__type__return @@ -1662,6 +1677,13 @@ msgstr "" msgid "Returned" msgstr "" +#. module: rma +#. odoo-python +#: code:addons/rma/wizard/stock_picking_return.py:0 +#, python-format +msgid "Returned Picking" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_rma_tag__rma_ids msgid "Rma" diff --git a/rma/i18n/es.po b/rma/i18n/es.po index 36ae0fd0..d0ab9f74 100644 --- a/rma/i18n/es.po +++ b/rma/i18n/es.po @@ -1598,11 +1598,21 @@ msgstr "Notificaciones automáticas de RMA" msgid "RMA count" msgstr "Cantidad de RMAs" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_in_route_id +msgid "RMA in Route" +msgstr "" + #. module: rma #: model:mail.message.subtype,description:rma.mt_rma_draft msgid "RMA in draft state" msgstr "RMA en estado borrador" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_out_route_id +msgid "RMA out Route" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_stock_move__rma_receiver_ids msgid "RMA receivers" @@ -1803,6 +1813,11 @@ msgstr "Lugar de devolución" msgid "Return Picking" msgstr "Albarán de devolución" +#. module: rma +#: model:ir.model,name:rma.model_stock_return_picking_line +msgid "Return Picking Line" +msgstr "" + #. module: rma #: model:ir.actions.act_window,name:rma.rma_delivery_wizard_action #: model:ir.model.fields.selection,name:rma.selection__rma_delivery_wizard__type__return @@ -1826,6 +1841,13 @@ msgstr "" msgid "Returned" msgstr "Devuelto" +#. module: rma +#. odoo-python +#: code:addons/rma/wizard/stock_picking_return.py:0 +#, python-format +msgid "Returned Picking" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_rma_tag__rma_ids msgid "Rma" diff --git a/rma/i18n/fr.po b/rma/i18n/fr.po index 021e4f55..eb99d589 100644 --- a/rma/i18n/fr.po +++ b/rma/i18n/fr.po @@ -1584,11 +1584,21 @@ msgstr "Notifications RMA automatiques au client" msgid "RMA count" msgstr "Nombre de RMA" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_in_route_id +msgid "RMA in Route" +msgstr "" + #. module: rma #: model:mail.message.subtype,description:rma.mt_rma_draft msgid "RMA in draft state" msgstr "RMA au statut brouillon" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_out_route_id +msgid "RMA out Route" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_stock_move__rma_receiver_ids msgid "RMA receivers" @@ -1789,6 +1799,11 @@ msgstr "Emplacement de retour" msgid "Return Picking" msgstr "Ordres de retour" +#. module: rma +#: model:ir.model,name:rma.model_stock_return_picking_line +msgid "Return Picking Line" +msgstr "" + #. module: rma #: model:ir.actions.act_window,name:rma.rma_delivery_wizard_action #: model:ir.model.fields.selection,name:rma.selection__rma_delivery_wizard__type__return @@ -1812,6 +1827,13 @@ msgstr "" msgid "Returned" msgstr "Retourné" +#. module: rma +#. odoo-python +#: code:addons/rma/wizard/stock_picking_return.py:0 +#, python-format +msgid "Returned Picking" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_rma_tag__rma_ids msgid "Rma" diff --git a/rma/i18n/it.po b/rma/i18n/it.po index cf32415d..885e6662 100644 --- a/rma/i18n/it.po +++ b/rma/i18n/it.po @@ -1591,11 +1591,21 @@ msgstr "Notifiche automatiche ai clienti RMA" msgid "RMA count" msgstr "Conteggio RMA" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_in_route_id +msgid "RMA in Route" +msgstr "" + #. module: rma #: model:mail.message.subtype,description:rma.mt_rma_draft msgid "RMA in draft state" msgstr "RMA in stato bozza" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_out_route_id +msgid "RMA out Route" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_stock_move__rma_receiver_ids msgid "RMA receivers" @@ -1795,6 +1805,11 @@ msgstr "Ubicazione di reso" msgid "Return Picking" msgstr "Prelievo di reso" +#. module: rma +#: model:ir.model,name:rma.model_stock_return_picking_line +msgid "Return Picking Line" +msgstr "" + #. module: rma #: model:ir.actions.act_window,name:rma.rma_delivery_wizard_action #: model:ir.model.fields.selection,name:rma.selection__rma_delivery_wizard__type__return @@ -1818,6 +1833,13 @@ msgstr "" msgid "Returned" msgstr "Restituito" +#. module: rma +#. odoo-python +#: code:addons/rma/wizard/stock_picking_return.py:0 +#, python-format +msgid "Returned Picking" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_rma_tag__rma_ids msgid "Rma" diff --git a/rma/i18n/nl.po b/rma/i18n/nl.po index aaae391c..7cbbec34 100644 --- a/rma/i18n/nl.po +++ b/rma/i18n/nl.po @@ -1445,11 +1445,21 @@ msgstr "" msgid "RMA count" msgstr "" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_in_route_id +msgid "RMA in Route" +msgstr "" + #. module: rma #: model:mail.message.subtype,description:rma.mt_rma_draft msgid "RMA in draft state" msgstr "" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_out_route_id +msgid "RMA out Route" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_stock_move__rma_receiver_ids msgid "RMA receivers" @@ -1641,6 +1651,11 @@ msgstr "" msgid "Return Picking" msgstr "" +#. module: rma +#: model:ir.model,name:rma.model_stock_return_picking_line +msgid "Return Picking Line" +msgstr "" + #. module: rma #: model:ir.actions.act_window,name:rma.rma_delivery_wizard_action #: model:ir.model.fields.selection,name:rma.selection__rma_delivery_wizard__type__return @@ -1662,6 +1677,13 @@ msgstr "" msgid "Returned" msgstr "" +#. module: rma +#. odoo-python +#: code:addons/rma/wizard/stock_picking_return.py:0 +#, python-format +msgid "Returned Picking" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_rma_tag__rma_ids msgid "Rma" diff --git a/rma/i18n/pt.po b/rma/i18n/pt.po index 8ec7f1e5..76572e2c 100644 --- a/rma/i18n/pt.po +++ b/rma/i18n/pt.po @@ -1470,11 +1470,21 @@ msgstr "Notificações automáticas a clientes de RMA" msgid "RMA count" msgstr "Contagem de RMA" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_in_route_id +msgid "RMA in Route" +msgstr "" + #. module: rma #: model:mail.message.subtype,description:rma.mt_rma_draft msgid "RMA in draft state" msgstr "RMA em estado de rascunho" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_out_route_id +msgid "RMA out Route" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_stock_move__rma_receiver_ids msgid "RMA receivers" @@ -1666,6 +1676,11 @@ msgstr "" msgid "Return Picking" msgstr "Operação de Devolução" +#. module: rma +#: model:ir.model,name:rma.model_stock_return_picking_line +msgid "Return Picking Line" +msgstr "" + #. module: rma #: model:ir.actions.act_window,name:rma.rma_delivery_wizard_action #: model:ir.model.fields.selection,name:rma.selection__rma_delivery_wizard__type__return @@ -1687,6 +1702,13 @@ msgstr "" msgid "Returned" msgstr "Devolvido" +#. module: rma +#. odoo-python +#: code:addons/rma/wizard/stock_picking_return.py:0 +#, python-format +msgid "Returned Picking" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_rma_tag__rma_ids msgid "Rma" diff --git a/rma/i18n/pt_BR.po b/rma/i18n/pt_BR.po index 7e23c4fc..9ebca233 100644 --- a/rma/i18n/pt_BR.po +++ b/rma/i18n/pt_BR.po @@ -1470,11 +1470,21 @@ msgstr "" msgid "RMA count" msgstr "Contagem de RMA" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_in_route_id +msgid "RMA in Route" +msgstr "" + #. module: rma #: model:mail.message.subtype,description:rma.mt_rma_draft msgid "RMA in draft state" msgstr "" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_out_route_id +msgid "RMA out Route" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_stock_move__rma_receiver_ids msgid "RMA receivers" @@ -1666,6 +1676,11 @@ msgstr "" msgid "Return Picking" msgstr "Retorno de Coleta" +#. module: rma +#: model:ir.model,name:rma.model_stock_return_picking_line +msgid "Return Picking Line" +msgstr "" + #. module: rma #: model:ir.actions.act_window,name:rma.rma_delivery_wizard_action #: model:ir.model.fields.selection,name:rma.selection__rma_delivery_wizard__type__return @@ -1687,6 +1702,13 @@ msgstr "" msgid "Returned" msgstr "Retornado(a)" +#. module: rma +#. odoo-python +#: code:addons/rma/wizard/stock_picking_return.py:0 +#, python-format +msgid "Returned Picking" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_rma_tag__rma_ids msgid "Rma" diff --git a/rma/i18n/rma.pot b/rma/i18n/rma.pot index 97fd11e5..b2f4ba5f 100644 --- a/rma/i18n/rma.pot +++ b/rma/i18n/rma.pot @@ -1437,11 +1437,21 @@ msgstr "" msgid "RMA count" msgstr "" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_in_route_id +msgid "RMA in Route" +msgstr "" + #. module: rma #: model:mail.message.subtype,description:rma.mt_rma_draft msgid "RMA in draft state" msgstr "" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_out_route_id +msgid "RMA out Route" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_stock_move__rma_receiver_ids msgid "RMA receivers" @@ -1633,6 +1643,11 @@ msgstr "" msgid "Return Picking" msgstr "" +#. module: rma +#: model:ir.model,name:rma.model_stock_return_picking_line +msgid "Return Picking Line" +msgstr "" + #. module: rma #: model:ir.actions.act_window,name:rma.rma_delivery_wizard_action #: model:ir.model.fields.selection,name:rma.selection__rma_delivery_wizard__type__return @@ -1654,6 +1669,13 @@ msgstr "" msgid "Returned" msgstr "" +#. module: rma +#. odoo-python +#: code:addons/rma/wizard/stock_picking_return.py:0 +#, python-format +msgid "Returned Picking" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_rma_tag__rma_ids msgid "Rma" diff --git a/rma/i18n/ro.po b/rma/i18n/ro.po index 87a0ac69..cd1a49f3 100644 --- a/rma/i18n/ro.po +++ b/rma/i18n/ro.po @@ -1468,11 +1468,21 @@ msgstr "" msgid "RMA count" msgstr "" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_in_route_id +msgid "RMA in Route" +msgstr "" + #. module: rma #: model:mail.message.subtype,description:rma.mt_rma_draft msgid "RMA in draft state" msgstr "" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_out_route_id +msgid "RMA out Route" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_stock_move__rma_receiver_ids msgid "RMA receivers" @@ -1664,6 +1674,11 @@ msgstr "" msgid "Return Picking" msgstr "" +#. module: rma +#: model:ir.model,name:rma.model_stock_return_picking_line +msgid "Return Picking Line" +msgstr "" + #. module: rma #: model:ir.actions.act_window,name:rma.rma_delivery_wizard_action #: model:ir.model.fields.selection,name:rma.selection__rma_delivery_wizard__type__return @@ -1685,6 +1700,13 @@ msgstr "" msgid "Returned" msgstr "" +#. module: rma +#. odoo-python +#: code:addons/rma/wizard/stock_picking_return.py:0 +#, python-format +msgid "Returned Picking" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_rma_tag__rma_ids msgid "Rma" diff --git a/rma/i18n/zh_CN.po b/rma/i18n/zh_CN.po index 11d3e492..4f47aaed 100644 --- a/rma/i18n/zh_CN.po +++ b/rma/i18n/zh_CN.po @@ -1447,11 +1447,21 @@ msgstr "" msgid "RMA count" msgstr "" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_in_route_id +msgid "RMA in Route" +msgstr "" + #. module: rma #: model:mail.message.subtype,description:rma.mt_rma_draft msgid "RMA in draft state" msgstr "" +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_out_route_id +msgid "RMA out Route" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_stock_move__rma_receiver_ids msgid "RMA receivers" @@ -1643,6 +1653,11 @@ msgstr "" msgid "Return Picking" msgstr "" +#. module: rma +#: model:ir.model,name:rma.model_stock_return_picking_line +msgid "Return Picking Line" +msgstr "" + #. module: rma #: model:ir.actions.act_window,name:rma.rma_delivery_wizard_action #: model:ir.model.fields.selection,name:rma.selection__rma_delivery_wizard__type__return @@ -1664,6 +1679,13 @@ msgstr "" msgid "Returned" msgstr "" +#. module: rma +#. odoo-python +#: code:addons/rma/wizard/stock_picking_return.py:0 +#, python-format +msgid "Returned Picking" +msgstr "" + #. module: rma #: model:ir.model.fields,field_description:rma.field_rma_tag__rma_ids msgid "Rma" diff --git a/rma/migrations/17.0.1.2.0/post-migration.py b/rma/migrations/17.0.1.2.0/post-migration.py new file mode 100644 index 00000000..71132ec7 --- /dev/null +++ b/rma/migrations/17.0.1.2.0/post-migration.py @@ -0,0 +1,17 @@ +# 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: + if not wh.rma_in_type_id or not wh.rma_out_type_id: + data = wh._create_or_update_sequences_and_picking_types() + wh.write(data) + route_vals = wh._create_or_update_route() + wh.write(route_vals) diff --git a/rma/models/res_partner.py b/rma/models/res_partner.py index e300f6b8..d96a2dda 100644 --- a/rma/models/res_partner.py +++ b/rma/models/res_partner.py @@ -27,7 +27,7 @@ class ResPartner(models.Model): def action_view_rma(self): 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.rma_ids if len(rma) == 1: action.update( diff --git a/rma/models/rma.py b/rma/models/rma.py index 57045089..124f2c2a 100644 --- a/rma/models/rma.py +++ b/rma/models/rma.py @@ -1,12 +1,13 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda # Copyright 2023 Tecnativa - Pedro M. Baeza +# Copyright 2023 Michael Tietz (MT Software) # 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 @@ -259,18 +260,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", @@ -609,22 +600,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() - reception_move.picked = True - 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 @@ -663,11 +717,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) @@ -686,11 +737,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( @@ -706,11 +754,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) @@ -722,11 +767,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) @@ -734,7 +776,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): @@ -759,25 +801,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() @@ -793,23 +838,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): @@ -925,83 +954,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 = f"{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_source( - "mail.message_origin_link", - render_values={"self": picking, "origin": self}, - subtype_id=self.env["ir.model.data"]._xmlid_to_res_id("mail.mt_note"), - ) - 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() @@ -1075,44 +1027,101 @@ 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: %(name)s 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: %(name)s 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_source( @@ -1122,42 +1131,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 @@ -1181,6 +1201,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 _( @@ -1193,74 +1219,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): diff --git a/rma/models/stock_move.py b/rma/models/stock_move.py index 56579b49..40329279 100644 --- a/rma/models/stock_move.py +++ b/rma/models/stock_move.py @@ -1,4 +1,5 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Michael Tietz (MT Software) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models @@ -23,7 +24,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", @@ -33,8 +34,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"} @@ -115,38 +116,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 diff --git a/rma/models/stock_warehouse.py b/rma/models/stock_warehouse.py index f3759274..79ad9dbf 100644 --- a/rma/models/stock_warehouse.py +++ b/rma/models/stock_warehouse.py @@ -1,7 +1,8 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Michael Tietz (MT Software) # 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) @@ -116,12 +123,13 @@ class StockWarehouse(models.Model): def _get_picking_type_update_values(self): data = super()._get_picking_type_update_values() - data.update( - { - "rma_in_type_id": {"default_location_dest_id": self.rma_loc_id.id}, - "rma_out_type_id": {"default_location_src_id": self.rma_loc_id.id}, - } - ) + picking_types = { + "rma_in_type_id": {"default_location_dest_id": self.rma_loc_id.id}, + "rma_out_type_id": {"default_location_src_id": self.rma_loc_id.id}, + } + if self.env.context.get("rma_post_init_hook"): + return picking_types + data.update(picking_types) return data def _create_or_update_sequences_and_picking_types(self): @@ -138,3 +146,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 diff --git a/rma/readme/CONTRIBUTORS.md b/rma/readme/CONTRIBUTORS.md index 36baf52b..801eb97b 100644 --- a/rma/readme/CONTRIBUTORS.md +++ b/rma/readme/CONTRIBUTORS.md @@ -7,3 +7,4 @@ - Giovanni Serra - Ooops \<\> - [APSL-Nagarro](https://www.apsl.tech): - Antoni Marroig \<\> +- Michael Tietz (MT Software) diff --git a/rma/readme/ROADMAP.md b/rma/readme/ROADMAP.md index 19864296..ee383231 100644 --- a/rma/readme/ROADMAP.md +++ b/rma/readme/ROADMAP.md @@ -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. diff --git a/rma/static/description/index.html b/rma/static/description/index.html index e59c143b..56ece24c 100644 --- a/rma/static/description/index.html +++ b/rma/static/description/index.html @@ -366,7 +366,7 @@ ul.auto-toc { !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:ed3081bf9b94660a0e6b35acbc7a874023c57e4bc3a87f3762eabbb1c75ce2f4 +!! source digest: sha256:8f36869aece97a0f6af8aa5d76b446e9cf0bd589d914c1f5e12c628e87317021 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Production/Stable License: AGPL-3 OCA/rma Translate me on Weblate Try me on Runboat

This module allows you to manage Return Merchandise Authorization @@ -493,6 +493,9 @@ team will be the default one if no team is set.

  • 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.
  • diff --git a/rma/tests/test_rma.py b/rma/tests/test_rma.py index 743226e8..6eb0987f 100644 --- a/rma/tests/test_rma.py +++ b/rma/tests/test_rma.py @@ -1,24 +1,20 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Michael Tietz (MT Software) # 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 odoo.tests import Form, new_test_user, users +from odoo.tools import mute_logger + +from odoo.addons.base.tests.common import BaseCommon + +from .. import hooks -class TestRma(TransactionCase): +class TestRma(BaseCommon): @classmethod def setUpClass(cls): super().setUpClass() - cls.env = cls.env( - context=dict( - cls.env.context, - mail_create_nolog=True, - mail_create_nosubscribe=True, - mail_notrack=True, - no_reset_password=True, - tracking_disable=True, - ) - ) cls.user_rma = new_test_user( cls.env, login="user_rma", @@ -72,6 +68,7 @@ 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.warehouse = cls.env.ref("stock.warehouse0") # Ensure grouping cls.env.company.rma_return_grouping = True @@ -95,7 +92,7 @@ class TestRma(TransactionCase): rma = self._create_rma(partner, product, qty, location) rma.action_confirm() rma.reception_move_id.quantity = rma.product_uom_qty - rma.reception_move_id.picking_id._action_done() + rma.reception_move_id.picking_id.button_validate() return rma def _create_delivery(self): @@ -132,6 +129,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) + 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 = 1 + rma.reception_move_id.picking_id.button_validate() + 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() @@ -169,7 +209,7 @@ class TestRmaCase(TestRma): move.product_id = product_2 move.product_uom_qty = 15 picking = picking_form.save() - picking._action_done() + picking.button_validate() rma.picking_id = picking rma.move_id = picking.move_ids self.assertEqual(rma.product_id, product_2) @@ -207,13 +247,18 @@ class TestRmaCase(TestRma): self.assertEqual(rma.state, "confirmed") rma.reception_move_id.quantity = 9 with self.assertRaises(ValidationError): - rma.reception_move_id.picking_id._action_done() + res = rma.reception_move_id.picking_id.button_validate() + wizard = ( + self.env[res["res_model"]].with_context(**res["context"]).create({}) + ) + wizard.process() rma.reception_move_id.quantity = 10 - rma.reception_move_id.picking_id._action_done() + rma.reception_move_id.picking_id.button_validate() self.assertEqual(rma.reception_move_id.picking_id.state, "done") self.assertEqual(rma.reception_move_id.quantity, 10) self.assertEqual(rma.state, "received") + @mute_logger("odoo.models.unlink") def test_cancel(self): # cancel a draft RMA rma = self._create_rma(self.partner, self.product) @@ -333,7 +378,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( @@ -524,6 +569,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"] @@ -534,6 +586,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) @@ -549,7 +605,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( @@ -620,14 +676,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 = reception_moves[0].product_uom_qty reception_moves[1].quantity = reception_moves[1].product_uom_qty @@ -645,7 +701,7 @@ class TestRmaCase(TestRma): rma = rma_form.save() rma.action_confirm() rma.reception_move_id.quantity = 10 - rma.reception_move_id.picking_id._action_done() + rma.reception_move_id.picking_id.button_validate() # Return quantity 4 of the same product to the customer delivery_form = Form( self.env["rma.delivery.wizard"].with_context( @@ -686,6 +742,7 @@ class TestRmaCase(TestRma): self.assertEqual(new_rma.move_id.quantity, 10) self.assertEqual(new_rma.reception_move_id.quantity, 10) + @mute_logger("odoo.models.unlink") def test_rma_to_receive_on_delete_invoice(self): rma = self._create_confirm_receive(self.partner, self.product, 10, self.rma_loc) rma.action_refund() diff --git a/rma/wizard/stock_picking_return.py b/rma/wizard/stock_picking_return.py index 4599f1cf..7c7ca364 100644 --- a/rma/wizard/stock_picking_return.py +++ b/rma/wizard/stock_picking_return.py @@ -1,8 +1,25 @@ # Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2023 Michael Tietz (MT Software) # 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()