Merge PR #444 into 18.0

Signed-off-by pedrobaeza
This commit is contained in:
OCA-git-bot
2025-02-14 19:18:15 +00:00
67 changed files with 31188 additions and 0 deletions

211
rma/README.rst Normal file
View File

@@ -0,0 +1,211 @@
===========================================
Return Merchandise Authorization Management
===========================================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:8f36869aece97a0f6af8aa5d76b446e9cf0bd589d914c1f5e12c628e87317021
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
:target: https://odoo-community.org/page/development-status
:alt: Production/Stable
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frma-lightgray.png?logo=github
:target: https://github.com/OCA/rma/tree/18.0/rma
:alt: OCA/rma
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/rma-18-0/rma-18-0-rma
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/rma&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module allows you to manage `Return Merchandise Authorization
(RMA) <https://en.wikipedia.org/wiki/Return_merchandise_authorization>`__.
RMA documents can be created from scratch, from a delivery order or from
an incoming email. Product receptions and returning delivery operations
of the RMA module are fully integrated with the Receipts and Deliveries
Operations of Odoo inventory core module. It also allows you to generate
refunds in the same way as Odoo generates it. Besides, you have full
integration of the RMA documents in the customer portal.
**Table of contents**
.. contents::
:local:
Configuration
=============
If you want RMAs to be created from incoming emails, you need to:
1. Go to *Settings > General Settings*.
2. Check 'External Email Servers' checkbox under *Discuss* section.
3. Set an 'alias domain' and an incoming server.
4. Go to *RMA > Configuration > RMA Team* and select a team or create a
new one.
5. Go to 'Email' tab and set an 'Email Alias'.
If you want to manually finish RMAs, you need to:
1. Go to *Settings > Inventory*.
2. Set *Finish RMAs manually* checkbox on.
By default, returns to customer are grouped by shipping address,
warehouse and company. If you want to avoid this grouping you can:
1. Go to *Settings > Inventory*.
2. Set *Group RMA returns by customer address and warehouse* checkbox
off.
The users will still be able to group those pickings from the wizard.
Usage
=====
To use this module, you need to:
1. Go to *RMA > Orders* and create a new RMA.
2. Select a partner, an invoice address, select a product (or select a
picking and a move instead), write a quantity, fill the rest of the
form and click on 'confirm' button in the status bar.
3. You will see an smart button labeled 'Receipt'. Click on that button
to see the reception operation form.
4. If everything is right, validate the operation and go back to the RMA
to see it in a 'received' state.
5. Now you are able to generate a refund, generate a delivery order to
return to the customer the same product or another product as a
replacement, split the RMA by extracting a part of the remaining
quantity to another RMA, preview the RMA in the website. All of these
operations can be done by clicking on the buttons in the status bar.
- If you click on 'To Refund' button, a refund will be created, and
it will be accessible via the smart button labeled Refund. The RMA
will be set automatically to 'Refunded' state when the refund is
validated.
- If you click on 'Replace' or 'Return to customer' button instead,
a popup wizard will guide you to create a Delivery order to the
client and this order will be accessible via the smart button
labeled Delivery. The RMA will be set automatically to 'Replaced'
or 'Returned' state when the RMA quantity is equal or lower than
the quantity in done delivery orders linked to it.
6. You can also finish the RMA without further ado. To do so click on
the *Finish* button. A wizard will ask you for the reason from a
selection of preconfigured ones. Be sure to configure them in advance
on *RMA > Configuration > Finalization Reasons*. Once the RMA is
finished, it will be set to that state and the reason will be
registered.
An RMA can also be created from a return of a delivery order:
1. Select a delivery order and click on 'Return' button to create a
return.
2. Check "Create RMAs" checkbox in the returning wizard, select the RMA
stock location and click on 'Return' button.
3. An RMA will be created for each product returned in the previous
step. Every RMA will be in confirmed state and they will be linked to
the returning operation generated previously.
There are Optional RMA Teams that can be used for:
- Organize RMAs in sections.
- Subscribe users to notifications.
- Create RMAs from incoming mail to special aliases (See
configuration section).
To create an RMA Team (RMA Responsible user level required):
1. Go to *RMA > Configuration > RMA Teams*
2. Create a new team and assign a name, a responsible and members.
3. Subscribe users to notifications, that can be of these subtypes:
- RMA draft. When a new RMA is created.
- Notes, Debates, Activities. As in standard Odoo.
4. In the list view, use the cross handle to sort RMA Teams. The top
team will be the default one if no team is set.
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
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/rma/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/rma/issues/new?body=module:%20rma%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Tecnativa
Contributors
------------
- `Tecnativa <https://www.tecnativa.com>`__:
- Ernesto Tejeda
- Pedro M. Baeza
- David Vidal
- Víctor Martínez
- Chafique Delli <chafique.delli@akretion.com>
- Giovanni Serra - Ooops <giovanni@ooops404.com>
- `APSL-Nagarro <https://www.apsl.tech>`__:
- Antoni Marroig <amarroig@apsl.net>
- Michael Tietz (MT Software) mtietz@mt-software.de
- Jacques-Etienne Baudoux - BCIM je@bcim.be
- Souheil Bejaoui - ACSONE SA/NV souheil.bejaoui@acsone.eu
Maintainers
-----------
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
.. |maintainer-pedrobaeza| image:: https://github.com/pedrobaeza.png?size=40px
:target: https://github.com/pedrobaeza
:alt: pedrobaeza
.. |maintainer-chienandalu| image:: https://github.com/chienandalu.png?size=40px
:target: https://github.com/chienandalu
:alt: chienandalu
Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-pedrobaeza| |maintainer-chienandalu|
This module is part of the `OCA/rma <https://github.com/OCA/rma/tree/18.0/rma>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

6
rma/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import controllers
from . import models
from . import wizard
from .hooks import post_init_hook

41
rma/__manifest__.py Normal file
View File

@@ -0,0 +1,41 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# Copyright 2021-2023 Tecnativa - David Vidal
# Copyright 2021-2023 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Return Merchandise Authorization Management",
"summary": "Return Merchandise Authorization (RMA)",
"version": "18.0.1.0.0",
"development_status": "Production/Stable",
"category": "RMA",
"website": "https://github.com/OCA/rma",
"author": "Tecnativa, Odoo Community Association (OCA)",
"maintainers": ["pedrobaeza", "chienandalu"],
"license": "AGPL-3",
"depends": ["stock_account"],
"data": [
"views/report_rma.xml",
"report/report.xml",
"data/mail_data.xml",
"data/rma_operation_data.xml",
"data/stock_data.xml",
"security/rma_security.xml",
"security/ir.model.access.csv",
"wizard/stock_picking_return_views.xml",
"wizard/rma_delivery_views.xml",
"wizard/rma_finalization_wizard_views.xml",
"wizard/rma_split_views.xml",
"views/menus.xml",
"views/res_partner_views.xml",
"views/rma_finalization_views.xml",
"views/rma_portal_templates.xml",
"views/rma_team_views.xml",
"views/rma_views.xml",
"views/rma_tag_views.xml",
"views/stock_picking_views.xml",
"views/stock_warehouse_views.xml",
"views/res_config_settings_views.xml",
],
"post_init_hook": "post_init_hook",
"application": True,
}

View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import main

142
rma/controllers/main.py Normal file
View File

@@ -0,0 +1,142 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# Copyright 2022-2025 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, exceptions, http
from odoo.exceptions import AccessError, MissingError
from odoo.http import request
from odoo.tools import consteq
from odoo.addons.portal.controllers.portal import CustomerPortal
from odoo.addons.portal.controllers.portal import pager as portal_pager
class PortalRma(CustomerPortal):
def _prepare_home_portal_values(self, counters):
values = super()._prepare_home_portal_values(counters)
if "rma_count" in counters:
rma_model = request.env["rma"]
rma_count = (
rma_model.search_count([]) if rma_model.has_access("read") else 0
)
values["rma_count"] = rma_count
return values
def _rma_get_page_view_values(self, rma, access_token, **kwargs):
values = {
"page_name": "RMA",
"rma": rma,
}
return self._get_page_view_values(
rma, access_token, values, "my_rmas_history", False, **kwargs
)
def _get_filter_domain(self, kw):
return []
@http.route(
["/my/rmas", "/my/rmas/page/<int:page>"], type="http", auth="user", website=True
)
def portal_my_rmas(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):
values = self._prepare_portal_layout_values()
rma_obj = request.env["rma"]
# Avoid error if the user does not have access.
if not rma_obj.has_access("read"):
return request.redirect("/my")
domain = self._get_filter_domain(kw)
searchbar_sortings = {
"date": {"label": _("Date"), "order": "date desc"},
"name": {"label": _("Name"), "order": "name desc"},
"state": {"label": _("Status"), "order": "state"},
}
# default sort by order
if not sortby:
sortby = "date"
order = searchbar_sortings[sortby]["order"]
if date_begin and date_end:
domain += [
("create_date", ">", date_begin),
("create_date", "<=", date_end),
]
# count for pager
rma_count = rma_obj.search_count(domain)
# pager
pager = portal_pager(
url="/my/rmas",
url_args={
"date_begin": date_begin,
"date_end": date_end,
"sortby": sortby,
},
total=rma_count,
page=page,
step=self._items_per_page,
)
# content according to pager and archive selected
rmas = rma_obj.search(
domain, order=order, limit=self._items_per_page, offset=pager["offset"]
)
request.session["my_rmas_history"] = rmas.ids[:100]
values.update(
{
"date": date_begin,
"rmas": rmas,
"page_name": "RMA",
"pager": pager,
"default_url": "/my/rmas",
"searchbar_sortings": searchbar_sortings,
"sortby": sortby,
}
)
return request.render("rma.portal_my_rmas", values)
@http.route(["/my/rmas/<int:rma_id>"], type="http", auth="public", website=True)
def portal_my_rma_detail(
self, rma_id, access_token=None, report_type=None, download=False, **kw
):
try:
rma_sudo = self._document_check_access("rma", rma_id, access_token)
except (AccessError, MissingError):
return request.redirect("/my")
if report_type in ("html", "pdf", "text"):
return self._show_report(
model=rma_sudo,
report_type=report_type,
report_ref="rma.report_rma_action",
download=download,
)
values = self._rma_get_page_view_values(rma_sudo, access_token, **kw)
return request.render("rma.portal_rma_page", values)
@http.route(
["/my/rma/picking/pdf/<int:rma_id>/<int:picking_id>"],
type="http",
auth="public",
website=True,
)
def portal_my_rma_picking_report(self, rma_id, picking_id, access_token=None, **kw):
try:
picking_sudo = self._picking_check_access(
rma_id, picking_id, access_token=access_token
)
except exceptions.AccessError:
return request.redirect("/my")
report_sudo = request.env.ref("stock.action_report_delivery").sudo()
pdf = report_sudo._render_qweb_pdf(report_sudo, res_ids=picking_sudo.ids)[0]
pdfhttpheaders = [
("Content-Type", "application/pdf"),
("Content-Length", len(pdf)),
]
return request.make_response(pdf, headers=pdfhttpheaders)
def _picking_check_access(self, rma_id, picking_id, access_token=None):
rma = request.env["rma"].browse([rma_id])
picking = request.env["stock.picking"].browse([picking_id])
picking_sudo = picking.sudo()
try:
picking.check_access("read")
except exceptions.AccessError:
if not access_token or not consteq(rma.access_token, access_token):
raise
return picking_sudo

141
rma/data/mail_data.xml Normal file
View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8" ?>
<data noupdate="1">
<!-- rma-related subtypes for messaging / Chatter -->
<record id="mt_rma_draft" model="mail.message.subtype">
<field name="name">Draft RMA</field>
<field name="res_model">rma</field>
<field name="default" eval="False" />
<field name="description">RMA in draft state</field>
</record>
<record id="mt_rma_notification" model="mail.message.subtype">
<field name="name">RMA Notification</field>
<field name="res_model">rma</field>
<field name="default" eval="False" />
<field name="description">RMA automatic customer notifications</field>
</record>
<!-- rma_team-related subtypes for messaging / Chatter -->
<record id="mt_rma_team_rma_draft" model="mail.message.subtype">
<field name="name">Draft RMA</field>
<field name="sequence">10</field>
<field name="res_model">rma.team</field>
<field name="default" eval="True" />
<field name="parent_id" eval="ref('rma.mt_rma_draft')" />
<field name="relation_field">team_id</field>
</record>
<record id="mt_rma_team_rma_notification" model="mail.message.subtype">
<field name="name">RMA Notification</field>
<field name="sequence">20</field>
<field name="res_model">rma.team</field>
<field name="default" eval="True" />
<field name="parent_id" eval="ref('rma.mt_rma_notification')" />
<field name="relation_field">team_id</field>
</record>
<!--RMA email template -->
<record id="mail_template_rma_notification" model="mail.template">
<field name="name">RMA Notification</field>
<field name="model_id" ref="model_rma" />
<field name="email_from">{{object.user_id.email_formatted}}</field>
<field name="partner_to">{{object.partner_id.id}}</field>
<field
name="subject"
>{{object.company_id.name}} RMA (Ref {{object.name or 'n/a' }})</field>
<field name="report_template_ids" eval="[(4, ref('rma.report_rma_action'))]" />
<field name="lang">{{object.partner_id.lang}}</field>
<field name="auto_delete" eval="True" />
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear
<t t-out="object.partner_id.name" />
<t t-if="object.partner_id.parent_id">
<t t-out="object.partner_id.parent_id.name" />
</t>
<br />
<br />
Here is the RMA
<strong>
<t t-out="object.name" />
</strong>
from
<t t-out="object.company_id.name" />
.
<br />
<br />
Do not hesitate to contact us if you have any question.
</p>
</div>
</field>
</record>
<!--RMA receipt confirmation email template -->
<record id="mail_template_rma_receipt_notification" model="mail.template">
<field name="name">RMA Receipt Notification</field>
<field name="model_id" ref="model_rma" />
<field name="email_from">{{object.user_id.email_formatted }}</field>
<field name="partner_to">{{object.partner_id.id}}</field>
<field
name="subject"
>{{object.company_id.name}} RMA (Ref {{object.name or 'n/a' }}) products received</field>
<field name="report_template_ids" eval="[(4, ref('rma.report_rma_action'))]" />
<field name="lang">{{object.partner_id.lang}}</field>
<field name="auto_delete" eval="True" />
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear
<t t-out="object.partner_id.name" />
<t t-if="object.partner_id.parent_id">
<t t-out="object.partner_id.parent_id.name" />
</t>
<br />
<br />
The products for your RMA
<strong>
<t t-out="object.name" />
</strong>
from
<t t-out="object.company_id.name" />
have been received in our warehouse.
<br />
<br />
Do not hesitate to contact us if you have any question.
</p>
</div>
</field>
</record>
<record id="mail_template_rma_draft_notification" model="mail.template">
<field name="name">RMA Draft Notification</field>
<field name="model_id" ref="model_rma" />
<field name="email_from">{{object.user_id.email_formatted}}</field>
<field name="partner_to">{{object.partner_id.id}}</field>
<field
name="subject"
>{{object.company_id.name}} Your RMA has been succesfully created (Ref {{object.name or 'n/a' }})</field>
<field name="report_template_ids" eval="[(4, ref('rma.report_rma_action'))]" />
<field name="lang">{{object.partner_id.lang}}</field>
<field name="auto_delete" eval="True" />
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear
<t t-out="object.partner_id.name" />
<t t-if="object.partner_id.parent_id">
<t t-out="object.partner_id.parent_id.name" />
</t>
<br />
<br />
You've succesfully placed your RMA
<strong>
<t t-out="object.name" />
</strong>
on
<t t-out="object.company_id.name" />
. Our team will check it and will validate it as soon as possible.
<br />
<br />
Do not hesitate to contact us if you have any question.
</p>
</div>
</field>
</record>
</data>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<data noupdate="1">
<record id="rma_operation_replace" model="rma.operation">
<field name="name">Replace</field>
</record>
<record id="rma_operation_return" model="rma.operation">
<field name="name">Repair</field>
</record>
<record id="rma_operation_refund" model="rma.operation">
<field name="name">Refund</field>
</record>
</data>

9
rma/data/stock_data.xml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<data noupdate="1">
<record id="stock_location_rma" model="stock.location">
<field name="name">RMA</field>
<field name="location_id" ref="stock.stock_location_locations" />
<field name="usage">view</field>
<field name="company_id" />
</record>
</data>

78
rma/hooks.py Normal file
View File

@@ -0,0 +1,78 @@
# 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).
def post_init_hook(env):
def _get_next_picking_type_color():
"""Choose the next available color for the operation types."""
stock_picking_type = env["stock.picking.type"]
picking_type = stock_picking_type.search_read(
[("warehouse_id", "!=", False), ("color", "!=", False)],
["color"],
order="color",
)
all_used_colors = [res["color"] for res in picking_type]
available_colors = [
color for color in range(0, 12) if color not in all_used_colors
]
return available_colors[0] if available_colors else 0
def create_rma_locations(warehouse):
stock_location = env["stock.location"]
if not warehouse.rma_loc_id:
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)
.id
)
def create_rma_picking_types(whs):
ir_sequence_sudo = env["ir.sequence"].sudo()
stock_picking_type = env["stock.picking.type"]
color = _get_next_picking_type_color()
stock_picking = stock_picking_type.search(
[("sequence", "!=", False)], limit=1, order="sequence desc"
)
max_sequence = stock_picking.sequence or 0
create_data = whs._get_picking_type_create_values(max_sequence)[0]
sequence_data = whs._get_sequence_values()
data = {}
for picking_type, values in create_data.items():
if (
picking_type in ["rma_in_type_id", "rma_out_type_id"]
and not whs[picking_type]
):
picking_sequence = sequence_data[picking_type]
sequence = ir_sequence_sudo.create(picking_sequence)
values.update(
warehouse_id=whs.id,
color=color,
sequence_id=sequence.id,
)
data[picking_type] = stock_picking_type.create(values).id
if data:
whs.write(data)
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()

2303
rma/i18n/de.po Normal file

File diff suppressed because it is too large Load Diff

2164
rma/i18n/de_AT.po Normal file

File diff suppressed because it is too large Load Diff

2362
rma/i18n/es.po Normal file

File diff suppressed because it is too large Load Diff

2343
rma/i18n/fr.po Normal file

File diff suppressed because it is too large Load Diff

2353
rma/i18n/it.po Normal file

File diff suppressed because it is too large Load Diff

2164
rma/i18n/nl.po Normal file

File diff suppressed because it is too large Load Diff

2207
rma/i18n/pt.po Normal file

File diff suppressed because it is too large Load Diff

2204
rma/i18n/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

2157
rma/i18n/rma.pot Normal file

File diff suppressed because it is too large Load Diff

2190
rma/i18n/ro.po Normal file

File diff suppressed because it is too large Load Diff

2166
rma/i18n/zh_CN.po Normal file

File diff suppressed because it is too large Load Diff

15
rma/models/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import account_move
from . import rma
from . import rma_finalization
from . import rma_operation
from . import rma_tag
from . import rma_team
from . import res_company
from . import res_config_settings
from . import res_partner
from . import res_users
from . import stock_move
from . import stock_picking
from . import stock_warehouse

View File

@@ -0,0 +1,55 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
from odoo.exceptions import ValidationError
from odoo.tools import float_compare
class AccountMove(models.Model):
_inherit = "account.move"
def _check_rma_invoice_lines_qty(self):
"""We can't refund a different qty than the stated in the RMA.
Extend to change criteria"""
precision = self.env["decimal.precision"].precision_get(
"Product Unit of Measure"
)
return (
self.sudo()
.mapped("invoice_line_ids")
.filtered(
lambda r: (
r.rma_id
and float_compare(r.quantity, r.rma_id.product_uom_qty, precision)
< 0
)
)
)
def action_post(self):
"""Avoids to validate a refund with less quantity of product than
quantity in the linked RMA.
"""
if self._check_rma_invoice_lines_qty():
raise ValidationError(
self.env._(
"There is at least one invoice lines whose quantity is "
"less than the quantity specified in its linked RMA."
)
)
return super().action_post()
def unlink(self):
rma = self.mapped("invoice_line_ids.rma_id")
rma.write({"state": "received"})
return super().unlink()
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
rma_id = fields.Many2one(
comodel_name="rma",
string="RMA",
)

89
rma/models/res_company.py Normal file
View File

@@ -0,0 +1,89 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# Copyright 2023 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class ResCompany(models.Model):
_inherit = "res.company"
def _default_rma_mail_confirmation_template(self):
try:
return self.env.ref("rma.mail_template_rma_notification").id
except ValueError:
return False
def _default_rma_mail_receipt_template(self):
try:
return self.env.ref("rma.mail_template_rma_receipt_notification").id
except ValueError:
return False
def _default_rma_mail_draft_template(self):
try:
return self.env.ref("rma.mail_template_rma_draft_notification").id
except ValueError:
return False
rma_return_grouping = fields.Boolean(
string="Group RMA returns by customer address and warehouse",
default=True,
)
send_rma_confirmation = fields.Boolean(
string="Send RMA Confirmation",
help="When the delivery is confirmed, send a confirmation email "
"to the customer.",
)
send_rma_receipt_confirmation = fields.Boolean(
string="Send RMA Receipt Confirmation",
help="When the RMA receipt is confirmed, send a confirmation email "
"to the customer.",
)
send_rma_draft_confirmation = fields.Boolean(
string="Send RMA draft Confirmation",
help="When a customer places an RMA, send a notification with it",
)
rma_mail_confirmation_template_id = fields.Many2one(
comodel_name="mail.template",
string="Email Template confirmation for RMA",
domain="[('model', '=', 'rma')]",
default=_default_rma_mail_confirmation_template,
help="Email sent to the customer once the RMA is confirmed.",
)
rma_mail_receipt_confirmation_template_id = fields.Many2one(
comodel_name="mail.template",
string="Email Template receipt confirmation for RMA",
domain="[('model', '=', 'rma')]",
default=_default_rma_mail_receipt_template,
help="Email sent to the customer once the RMA products are received.",
)
rma_mail_draft_confirmation_template_id = fields.Many2one(
comodel_name="mail.template",
string="Email Template draft notification for RMA",
domain="[('model', '=', 'rma')]",
default=_default_rma_mail_draft_template,
help="Email sent to the customer when they place " "an RMA from the portal",
)
@api.model_create_multi
def create(self, vals_list):
companies = super().create(vals_list)
for company in companies:
company.create_rma_index()
return companies
def create_rma_index(self):
return (
self.env["ir.sequence"]
.sudo()
.create(
{
"name": self.env._("RMA Code"),
"prefix": "RMA",
"code": "rma",
"padding": 4,
"company_id": self.id,
}
)
)

View File

@@ -0,0 +1,44 @@
# Copyright 2021 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
group_rma_manual_finalization = fields.Boolean(
string="Finish RMA manually choosing a reason",
help="Allow to finish an RMA without returning back a product or refunding",
implied_group="rma.group_rma_manual_finalization",
)
rma_return_grouping = fields.Boolean(
related="company_id.rma_return_grouping",
readonly=False,
)
send_rma_confirmation = fields.Boolean(
related="company_id.send_rma_confirmation",
readonly=False,
)
rma_mail_confirmation_template_id = fields.Many2one(
related="company_id.rma_mail_confirmation_template_id",
domain="[('model', '=', 'rma')]",
readonly=False,
)
send_rma_receipt_confirmation = fields.Boolean(
related="company_id.send_rma_receipt_confirmation",
readonly=False,
)
rma_mail_receipt_confirmation_template_id = fields.Many2one(
related="company_id.rma_mail_receipt_confirmation_template_id",
domain="[('model', '=', 'rma')]",
readonly=False,
)
send_rma_draft_confirmation = fields.Boolean(
related="company_id.send_rma_draft_confirmation",
readonly=False,
)
rma_mail_draft_confirmation_template_id = fields.Many2one(
related="company_id.rma_mail_draft_confirmation_template_id",
domain="[('model', '=', 'rma')]",
readonly=False,
)

41
rma/models/res_partner.py Normal file
View File

@@ -0,0 +1,41 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResPartner(models.Model):
_inherit = "res.partner"
rma_ids = fields.One2many(
comodel_name="rma",
inverse_name="partner_id",
string="RMAs",
)
rma_count = fields.Integer(
string="RMA count",
compute="_compute_rma_count",
)
def _compute_rma_count(self):
rma_data = self.env["rma"].read_group(
[("partner_id", "in", self.ids)], ["partner_id"], ["partner_id"]
)
mapped_data = {r["partner_id"][0]: r["partner_id_count"] for r in rma_data}
for record in self:
record.rma_count = mapped_data.get(record.id, 0)
def action_view_rma(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id("rma.rma_action")
rma = self.rma_ids
if len(rma) == 1:
action.update(
res_id=rma.id,
view_mode="form",
view_id=False,
views=False,
)
else:
action["domain"] = [("partner_id", "in", self.ids)]
return action

14
rma/models/res_users.py Normal file
View File

@@ -0,0 +1,14 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResUsers(models.Model):
_inherit = "res.users"
rma_team_id = fields.Many2one(
comodel_name="rma.team",
string="RMA Team",
help="RMA Team the user is member of.",
)

1351
rma/models/rma.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
# Copyright 2022 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class RmaFinalization(models.Model):
_description = "RMA Finalization Reason"
_name = "rma.finalization"
_order = "name"
active = fields.Boolean(default=True)
name = fields.Char(
string="Reason Name",
required=True,
translate=True,
copy=False,
)
company_id = fields.Many2one(comodel_name="res.company")
_sql_constraints = [
(
"name_company_uniq",
"unique (name, company_id)",
"Finalization name already exists !",
),
]

View File

@@ -0,0 +1,16 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class RmaOperation(models.Model):
_name = "rma.operation"
_description = "RMA requested operation"
active = fields.Boolean(default=True)
name = fields.Char(required=True, translate=True)
_sql_constraints = [
("name_uniq", "unique (name)", "That operation name already exists !"),
]

30
rma/models/rma_tag.py Normal file
View File

@@ -0,0 +1,30 @@
# Copyright 2021 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class RmaTag(models.Model):
_description = "RMA Tags"
_name = "rma.tag"
_order = "name"
active = fields.Boolean(
default=True,
help="The active field allows you to hide the category without " "removing it.",
)
name = fields.Char(
string="Tag Name",
required=True,
translate=True,
copy=False,
)
is_public = fields.Boolean(
string="Public Tag",
help="The tag is visible in the portal view",
)
color = fields.Integer(string="Color Index")
rma_ids = fields.Many2many(comodel_name="rma")
_sql_constraints = [
("name_uniq", "unique (name)", "Tag name already exists !"),
]

63
rma/models/rma_team.py Normal file
View File

@@ -0,0 +1,63 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import ast
from odoo import fields, models
class RmaTeam(models.Model):
_name = "rma.team"
_inherit = ["mail.alias.mixin", "mail.thread"]
_description = "RMA Team"
_order = "sequence, name"
sequence = fields.Integer()
name = fields.Char(
required=True,
translate=True,
)
active = fields.Boolean(
default=True,
help="If the active field is set to false, it will allow you "
"to hide the RMA Team without removing it.",
)
company_id = fields.Many2one(
comodel_name="res.company",
string="Company",
default=lambda self: self.env.company,
)
user_id = fields.Many2one(
comodel_name="res.users",
string="Team Leader",
domain=[("share", "=", False)],
default=lambda self: self.env.user,
)
member_ids = fields.One2many(
comodel_name="res.users",
inverse_name="rma_team_id",
string="Team Members",
)
def copy(self, default=None):
default = dict(default or {})
new_teams = super().copy(default)
for old_team, new_team in zip(self, new_teams, strict=False):
if not default.get("name"):
new_team.name = self.env._("%s (copy)") % old_team.name
for follower in old_team.message_follower_ids:
new_team.message_subscribe(
partner_ids=follower.partner_id.ids,
subtype_ids=follower.subtype_ids.ids,
)
return new_teams
def _alias_get_creation_values(self):
values = super()._alias_get_creation_values()
values["alias_model_id"] = self.env.ref("rma.model_rma").id
if self.id:
values["alias_defaults"] = defaults = ast.literal_eval(
self.alias_defaults or "{}"
)
defaults["team_id"] = self.id
return values

135
rma/models/stock_move.py Normal file
View File

@@ -0,0 +1,135 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import float_compare
class StockMove(models.Model):
_inherit = "stock.move"
# RMAs that were created from the delivery move
rma_ids = fields.One2many(
comodel_name="rma",
inverse_name="move_id",
string="RMAs",
copy=False,
)
# RMAs linked to the incoming movement from client
rma_receiver_ids = fields.One2many(
comodel_name="rma",
inverse_name="reception_move_id",
string="RMA receivers",
copy=False,
)
# RMA that creates the out move
rma_id = fields.Many2one(
comodel_name="rma", string="RMA return", copy=False, index=True
)
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().rma_receiver_ids
rma = self.sudo().rma_id
res = super().unlink()
rma_receiver.filtered(lambda x: x.state != "cancelled").write(
{"state": "draft"}
)
rma.update_received_state()
rma.update_replaced_state()
return res
def _action_cancel(self):
res = super()._action_cancel()
# A stock user could have no RMA permissions, so the ids wouldn't
# be accessible due to record rules.
cancelled_moves = self.filtered(lambda r: r.state == "cancel").sudo()
cancelled_moves.mapped("rma_receiver_ids").write({"state": "draft"})
cancelled_moves.mapped("rma_id").update_received_state()
cancelled_moves.mapped("rma_id").update_replaced_state()
return res
def _action_done(self, cancel_backorder=False):
"""Avoids to validate stock.move with less quantity than the
quantity in the linked receiver RMA. It also set the appropriated
linked RMA to 'received' or 'delivered'.
"""
for move in self.filtered(lambda r: r.state not in ("done", "cancel")):
rma_receiver = move.sudo().rma_receiver_ids
qty_prec = self.env["decimal.precision"].precision_get(
"Product Unit of Measure"
)
if (
rma_receiver
and float_compare(
move.quantity,
rma_receiver.product_uom_qty,
precision_digits=qty_prec,
)
!= 0
):
raise ValidationError(
self.env._(
"The quantity done for the product '%(id)s' must "
"be equal to its initial demand because the "
"stock move is linked to an RMA (%(name)s)."
)
% (
{
"id": move.product_id.name,
"name": move.rma_receiver_ids.name,
}
)
)
res = super()._action_done(cancel_backorder=cancel_backorder)
move_done = self.filtered(lambda r: r.state == "done").sudo()
# Set RMAs as received. We sudo so we can grant the operation even
# if the stock user has no RMA permissions.
to_be_received = (
move_done.sudo()
.mapped("rma_receiver_ids")
.filtered(lambda r: r.state == "confirmed")
)
to_be_received.update_received_state_on_reception()
# Set RMAs as delivered
move_done.mapped("rma_id").update_replaced_state()
move_done.mapped("rma_id").update_returned_state()
return res
@api.model
def _prepare_merge_moves_distinct_fields(self):
"""The main use is that launched delivery RMAs doesn't merge
two moves if they are linked to a different RMAs.
"""
return super()._prepare_merge_moves_distinct_fields() + ["rma_id"]
def _prepare_move_split_vals(self, qty):
"""Intended to the backport of picking linked to RMAs propagates the
RMA link id.
"""
res = super()._prepare_move_split_vals(qty)
res["rma_id"] = self.sudo().rma_id.id
return res
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):
move_fields = super()._get_custom_move_fields()
move_fields += [
"rma_id",
"origin_returned_move_id",
"move_orig_ids",
"rma_receiver_ids",
]
return move_fields

View File

@@ -0,0 +1,32 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class StockPicking(models.Model):
_inherit = "stock.picking"
rma_count = fields.Integer(
string="RMA count",
compute="_compute_rma_count",
)
def _compute_rma_count(self):
for rec in self:
rec.rma_count = len(rec.move_ids.mapped("rma_ids"))
def action_view_rma(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id("rma.rma_action")
rma = self.move_ids.rma_ids
if len(rma) == 1:
action.update(
res_id=rma.id,
view_mode="form",
view_id=False,
views=False,
)
else:
action["domain"] = [("id", "in", rma.ids)]
return action

View File

@@ -0,0 +1,221 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class StockWarehouse(models.Model):
_inherit = "stock.warehouse"
# This is a strategic field used to create an rma location
# and rma operation types in existing warehouses when
# installing this module.
rma = fields.Boolean(
"RMA",
default=True,
help="RMA related products can be stored in this warehouse.",
)
rma_in_type_id = fields.Many2one(
comodel_name="stock.picking.type",
string="RMA In Type",
check_company=True,
copy=False,
)
rma_out_type_id = fields.Many2one(
comodel_name="stock.picking.type",
string="RMA Out Type",
check_company=True,
copy=False,
)
rma_loc_id = fields.Many2one(
comodel_name="stock.location", string="RMA Location", check_company=True
)
rma_in_route_id = fields.Many2one(
"stock.route", "RMA in Route", ondelete="restrict", copy=False
)
rma_out_route_id = fields.Many2one(
"stock.route", "RMA out Route", ondelete="restrict", copy=False
)
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": view_location.name,
"active": True,
"usage": "internal",
"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)
name = name if name else self.name
code = code if code else self.code
values.update(
{
"rma_in_type_id": {
"name": name + " " + self.env._("Sequence RMA in"),
"prefix": code + "/RMA/IN/",
"padding": 5,
"company_id": self.company_id.id,
},
"rma_out_type_id": {
"name": name + " " + self.env._("Sequence RMA out"),
"prefix": code + "/RMA/OUT/",
"padding": 5,
"company_id": self.company_id.id,
},
}
)
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)
data.update(
{
"rma_in_type_id": {
"name": self.env._("RMA Receipts"),
"code": "incoming",
"use_create_lots": False,
"use_existing_lots": True,
"default_location_dest_id": self.rma_loc_id.id,
"sequence": max_sequence + 1,
"sequence_code": "RMA/IN",
"company_id": self.company_id.id,
},
"rma_out_type_id": {
"name": self.env._("RMA Delivery Orders"),
"code": "outgoing",
"use_create_lots": False,
"use_existing_lots": True,
"default_location_src_id": self.rma_loc_id.id,
"sequence": max_sequence + 2,
"sequence_code": "RMA/OUT",
"company_id": self.company_id.id,
},
}
)
return data, max_sequence + 3
def _get_picking_type_update_values(self):
data = super()._get_picking_type_update_values()
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):
data = super()._create_or_update_sequences_and_picking_types()
stock_picking_type = self.env["stock.picking.type"]
if "out_type_id" in data:
rma_out_type = stock_picking_type.browse(data["rma_out_type_id"])
rma_out_type.write(
{"return_picking_type_id": data.get("rma_in_type_id", False)}
)
if "rma_in_type_id" in data:
rma_in_type = stock_picking_type.browse(data["rma_in_type_id"])
rma_in_type.write(
{"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

3
rma/pyproject.toml Normal file
View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

22
rma/readme/CONFIGURE.md Normal file
View File

@@ -0,0 +1,22 @@
If you want RMAs to be created from incoming emails, you need to:
1. Go to *Settings \> General Settings*.
2. Check 'External Email Servers' checkbox under *Discuss* section.
3. Set an 'alias domain' and an incoming server.
4. Go to *RMA \> Configuration \> RMA Team* and select a team or create
a new one.
5. Go to 'Email' tab and set an 'Email Alias'.
If you want to manually finish RMAs, you need to:
1. Go to *Settings \> Inventory*.
2. Set *Finish RMAs manually* checkbox on.
By default, returns to customer are grouped by shipping address,
warehouse and company. If you want to avoid this grouping you can:
1. Go to *Settings \> Inventory*.
2. Set *Group RMA returns by customer address and warehouse* checkbox
off.
The users will still be able to group those pickings from the wizard.

View File

@@ -0,0 +1,12 @@
- [Tecnativa](https://www.tecnativa.com):
- Ernesto Tejeda
- Pedro M. Baeza
- David Vidal
- Víctor Martínez
- Chafique Delli \<<chafique.delli@akretion.com>\>
- Giovanni Serra - Ooops \<<giovanni@ooops404.com>\>
- [APSL-Nagarro](https://www.apsl.tech):
- Antoni Marroig \<<amarroig@apsl.net>\>
- Michael Tietz (MT Software) <mtietz@mt-software.de>
- Jacques-Etienne Baudoux - BCIM <je@bcim.be>
- Souheil Bejaoui - ACSONE SA/NV <souheil.bejaoui@acsone.eu>

View File

@@ -0,0 +1,8 @@
This module allows you to manage [Return Merchandise Authorization
(RMA)](https://en.wikipedia.org/wiki/Return_merchandise_authorization).
RMA documents can be created from scratch, from a delivery order or from
an incoming email. Product receptions and returning delivery operations
of the RMA module are fully integrated with the Receipts and Deliveries
Operations of Odoo inventory core module. It also allows you to generate
refunds in the same way as Odoo generates it. Besides, you have full
integration of the RMA documents in the customer portal.

6
rma/readme/ROADMAP.md Normal file
View File

@@ -0,0 +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.

59
rma/readme/USAGE.md Normal file
View File

@@ -0,0 +1,59 @@
To use this module, you need to:
1. Go to *RMA \> Orders* and create a new RMA.
2. Select a partner, an invoice address, select a product (or select a
picking and a move instead), write a quantity, fill the rest of the
form and click on 'confirm' button in the status bar.
3. You will see an smart button labeled 'Receipt'. Click on that button
to see the reception operation form.
4. If everything is right, validate the operation and go back to the
RMA to see it in a 'received' state.
5. Now you are able to generate a refund, generate a delivery order to
return to the customer the same product or another product as a
replacement, split the RMA by extracting a part of the remaining
quantity to another RMA, preview the RMA in the website. All of
these operations can be done by clicking on the buttons in the
status bar.
- If you click on 'To Refund' button, a refund will be created, and it
will be accessible via the smart button labeled Refund. The RMA
will be set automatically to 'Refunded' state when the refund is
validated.
- If you click on 'Replace' or 'Return to customer' button instead,
a popup wizard will guide you to create a Delivery order to the
client and this order will be accessible via the smart button
labeled Delivery. The RMA will be set automatically to 'Replaced'
or 'Returned' state when the RMA quantity is equal or lower than
the quantity in done delivery orders linked to it.
6. You can also finish the RMA without further ado. To do so click on
the *Finish* button. A wizard will ask you for the reason from a
selection of preconfigured ones. Be sure to configure them in
advance on *RMA \> Configuration \> Finalization Reasons*. Once the
RMA is finished, it will be set to that state and the reason will be
registered.
An RMA can also be created from a return of a delivery order:
1. Select a delivery order and click on 'Return' button to create a
return.
2. Check "Create RMAs" checkbox in the returning wizard, select the RMA
stock location and click on 'Return' button.
3. An RMA will be created for each product returned in the previous
step. Every RMA will be in confirmed state and they will be linked
to the returning operation generated previously.
There are Optional RMA Teams that can be used for:
> - Organize RMAs in sections.
> - Subscribe users to notifications.
> - Create RMAs from incoming mail to special aliases (See configuration
> section).
To create an RMA Team (RMA Responsible user level required):
> 1. Go to *RMA \> Configuration \> RMA Teams*
> 2. Create a new team and assign a name, a responsible and members.
> 3. Subscribe users to notifications, that can be of these subtypes:
> - RMA draft. When a new RMA is created.
> - Notes, Debates, Activities. As in standard Odoo.
> 4. In the list view, use the cross handle to sort RMA Teams. The top
> team will be the default one if no team is set.

11
rma/report/report.xml Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="report_rma_action" model="ir.actions.report">
<field name="name">RMA Report</field>
<field name="model">rma</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">rma.report_rma</field>
<field name="report_file">rma.report_rma</field>
<field name="print_report_name">(object._get_report_base_filename())</field>
</record>
</odoo>

View File

@@ -0,0 +1,18 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_rma_team_user_own,rma.team.user.own,model_rma_team,rma_group_user_own,1,0,0,0
access_rma_team_manager,rma.team.manager,model_rma_team,rma_group_manager,1,1,1,1
access_rma_portal,rma.portal,model_rma,base.group_portal,1,0,0,0
access_rma_user_own,rma.user.own,model_rma,rma_group_user_own,1,1,1,0
access_rma_manager,rma.manager,model_rma,rma_group_manager,1,1,1,1
access_rma_operation_user_own,rma.operation.user.own,model_rma_operation,rma_group_user_own,1,0,0,0
access_rma_operation_manager,rma.operation.manager,model_rma_operation,rma_group_manager,1,1,1,1
access_rma_tag_user_own,rma.tag.user.own,model_rma_tag,rma_group_user_own,1,0,0,0
access_rma_tag_manager,rma.tag.manager,model_rma_tag,rma_group_manager,1,1,1,1
access_rma_delivery_wizard_user_all,rma.delivery.wizard.user.all,model_rma_delivery_wizard,rma_group_user_all,1,1,1,1
access_rma_split_wizard_user_all,rma.split.wizard.user.all,model_rma_split_wizard,rma_group_user_all,1,1,1,1
access_rma_finalization_portal,rma.finalization.portal,model_rma_finalization,base.group_portal,1,0,0,0
access_rma_finalization_user_own,rma.finalization.user.own,model_rma_finalization,rma_group_user_own,1,0,0,0
access_rma_finalization_manager,rma.finalization.manager,model_rma_finalization,rma_group_manager,1,1,1,1
access_rma_finalization_wizard_user_own,rma.finalization.wizard.user.own,model_rma_finalization_wizard,group_rma_manual_finalization,1,1,1,1
access_account_move_rma_user,account_move rma_user,account.model_account_move,rma.rma_group_user_own,1,0,0,0
access_account_move_line_rma_user,account_move_line rma_user,account.model_account_move_line,rma.rma_group_user_own,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_rma_team_user_own rma.team.user.own model_rma_team rma_group_user_own 1 0 0 0
3 access_rma_team_manager rma.team.manager model_rma_team rma_group_manager 1 1 1 1
4 access_rma_portal rma.portal model_rma base.group_portal 1 0 0 0
5 access_rma_user_own rma.user.own model_rma rma_group_user_own 1 1 1 0
6 access_rma_manager rma.manager model_rma rma_group_manager 1 1 1 1
7 access_rma_operation_user_own rma.operation.user.own model_rma_operation rma_group_user_own 1 0 0 0
8 access_rma_operation_manager rma.operation.manager model_rma_operation rma_group_manager 1 1 1 1
9 access_rma_tag_user_own rma.tag.user.own model_rma_tag rma_group_user_own 1 0 0 0
10 access_rma_tag_manager rma.tag.manager model_rma_tag rma_group_manager 1 1 1 1
11 access_rma_delivery_wizard_user_all rma.delivery.wizard.user.all model_rma_delivery_wizard rma_group_user_all 1 1 1 1
12 access_rma_split_wizard_user_all rma.split.wizard.user.all model_rma_split_wizard rma_group_user_all 1 1 1 1
13 access_rma_finalization_portal rma.finalization.portal model_rma_finalization base.group_portal 1 0 0 0
14 access_rma_finalization_user_own rma.finalization.user.own model_rma_finalization rma_group_user_own 1 0 0 0
15 access_rma_finalization_manager rma.finalization.manager model_rma_finalization rma_group_manager 1 1 1 1
16 access_rma_finalization_wizard_user_own rma.finalization.wizard.user.own model_rma_finalization_wizard group_rma_manual_finalization 1 1 1 1
17 access_account_move_rma_user account_move rma_user account.model_account_move rma.rma_group_user_own 1 0 0 0
18 access_account_move_line_rma_user account_move_line rma_user account.model_account_move_line rma.rma_group_user_own 1 0 0 0

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<!-- Application -->
<record id="rma_module_category" model="ir.module.category">
<field name="name">RMA</field>
<field
name="description"
>Manage Return Merchandise Authorizations (RMAs).</field>
</record>
<!-- Access Groups -->
<record id="rma_group_user_own" model="res.groups">
<field name="name">User: Own Documents Only</field>
<field name="category_id" ref="rma_module_category" />
<field name="implied_ids" eval="[(4, ref('base.group_user'))]" />
<field
name="comment"
>the user will have access to his own data in the RMA application.</field>
</record>
<record id="rma_group_user_all" model="res.groups">
<field name="name">User: All Documents</field>
<field name="category_id" ref="rma_module_category" />
<field name="implied_ids" eval="[(4, ref('rma_group_user_own'))]" />
<field
name="comment"
>the user will have access to all records of everyone in the RMA application.</field>
</record>
<record id="rma_group_manager" model="res.groups">
<field name="name">Manager</field>
<field
name="comment"
>the user will have an access to the RMA configuration as well as statistic reports.</field>
<field name="category_id" ref="rma_module_category" />
<field name="implied_ids" eval="[(4, ref('rma_group_user_all'))]" />
<field
name="users"
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
/>
</record>
<record id="group_rma_manual_finalization" model="res.groups">
<field name="name">Allow RMA manual finalization</field>
<field name="category_id" ref="base.module_category_hidden" />
</record>
<!-- Record Rules -->
<record id="rma_rule_user_own" model="ir.rule">
<field name="name">Personal RMAs</field>
<field name="model_id" ref="model_rma" />
<field
name="domain_force"
>['|',('user_id','=',user.id),('user_id','=',False)]</field>
<field name="groups" eval="[(4, ref('rma_group_user_own'))]" />
</record>
<record id="rma_rule_user_all" model="ir.rule">
<field name="name">All RMAs</field>
<field name="model_id" ref="model_rma" />
<field name="domain_force">[(1,'=',1)]</field>
<field name="groups" eval="[(4, ref('rma_group_user_all'))]" />
</record>
<!-- RMA model rules for portal users -->
<record id="rma_rule_portal" model="ir.rule">
<field name="name">RMA portal users</field>
<field name="model_id" ref="rma.model_rma" />
<field
name="domain_force"
>[('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id])]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]" />
</record>
<!-- Multi-Company Rules -->
<record id="rma_rule_multi_company" model="ir.rule">
<field name="name">RMA multi-company</field>
<field name="model_id" ref="model_rma" />
<field name="global" eval="True" />
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record>
<record id="rma_team_rule_multi_company" model="ir.rule">
<field name="name">RMA team multi-company</field>
<field name="model_id" ref="model_rma_team" />
<field name="global" eval="True" />
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record>
<record id="rma_finalization_comp_rule" model="ir.rule">
<field name="name">RMA Finalization Reason multi-company</field>
<field name="model_id" ref="model_rma_finalization" />
<field name="global" eval="True" />
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record>
<!-- Allow to refund RMAs -->
<record id="rma_account_move_personal_rule" model="ir.rule">
<field name="name">RMA Personal Invoice</field>
<field ref="model_account_move" name="model_id" />
<field
name="domain_force"
>[('move_type', '=', 'out_refund'), '|', ('invoice_user_id', '=', user.id), ('invoice_user_id', '=', False)]</field>
<field name="groups" eval="[(4, ref('rma.rma_group_user_own'))]" />
</record>
<record id="rma_account_move_line_personal_rule" model="ir.rule">
<field name="name">RMA Personal Invoice Lines</field>
<field ref="model_account_move_line" name="model_id" />
<field
name="domain_force"
>[('move_id.move_type', '=', 'out_refund'), '|', ('move_id.invoice_user_id', '=', user.id), ('move_id.invoice_user_id', '=', False)]</field>
<field name="groups" eval="[(4, ref('rma.rma_group_user_own'))]" />
</record>
<!-- New users will belong to rma_group_user_own -->
<record id="base.default_user" model="res.users">
<field name="groups_id" eval="[(4, ref('rma_group_user_own'))]" />
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,556 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Return Merchandise Authorization Management</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="return-merchandise-authorization-management">
<h1 class="title">Return Merchandise Authorization Management</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:8f36869aece97a0f6af8aa5d76b446e9cf0bd589d914c1f5e12c628e87317021
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/rma/tree/18.0/rma"><img alt="OCA/rma" src="https://img.shields.io/badge/github-OCA%2Frma-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/rma-18-0/rma-18-0-rma"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/rma&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows you to manage <a class="reference external" href="https://en.wikipedia.org/wiki/Return_merchandise_authorization">Return Merchandise Authorization
(RMA)</a>.
RMA documents can be created from scratch, from a delivery order or from
an incoming email. Product receptions and returning delivery operations
of the RMA module are fully integrated with the Receipts and Deliveries
Operations of Odoo inventory core module. It also allows you to generate
refunds in the same way as Odoo generates it. Besides, you have full
integration of the RMA documents in the customer portal.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-3">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
<p>If you want RMAs to be created from incoming emails, you need to:</p>
<ol class="arabic simple">
<li>Go to <em>Settings &gt; General Settings</em>.</li>
<li>Check External Email Servers checkbox under <em>Discuss</em> section.</li>
<li>Set an alias domain and an incoming server.</li>
<li>Go to <em>RMA &gt; Configuration &gt; RMA Team</em> and select a team or create a
new one.</li>
<li>Go to Email tab and set an Email Alias.</li>
</ol>
<p>If you want to manually finish RMAs, you need to:</p>
<ol class="arabic simple">
<li>Go to <em>Settings &gt; Inventory</em>.</li>
<li>Set <em>Finish RMAs manually</em> checkbox on.</li>
</ol>
<p>By default, returns to customer are grouped by shipping address,
warehouse and company. If you want to avoid this grouping you can:</p>
<ol class="arabic simple">
<li>Go to <em>Settings &gt; Inventory</em>.</li>
<li>Set <em>Group RMA returns by customer address and warehouse</em> checkbox
off.</li>
</ol>
<p>The users will still be able to group those pickings from the wizard.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
<p>To use this module, you need to:</p>
<ol class="arabic simple">
<li>Go to <em>RMA &gt; Orders</em> and create a new RMA.</li>
<li>Select a partner, an invoice address, select a product (or select a
picking and a move instead), write a quantity, fill the rest of the
form and click on confirm button in the status bar.</li>
<li>You will see an smart button labeled Receipt. Click on that button
to see the reception operation form.</li>
<li>If everything is right, validate the operation and go back to the RMA
to see it in a received state.</li>
<li>Now you are able to generate a refund, generate a delivery order to
return to the customer the same product or another product as a
replacement, split the RMA by extracting a part of the remaining
quantity to another RMA, preview the RMA in the website. All of these
operations can be done by clicking on the buttons in the status bar.<ul>
<li>If you click on To Refund button, a refund will be created, and
it will be accessible via the smart button labeled Refund. The RMA
will be set automatically to Refunded state when the refund is
validated.</li>
<li>If you click on Replace or Return to customer button instead,
a popup wizard will guide you to create a Delivery order to the
client and this order will be accessible via the smart button
labeled Delivery. The RMA will be set automatically to Replaced
or Returned state when the RMA quantity is equal or lower than
the quantity in done delivery orders linked to it.</li>
</ul>
</li>
<li>You can also finish the RMA without further ado. To do so click on
the <em>Finish</em> button. A wizard will ask you for the reason from a
selection of preconfigured ones. Be sure to configure them in advance
on <em>RMA &gt; Configuration &gt; Finalization Reasons</em>. Once the RMA is
finished, it will be set to that state and the reason will be
registered.</li>
</ol>
<p>An RMA can also be created from a return of a delivery order:</p>
<ol class="arabic simple">
<li>Select a delivery order and click on Return button to create a
return.</li>
<li>Check “Create RMAs” checkbox in the returning wizard, select the RMA
stock location and click on Return button.</li>
<li>An RMA will be created for each product returned in the previous
step. Every RMA will be in confirmed state and they will be linked to
the returning operation generated previously.</li>
</ol>
<p>There are Optional RMA Teams that can be used for:</p>
<blockquote>
<ul class="simple">
<li>Organize RMAs in sections.</li>
<li>Subscribe users to notifications.</li>
<li>Create RMAs from incoming mail to special aliases (See
configuration section).</li>
</ul>
</blockquote>
<p>To create an RMA Team (RMA Responsible user level required):</p>
<blockquote>
<ol class="arabic simple">
<li>Go to <em>RMA &gt; Configuration &gt; RMA Teams</em></li>
<li>Create a new team and assign a name, a responsible and members.</li>
<li>Subscribe users to notifications, that can be of these subtypes:<ul>
<li>RMA draft. When a new RMA is created.</li>
<li>Notes, Debates, Activities. As in standard Odoo.</li>
</ul>
</li>
<li>In the list view, use the cross handle to sort RMA Teams. The top
team will be the default one if no team is set.</li>
</ol>
</blockquote>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-3">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>As soon as the picking is selected, the user should select the move,
but perhaps stock.move _rec_name could be improved to better show
what the product of that move is.</li>
<li>Add RMA reception and/or RMA delivery on several steps - 2 or 3 -
like normal receptions/deliveries. It should be a separate option
inside the warehouse definition.</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-4">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/rma/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/rma/issues/new?body=module:%20rma%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-5">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-6">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-7">Contributors</a></h2>
<ul class="simple">
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>Ernesto Tejeda</li>
<li>Pedro M. Baeza</li>
<li>David Vidal</li>
<li>Víctor Martínez</li>
</ul>
</li>
<li>Chafique Delli &lt;<a class="reference external" href="mailto:chafique.delli&#64;akretion.com">chafique.delli&#64;akretion.com</a>&gt;</li>
<li>Giovanni Serra - Ooops &lt;<a class="reference external" href="mailto:giovanni&#64;ooops404.com">giovanni&#64;ooops404.com</a>&gt;</li>
<li><a class="reference external" href="https://www.apsl.tech">APSL-Nagarro</a>:<ul>
<li>Antoni Marroig &lt;<a class="reference external" href="mailto:amarroig&#64;apsl.net">amarroig&#64;apsl.net</a>&gt;</li>
</ul>
</li>
<li>Michael Tietz (MT Software) <a class="reference external" href="mailto:mtietz&#64;mt-software.de">mtietz&#64;mt-software.de</a></li>
<li>Jacques-Etienne Baudoux - BCIM <a class="reference external" href="mailto:je&#64;bcim.be">je&#64;bcim.be</a></li>
<li>Souheil Bejaoui - ACSONE SA/NV <a class="reference external" href="mailto:souheil.bejaoui&#64;acsone.eu">souheil.bejaoui&#64;acsone.eu</a></li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainers</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/pedrobaeza"><img alt="pedrobaeza" src="https://github.com/pedrobaeza.png?size=40px" /></a> <a class="reference external image-reference" href="https://github.com/chienandalu"><img alt="chienandalu" src="https://github.com/chienandalu.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/rma/tree/18.0/rma">OCA/rma</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

3
rma/tests/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import test_rma

872
rma/tests/test_rma.py Normal file
View File

@@ -0,0 +1,872 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
# Copyright 2025 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.exceptions import UserError, ValidationError
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(BaseCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user_rma = new_test_user(
cls.env,
login="user_rma",
groups="rma.rma_group_user_own,stock.group_stock_user",
)
cls.res_partner = cls.env["res.partner"]
cls.product_product = cls.env["product.product"]
cls.company = cls.env.user.company_id
cls.warehouse_company = cls.env["stock.warehouse"].search(
[("company_id", "=", cls.company.id)], limit=1
)
cls.rma_loc = cls.warehouse_company.rma_loc_id
cls.product = cls.product_product.create(
{"name": "Product test 1", "type": "consu", "is_storable": True}
)
cls.account_receiv = cls.env["account.account"].create(
{
"name": "Receivable",
"code": "RCV00",
"account_type": "asset_receivable",
"reconcile": True,
}
)
cls.partner = cls.res_partner.create(
{
"name": "Partner test",
"property_account_receivable_id": cls.account_receiv.id,
"property_payment_term_id": cls.env.ref(
"account.account_payment_term_30days"
).id,
}
)
cls.partner_invoice = cls.res_partner.create(
{
"name": "Partner invoice test",
"parent_id": cls.partner.id,
"type": "invoice",
}
)
cls.partner_shipping = cls.res_partner.create(
{
"name": "Partner shipping test",
"parent_id": cls.partner.id,
"type": "delivery",
}
)
cls.finalization_reason_1 = cls.env["rma.finalization"].create(
{"name": ("[Test] It can't be repaired and customer doesn't want it")}
)
cls.finalization_reason_2 = cls.env["rma.finalization"].create(
{"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
cls.operation = cls.env.ref("rma.rma_operation_replace")
def _create_rma(
self, partner=None, product=None, qty=None, location=None, operation=None
):
vals = {}
if partner:
vals["partner_id"] = partner.id
if product:
vals["product_id"] = product.id
if qty:
vals["product_uom_qty"] = qty
if location:
vals["location_id"] = location.id
if operation:
vals["operation_id"] = operation.id
elif operation is None:
vals["operation_id"] = self.operation.id
vals["user_id"] = self.env.user.id
return self.env["rma"].create(vals)
def _create_confirm_receive(
self, partner=None, product=None, qty=None, location=None, operation=None
):
rma = self._create_rma(partner, product, qty, location, operation)
rma.action_confirm()
rma.reception_move_id.quantity = rma.product_uom_qty
rma.reception_move_id.picking_id.button_validate()
return rma
def _create_delivery(self):
picking_type = self.env["stock.picking.type"].search(
[
("code", "=", "outgoing"),
"|",
("warehouse_id.company_id", "=", self.company.id),
("warehouse_id", "=", False),
],
limit=1,
)
picking_form = Form(
record=self.env["stock.picking"].with_context(
default_picking_type_id=picking_type.id
),
view="stock.view_picking_form",
)
picking_form.partner_id = self.partner
with picking_form.move_ids_without_package.new() as move:
move.product_id = self.product
move.product_uom_qty = 10
with picking_form.move_ids_without_package.new() as move:
move.product_id = self.product_product.create(
{"name": "Product 2 test", "type": "consu", "is_storable": True}
)
move.product_uom_qty = 20
picking = picking_form.save()
picking.action_confirm()
for move in picking.move_ids:
move.quantity = move.product_uom_qty
picking.button_validate()
return picking
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()
out_picking = rma.delivery_move_ids.picking_id
out_picking.move_ids.quantity = 1
out_picking.button_validate()
self.assertEqual(out_picking.state, "done")
self.assertEqual(out_picking.picking_type_id, self.warehouse.pick_type_id)
next_transfer = out_picking._get_next_transfers()
self.assertEqual(next_transfer.picking_type_id, self.warehouse.out_type_id)
self.assertEqual(rma.delivery_picking_count, 1)
def test_rma_replace_pick_pack_ship(self):
self.warehouse.write({"delivery_steps": "pick_pack_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()
out_picking = rma.delivery_move_ids.picking_id
out_picking.move_ids.quantity = 1
out_picking.button_validate()
self.assertEqual(out_picking.state, "done")
self.assertEqual(out_picking.picking_type_id, self.warehouse.pick_type_id)
next_transfer = out_picking._get_next_transfers()
self.assertEqual(next_transfer.picking_type_id, self.warehouse.pack_type_id)
next_transfer.move_ids.quantity = 1
next_transfer.button_validate()
self.assertEqual(next_transfer.state, "done")
next_transfer_extra = next_transfer._get_next_transfers()
self.assertEqual(
next_transfer_extra.picking_type_id, self.warehouse.out_type_id
)
self.assertEqual(rma.delivery_picking_count, 1)
def test_computed(self):
# If partner changes, the invoice address is set
rma = self.env["rma"].new()
rma.partner_id = self.partner
self.assertEqual(rma.partner_invoice_id, self.partner_invoice)
# If origin move changes, the product is set
uom_ten = self.env["uom.uom"].create(
{
"name": "Ten",
"category_id": self.env.ref("uom.product_uom_unit").id,
"factor_inv": 10,
"uom_type": "bigger",
}
)
product_2 = self.product_product.create(
{
"name": "Product test 2",
"type": "consu",
"is_storable": True,
"uom_id": uom_ten.id,
}
)
outgoing_picking_type = self.env["stock.picking.type"].search(
[
("code", "=", "outgoing"),
"|",
("warehouse_id.company_id", "=", self.company.id),
("warehouse_id", "=", False),
],
limit=1,
)
picking_form = Form(
record=self.env["stock.picking"].with_context(
default_picking_type_id=outgoing_picking_type.id
),
view="stock.view_picking_form",
)
picking_form.partner_id = self.partner
with picking_form.move_ids_without_package.new() as move:
move.product_id = product_2
move.product_uom_qty = 15
picking = picking_form.save()
picking.button_validate()
rma.picking_id = picking
rma.move_id = picking.move_ids
self.assertEqual(rma.product_id, product_2)
self.assertEqual(rma.product_uom_qty, 15)
self.assertEqual(rma.product_uom, uom_ten)
# If product changes, unit of measure changes
rma.move_id = False
rma.product_id = self.product
self.assertEqual(rma.product_uom, self.product.uom_id)
def test_ensure_required_fields_on_confirm(self):
rma = self._create_rma(operation=False)
with self.assertRaises(ValidationError) as e:
rma.action_confirm()
self.assertEqual(
e.exception.args[0],
"Required field(s):\nCustomer\nShipping Address\nInvoice Address\nProduct"
"\nRequested operation",
)
rma.partner_id = self.partner.id
with self.assertRaises(ValidationError) as e:
rma.action_confirm()
self.assertEqual(
e.exception.args[0], "Required field(s):\nProduct\nRequested operation"
)
rma.product_id = self.product.id
rma.location_id = self.rma_loc.id
with self.assertRaises(ValidationError) as e:
rma.action_confirm()
self.assertEqual(e.exception.args[0], "Required field(s):\nRequested operation")
rma.operation_id = self.operation
rma.action_confirm()
self.assertEqual(rma.state, "confirmed")
def test_confirm_and_receive(self):
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma.action_confirm()
self.assertEqual(rma.reception_move_id.picking_id.state, "assigned")
self.assertEqual(rma.reception_move_id.product_id, rma.product_id)
self.assertEqual(rma.reception_move_id.product_uom_qty, 10)
self.assertEqual(rma.reception_move_id.product_uom, rma.product_uom)
self.assertEqual(rma.state, "confirmed")
rma.reception_move_id.quantity = 9
with self.assertRaises(ValidationError):
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.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)
rma.action_cancel()
self.assertEqual(rma.state, "cancelled")
# cancel a confirmed RMA
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma.action_confirm()
rma.action_cancel()
self.assertEqual(rma.state, "cancelled")
# A RMA is only cancelled from draft and confirmed states
rma = self._create_confirm_receive(self.partner, self.product, 10, self.rma_loc)
with self.assertRaises(UserError):
rma.action_cancel()
def test_lock_unlock(self):
# A RMA is only locked from 'received' state
rma_1 = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma_2 = self._create_confirm_receive(
self.partner, self.product, 10, self.rma_loc
)
self.assertEqual(rma_1.state, "draft")
self.assertEqual(rma_2.state, "received")
(rma_1 | rma_2).action_lock()
self.assertEqual(rma_1.state, "draft")
self.assertEqual(rma_2.state, "locked")
# A RMA is only unlocked from 'lock' state and it will be set
# to 'received' state
(rma_1 | rma_2).action_unlock()
self.assertEqual(rma_1.state, "draft")
self.assertEqual(rma_2.state, "received")
@users("__system__", "user_rma")
@mute_logger("odoo.models.unlink")
def test_action_refund(self):
rma = self._create_confirm_receive(self.partner, self.product, 10, self.rma_loc)
self.assertEqual(rma.state, "received")
self.assertTrue(rma.can_be_refunded)
self.assertTrue(rma.can_be_returned)
self.assertTrue(rma.can_be_replaced)
rma.action_refund()
self.assertEqual(rma.refund_id.move_type, "out_refund")
self.assertEqual(rma.refund_id.state, "draft")
self.assertFalse(rma.refund_id.invoice_payment_term_id)
self.assertEqual(rma.refund_line_id.product_id, rma.product_id)
self.assertEqual(rma.refund_line_id.quantity, 10)
self.assertEqual(rma.refund_line_id.product_uom_id, rma.product_uom)
self.assertEqual(rma.state, "refunded")
self.assertFalse(rma.can_be_refunded)
self.assertFalse(rma.can_be_returned)
self.assertFalse(rma.can_be_replaced)
# A regular user can create the refund but only Invoicing users will be able
# to edit it and post it
if self.env.user.login != "__system__":
return
with Form(rma.refund_line_id.move_id) as refund_form:
with refund_form.invoice_line_ids.edit(0) as refund_line:
refund_line.quantity = 9
with self.assertRaises(ValidationError):
rma.refund_id.action_post()
with Form(rma.refund_line_id.move_id) as refund_form:
with refund_form.invoice_line_ids.edit(0) as refund_line:
refund_line.quantity = 10
rma.refund_id.action_post()
self.assertFalse(rma.can_be_refunded)
self.assertFalse(rma.can_be_returned)
self.assertFalse(rma.can_be_replaced)
@mute_logger("odoo.models.unlink")
def test_mass_refund(self):
# Create, confirm and receive rma_1
rma_1 = self._create_confirm_receive(
self.partner, self.product, 10, self.rma_loc
)
# create, confirm and receive 3 more RMAs
# rma_2: Same partner and same product as rma_1
rma_2 = self._create_confirm_receive(
self.partner, self.product, 15, self.rma_loc
)
# rma_3: Same partner and different product than rma_1
product = self.product_product.create(
{"name": "Product 2 test", "type": "consu", "is_storable": True}
)
rma_3 = self._create_confirm_receive(self.partner, product, 20, self.rma_loc)
# rma_4: Different partner and same product as rma_1
partner = self.res_partner.create(
{
"name": "Partner 2 test",
"property_account_receivable_id": self.account_receiv.id,
"company_id": self.company.id,
}
)
rma_4 = self._create_confirm_receive(partner, product, 25, self.rma_loc)
# all rmas are ready to refund
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_refunded"), [True] * 4)
# Mass refund of those four RMAs
action = self.env.ref("rma.rma_refund_action_server")
ctx = dict(self.env.context)
ctx.update(active_ids=all_rmas.ids, active_model="rma")
action.with_context(**ctx).run()
# After that all RMAs are in 'refunded' state
self.assertEqual(all_rmas.mapped("state"), ["refunded"] * 4)
# Two refunds were created
refund_1 = (rma_1 | rma_2 | rma_3).mapped("refund_id")
refund_2 = rma_4.refund_id
self.assertEqual(len(refund_1), 1)
self.assertEqual(len(refund_2), 1)
self.assertEqual((refund_1 | refund_2).mapped("state"), ["draft"] * 2)
# One refund per partner
self.assertNotEqual(refund_1.partner_id, refund_2.partner_id)
self.assertEqual(
refund_1.partner_id,
(rma_1 | rma_2 | rma_3).mapped("partner_invoice_id"),
)
self.assertEqual(refund_2.partner_id, rma_4.partner_invoice_id)
# Each RMA (rma_1, rma_2 and rma_3) is linked with a different
# line of refund_1
self.assertEqual(len(refund_1.invoice_line_ids), 3)
self.assertEqual(
refund_1.invoice_line_ids.rma_id,
(rma_1 | rma_2 | rma_3),
)
self.assertEqual(
(rma_1 | rma_2 | rma_3).mapped("refund_line_id"),
refund_1.invoice_line_ids,
)
# rma_4 is linked with the unique line of refund_2
self.assertEqual(len(refund_2.invoice_line_ids), 1)
self.assertEqual(refund_2.invoice_line_ids.rma_id, rma_4)
self.assertEqual(rma_4.refund_line_id, refund_2.invoice_line_ids)
# Assert product and quantities are propagated correctly
for rma in all_rmas:
self.assertEqual(rma.product_id, rma.refund_line_id.product_id)
self.assertEqual(rma.product_uom_qty, rma.refund_line_id.quantity)
self.assertEqual(rma.product_uom, rma.refund_line_id.product_uom_id)
# Less quantity -> error on confirm
with Form(rma_2.refund_line_id.move_id) as refund_form:
with refund_form.invoice_line_ids.edit(1) as refund_line:
refund_line.quantity = 14
with self.assertRaises(ValidationError):
refund_1.action_post()
with Form(rma_2.refund_line_id.move_id) as refund_form:
with refund_form.invoice_line_ids.edit(1) as refund_line:
refund_line.quantity = 15
refund_1.action_post()
refund_2.action_post()
def test_replace(self):
# Create, confirm and receive an RMA
rma = self._create_confirm_receive(self.partner, self.product, 10, self.rma_loc)
# Replace with another product with quantity 2.
product_2 = self.product_product.create(
{"name": "Product 2 test", "type": "consu", "is_storable": True}
)
delivery_form = Form(
self.env["rma.delivery.wizard"].with_context(
active_ids=rma.ids,
rma_delivery_type="replace",
)
)
delivery_form.product_id = product_2
delivery_form.product_uom_qty = 2
delivery_wizard = delivery_form.save()
delivery_wizard.action_deliver()
self.assertEqual(len(rma.delivery_move_ids.picking_id.move_ids), 1)
self.assertEqual(rma.delivery_move_ids.product_id, product_2)
self.assertEqual(rma.delivery_move_ids.product_uom_qty, 2)
self.assertTrue(rma.delivery_move_ids.picking_id.state, "waiting")
self.assertEqual(rma.state, "waiting_replacement")
self.assertFalse(rma.can_be_refunded)
self.assertFalse(rma.can_be_returned)
self.assertTrue(rma.can_be_replaced)
self.assertEqual(rma.delivered_qty, 2)
self.assertEqual(rma.remaining_qty, 8)
first_move = rma.delivery_move_ids
picking = first_move.picking_id
# Replace again with another product with the remaining quantity
product_3 = self.product_product.create(
{"name": "Product 3 test", "type": "consu", "is_storable": True}
)
delivery_form = Form(
self.env["rma.delivery.wizard"].with_context(
active_ids=rma.ids,
rma_delivery_type="replace",
)
)
delivery_form.product_id = product_3
delivery_wizard = delivery_form.save()
delivery_wizard.action_deliver()
second_move = rma.delivery_move_ids - first_move
self.assertEqual(len(rma.delivery_move_ids), 2)
self.assertEqual(rma.delivery_move_ids.mapped("picking_id"), picking)
self.assertEqual(first_move.product_id, product_2)
self.assertEqual(first_move.product_uom_qty, 2)
self.assertEqual(second_move.product_id, product_3)
self.assertEqual(second_move.product_uom_qty, 8)
self.assertTrue(picking.state, "waiting")
self.assertEqual(rma.delivered_qty, 10)
self.assertEqual(rma.remaining_qty, 0)
# remaining_qty is 0 but rma is not set to 'replaced' until
first_move.quantity = 2
second_move.quantity = 8
picking.button_validate()
self.assertEqual(picking.state, "done")
self.assertEqual(rma.delivered_qty, 10)
self.assertEqual(rma.remaining_qty, 0)
# The RMA is now in 'replaced' state
self.assertEqual(rma.state, "replaced")
self.assertFalse(rma.can_be_refunded)
self.assertFalse(rma.can_be_returned)
# Despite being in 'replaced' state,
# RMAs can still perform replacements.
self.assertTrue(rma.can_be_replaced)
def test_return_to_customer(self):
# Create, confirm and receive an RMA
rma = self._create_confirm_receive(self.partner, self.product, 10, self.rma_loc)
# Return the same product with quantity 2 to the customer.
delivery_form = Form(
self.env["rma.delivery.wizard"].with_context(
active_ids=rma.ids,
rma_delivery_type="return",
)
)
delivery_form.product_uom_qty = 2
delivery_wizard = delivery_form.save()
delivery_wizard.action_deliver()
picking = rma.delivery_move_ids.picking_id
self.assertEqual(len(picking.move_ids), 1)
self.assertEqual(rma.delivery_move_ids.product_id, self.product)
self.assertEqual(rma.delivery_move_ids.product_uom_qty, 2)
self.assertTrue(picking.state, "waiting")
self.assertEqual(rma.state, "waiting_return")
self.assertFalse(rma.can_be_refunded)
self.assertFalse(rma.can_be_replaced)
self.assertTrue(rma.can_be_returned)
self.assertEqual(rma.delivered_qty, 2)
self.assertEqual(rma.remaining_qty, 8)
first_move = rma.delivery_move_ids
picking = first_move.picking_id
# Validate the picking
first_move.quantity = 2
picking.button_validate()
self.assertEqual(picking.state, "done")
self.assertEqual(rma.delivered_qty, 2)
self.assertEqual(rma.remaining_qty, 8)
# Return the remaining quantity to the customer
delivery_form = Form(
self.env["rma.delivery.wizard"].with_context(
active_ids=rma.ids,
rma_delivery_type="return",
)
)
delivery_wizard = delivery_form.save()
delivery_wizard.action_deliver()
second_move = rma.delivery_move_ids - first_move
second_move.quantity = 8
self.assertEqual(rma.delivered_qty, 10)
self.assertEqual(rma.remaining_qty, 0)
self.assertEqual(rma.state, "waiting_return")
# remaining_qty is 0 but rma is not set to 'returned' until
picking_2 = second_move.picking_id
picking_2.button_validate()
self.assertEqual(picking_2.state, "done")
self.assertEqual(rma.delivered_qty, 10)
self.assertEqual(rma.remaining_qty, 0)
# The RMA is now in 'returned' state
self.assertEqual(rma.state, "returned")
self.assertFalse(rma.can_be_refunded)
self.assertFalse(rma.can_be_returned)
self.assertFalse(rma.can_be_replaced)
def test_finish_rma(self):
# Create, confirm and receive an RMA
rma = self._create_confirm_receive(self.partner, self.product, 10, self.rma_loc)
rma.action_finish()
finalization_form = Form(
self.env["rma.finalization.wizard"].with_context(
active_ids=rma.ids,
rma_finalization_type="replace",
)
)
finalization_form.finalization_id = self.finalization_reason_2
finalization_wizard = finalization_form.save()
finalization_wizard.action_finish()
self.assertEqual(rma.state, "finished")
self.assertEqual(rma.finalization_id, self.finalization_reason_2)
def test_mass_return_to_customer(self):
# Create, confirm and receive rma_1
rma_1 = self._create_confirm_receive(
self.partner, self.product, 10, self.rma_loc
)
# create, confirm and receive 3 more RMAs
# rma_2: Same partner and same product as rma_1
rma_2 = self._create_confirm_receive(
self.partner, self.product, 15, self.rma_loc
)
# rma_3: Same partner and different product than rma_1
product = self.product_product.create(
{"name": "Product 2 test", "type": "consu", "is_storable": True}
)
rma_3 = self._create_confirm_receive(self.partner, product, 20, self.rma_loc)
# rma_4: Different partner and same product as rma_1
partner = self.res_partner.create({"name": "Partner 2 test"})
rma_4 = self._create_confirm_receive(partner, product, 25, self.rma_loc)
# all rmas are ready to be returned to the customer
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"]
.with_context(active_ids=all_rmas.ids, rma_delivery_type="return")
.create({})
)
delivery_wizard.action_deliver()
# 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)
self.assertEqual((pick_1 | pick_2).mapped("state"), ["assigned"] * 2)
# One picking per partner
self.assertNotEqual(pick_1.partner_id, pick_2.partner_id)
self.assertEqual(
pick_1.partner_id,
(rma_1 | rma_2 | rma_3).mapped("partner_shipping_id"),
)
self.assertEqual(pick_2.partner_id, rma_4.partner_id)
# Each RMA of (rma_1, rma_2 and rma_3) is linked to a different
# line of picking_1
self.assertEqual(len(pick_1.move_ids), 3)
self.assertEqual(
pick_1.move_ids.rma_id,
(rma_1 | rma_2 | rma_3),
)
self.assertEqual(
(rma_1 | rma_2 | rma_3).mapped("delivery_move_ids"),
pick_1.move_ids,
)
# rma_4 is linked with the unique move of pick_2
self.assertEqual(len(pick_2.move_ids), 1)
self.assertEqual(pick_2.move_ids.rma_id, rma_4)
self.assertEqual(rma_4.delivery_move_ids, pick_2.move_ids)
# Assert product and quantities are propagated correctly
for rma in all_rmas:
self.assertEqual(rma.product_id, rma.delivery_move_ids.product_id)
self.assertEqual(rma.product_uom_qty, rma.delivery_move_ids.product_uom_qty)
self.assertEqual(rma.product_uom, rma.delivery_move_ids.product_uom)
rma.delivery_move_ids.quantity = rma.product_uom_qty
pick_1.button_validate()
pick_2.button_validate()
self.assertEqual(all_rmas.mapped("state"), ["returned"] * 4)
def test_mass_return_to_customer_ungrouped(self):
"""We can choose to avoid the customer returns grouping"""
self.env.company.rma_return_grouping = False
# Create, confirm and receive rma_1
rma_1 = self._create_confirm_receive(
self.partner, self.product, 10, self.rma_loc
)
# create, confirm and receive 3 more RMAs
# rma_2: Same partner and same product as rma_1
rma_2 = self._create_confirm_receive(
self.partner, self.product, 15, self.rma_loc
)
# rma_3: Same partner and different product than rma_1
product = self.product_product.create(
{"name": "Product 2 test", "type": "consu", "is_storable": True}
)
rma_3 = self._create_confirm_receive(self.partner, product, 20, self.rma_loc)
# rma_4: Different partner and same product as rma_1
partner = self.res_partner.create({"name": "Partner 2 test"})
rma_4 = self._create_confirm_receive(partner, product, 25, self.rma_loc)
# all rmas are ready to be returned to the customer
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)
# Mass return of those four RMAs
delivery_wizard = (
self.env["rma.delivery.wizard"]
.with_context(active_ids=all_rmas.ids, rma_delivery_type="return")
.create({})
)
delivery_wizard.action_deliver()
self.assertEqual(4, len(all_rmas.delivery_move_ids.picking_id))
def test_rma_from_picking_return(self):
# Create a return from a delivery picking
origin_delivery = self._create_delivery()
stock_return_picking_form = Form(
self.env["stock.return.picking"].with_context(
active_ids=origin_delivery.ids,
active_id=origin_delivery.id,
active_model="stock.picking",
)
)
stock_return_picking_form.create_rma = True
stock_return_picking_form.rma_operation_id = self.operation
return_wizard = stock_return_picking_form.save()
for move in origin_delivery.move_ids_without_package:
return_wizard.product_return_moves.filtered(
lambda x, move=move: x.move_id == move
).quantity = move.quantity
picking_action = return_wizard.action_create_returns()
# Each origin move is linked to a different RMA
origin_moves = origin_delivery.move_ids
self.assertTrue(origin_moves[0].rma_ids)
self.assertTrue(origin_moves[1].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.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
reception.button_validate()
self.assertEqual(rmas.mapped("state"), ["received"] * 2)
def test_split(self):
origin_delivery = self._create_delivery()
rma_form = Form(self.env["rma"])
rma_form.partner_id = self.partner
rma_form.picking_id = origin_delivery
rma_form.move_id = origin_delivery.move_ids.filtered(
lambda r: r.product_id == self.product
)
rma_form.operation_id = self.operation
rma = rma_form.save()
rma.action_confirm()
rma.reception_move_id.quantity = 10
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(
active_ids=rma.ids,
rma_delivery_type="return",
)
)
delivery_form.product_uom_qty = 4
delivery_wizard = delivery_form.save()
delivery_wizard.action_deliver()
rma.delivery_move_ids.quantity = 4
rma.delivery_move_ids.picking_id.button_validate()
self.assertEqual(rma.state, "waiting_return")
# Extract the remaining quantity to another RMA
self.assertTrue(rma.can_be_split)
split_wizard = (
self.env["rma.split.wizard"]
.with_context(
active_id=rma.id,
active_ids=rma.ids,
)
.create({})
)
action = split_wizard.action_split()
# Check rma is set to 'returned' after split. Check new_rma values
self.assertEqual(rma.state, "returned")
new_rma = self.env["rma"].browse(action["res_id"])
self.assertEqual(new_rma.origin_split_rma_id, rma)
self.assertEqual(new_rma.delivered_qty, 0)
self.assertEqual(new_rma.remaining_qty, 6)
self.assertEqual(new_rma.state, "received")
self.assertTrue(new_rma.can_be_refunded)
self.assertTrue(new_rma.can_be_returned)
self.assertTrue(new_rma.can_be_replaced)
self.assertEqual(new_rma.move_id, rma.move_id)
self.assertEqual(new_rma.reception_move_id, rma.reception_move_id)
self.assertEqual(new_rma.product_uom_qty + rma.product_uom_qty, 10)
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()
self.assertEqual(rma.state, "refunded")
rma.refund_id.unlink()
self.assertFalse(rma.refund_id)
self.assertEqual(rma.state, "received")
self.assertTrue(rma.can_be_refunded)
self.assertTrue(rma.can_be_returned)
self.assertTrue(rma.can_be_replaced)
def test_rma_picking_type_default_values(self):
warehouse = self.env["stock.warehouse"].create(
{"name": "Stock - RMA Test", "code": "SRT"}
)
self.assertFalse(warehouse.rma_in_type_id.use_create_lots)
self.assertTrue(warehouse.rma_in_type_id.use_existing_lots)
def test_quantities_on_hand(self):
rma = self._create_confirm_receive(self.partner, self.product, 10, self.rma_loc)
self.assertEqual(rma.product_id.qty_available, 0)
def test_autoconfirm_email(self):
self.company.send_rma_confirmation = True
self.company.send_rma_receipt_confirmation = True
self.company.send_rma_draft_confirmation = True
self.company.rma_mail_confirmation_template_id = self.env.ref(
"rma.mail_template_rma_notification"
)
self.company.rma_mail_receipt_confirmation_template_id = self.env.ref(
"rma.mail_template_rma_receipt_notification"
)
self.company.rma_mail_draft_confirmation_template_id = self.env.ref(
"rma.mail_template_rma_draft_notification"
)
previous_mails = self.env["mail.mail"].search(
[("partner_ids", "in", self.partner.ids)]
)
self.assertFalse(previous_mails)
# Force the context to mock an RMA created from the portal, which is
# feature that we get on `rma_sale`. We drop it after the RMA creation
# to avoid uncontrolled side effects
ctx = self.env.context
self.env.context = dict(ctx, from_portal=True)
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
self.env.context = ctx
mail_draft = self.env["mail.message"].search(
[("partner_ids", "in", self.partner.ids)]
)
rma.action_confirm()
mail_confirm = (
self.env["mail.message"].search([("partner_ids", "in", self.partner.ids)])
- mail_draft
)
self.assertTrue(rma.name in mail_confirm.subject)
self.assertTrue(rma.name in mail_confirm.body)
self.assertEqual(
self.env.ref("rma.mt_rma_notification"), mail_confirm.subtype_id
)
# Now we'll confirm the incoming goods picking and the automatic
# reception notification should be sent
rma.reception_move_id.quantity = rma.product_uom_qty
rma.reception_move_id.picking_id.button_validate()
mail_receipt = (
self.env["mail.message"].search([("partner_ids", "in", self.partner.ids)])
- mail_draft
- mail_confirm
)
self.assertTrue(rma.name in mail_receipt.subject)
self.assertTrue("products received" in mail_receipt.subject)

19
rma/views/menus.xml Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<menuitem id="rma_menu" name="RMA" web_icon="rma,static/description/icon.png" />
<menuitem id="rma_orders_menu" parent="rma_menu" name="Orders" sequence="10" />
<menuitem
id="rma_reporting_menu"
parent="rma_menu"
name="Reporting"
sequence="20"
/>
<menuitem
id="rma_configuration_menu"
parent="rma_menu"
name="Configuration"
sequence="30"
/>
</odoo>

154
rma/views/report_rma.xml Normal file
View File

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template id="report_rma_document">
<t t-call="web.external_layout">
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)" />
<t t-if="doc.partner_id">
<t t-set="address">
<div
t-field="doc.partner_id"
t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'
/>
<p t-if="doc.partner_id.vat">
<t t-esc="doc.company_id.country_id.vat_label or 'Tax ID'" />
:
<span t-field="doc.partner_id.vat" />
</p>
</t>
</t>
<t
t-if="doc.partner_shipping_id == doc.partner_invoice_id
and doc.partner_invoice_id != doc.partner_id
or doc.partner_shipping_id != doc.partner_invoice_id"
>
<t t-set="information_block">
<strong
t-if="doc.partner_shipping_id == doc.partner_invoice_id"
>Invoicing and Shipping Address:</strong>
<strong
t-if="doc.partner_shipping_id != doc.partner_invoice_id"
>Invoicing Address:</strong>
<div
t-field="doc.partner_invoice_id"
t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True, "phone_icons": True}'
/>
<t t-if="doc.partner_shipping_id != doc.partner_invoice_id">
<strong>Shipping Address:</strong>
<div
t-field="doc.partner_shipping_id"
t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True, "phone_icons": True}'
/>
</t>
</t>
</t>
<div class="page">
<h2 class="mt16">
<span t-if="doc.state not in ['draft', 'cancelled']">RMA # </span>
<span t-field="doc.name" />
</h2>
<div class="row mt32 mb32" id="general_information">
<div t-if="doc.origin" class="col-auto mw-100 mb-2">
<strong>Origin:</strong>
<p class="m-0" t-field="doc.origin" />
</div>
<div class="col-auto mw-100 mb-2">
<strong>Date:</strong>
<p class="m-0" t-field="doc.date" />
</div>
<div t-if="doc.deadline" class="col-auto mw-100 mb-2">
<strong>Deadline:</strong>
<p class="m-0" t-field="doc.deadline" />
</div>
<div t-if="doc.user_id" class="col-auto mw-100 mb-2">
<strong>Responsible:</strong>
<p class="m-0" t-field="doc.user_id" />
</div>
<div class="col-auto mw-100 mb-2">
<strong>State:</strong>
<p class="m-0">
<t t-if="doc.state in ['refunded', 'replaced', 'returned']">
<span
class="small text-success orders_label_text_align"
>
<i class="fa fa-fw fa-check" />
<b>
<span t-field="doc.state" />
</b>
</span>
</t>
<t t-elif="doc.state in ['cancelled', 'locked']">
<span class="small text-danger orders_label_text_align">
<i class="fa fa-fw fa-times" />
<b>
<span t-field="doc.state" />
</b>
</span>
</t>
<t t-else="">
<span class="small text-info orders_label_text_align">
<i class="fa fa-fw fa-clock-o" />
<b>
<span t-field="doc.state" />
</b>
</span>
</t>
</p>
</div>
</div>
<table class="table table-sm o_main_table table-borderless mt-4">
<tbody>
<tr t-if="doc.picking_id" name="tr_picking">
<td>Origin delivery</td>
<td>
<span t-field="doc.picking_id" />
</td>
</tr>
<tr>
<td>Product</td>
<td>
<span t-field="doc.product_id" />
</td>
</tr>
<tr>
<td>Quantity</td>
<td>
<span t-field="doc.product_uom_qty" />
<span
t-field="doc.product_uom"
groups="uom.group_uom"
/>
</td>
</tr>
<tr t-if="doc.delivered_qty">
<td>Delivered Quantity</td>
<td>
<span t-field="doc.delivered_qty" />
<span
t-field="doc.product_uom"
groups="uom.group_uom"
/>
</td>
</tr>
<tr>
<td>Requested operation</td>
<td>
<span t-field="doc.operation_id" />
</td>
</tr>
</tbody>
</table>
<div t-if="doc.description">
<strong>RMA Note:</strong>
<p t-out="doc.description" />
</div>
</div>
</t>
</template>
<template id="report_rma">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="rma.report_rma_document" t-lang="doc.partner_id.lang" />
</t>
</t>
</template>
</odoo>

View File

@@ -0,0 +1,182 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<app string="RMA" groups="rma.rma_group_manager" name="rma">
<h2>Return Merchandise Authorization Management</h2>
<div
class="row mt16 o_settings_container"
name="operations_setting_container"
>
<div
class="col-12 col-lg-6 o_setting_box"
title="Finish RMAs manually"
>
<div class="o_setting_left_pane">
<field name="group_rma_manual_finalization" />
</div>
<div class="o_setting_right_pane">
<label
for="group_rma_manual_finalization"
string="RMA Manual Finalization"
/>
<div class="text-muted">
When the RMA is receive, allow to finsish it manually choosing
a finalization reason.
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="rma_return_grouping" />
</div>
<div class="o_setting_right_pane">
<label for="rma_return_grouping" />
<span
class="fa fa-lg fa-building-o"
title="Values set here are company-specific."
groups="base.group_multi_company"
/>
<div
class="text-muted"
>Group RMA returns by customer and warehouse.</div>
</div>
</div>
<div
class="col-12 col-lg-6 o_setting_box"
title="Send automatic RMA info to customer"
>
<div class="o_setting_left_pane">
<field name="send_rma_confirmation" />
</div>
<div class="o_setting_right_pane">
<label
for="send_rma_confirmation"
string="RMA Confirmation Email"
/>
<span
class="fa fa-lg fa-building-o"
title="Values set here are company-specific."
groups="base.group_multi_company"
/>
<div
class="text-muted"
>When the RMA is confirmed, send an automatic information email.</div>
<div
class="row mt16"
invisible="not send_rma_confirmation"
>
<label
for="rma_mail_confirmation_template_id"
string="Email Template"
class="col-lg-4 o_light_label"
/>
<field
name="rma_mail_confirmation_template_id"
class="oe_inline"
required="send_rma_confirmation"
/>
</div>
</div>
</div>
<div
class="col-12 col-lg-6 o_setting_box"
title="Send automatic RMA products reception notification to customer"
>
<div class="o_setting_left_pane">
<field name="send_rma_receipt_confirmation" />
</div>
<div class="o_setting_right_pane">
<label
for="send_rma_receipt_confirmation"
string="RMA Receipt Confirmation Email"
/>
<span
class="fa fa-lg fa-building-o"
title="Values set here are company-specific."
groups="base.group_multi_company"
/>
<div
class="text-muted"
>When the RMA products are received, send an automatic information email.</div>
<div
class="row mt16"
invisible="not send_rma_receipt_confirmation"
>
<label
for="rma_mail_receipt_confirmation_template_id"
string="Email Template"
class="col-lg-4 o_light_label"
/>
<field
name="rma_mail_receipt_confirmation_template_id"
class="oe_inline"
required="send_rma_receipt_confirmation"
/>
</div>
</div>
</div>
<div
class="col-12 col-lg-6 o_setting_box"
title="Send automatic notification when the customer places an RMA"
>
<div class="o_setting_left_pane">
<field name="send_rma_draft_confirmation" />
</div>
<div class="o_setting_right_pane">
<label
for="send_rma_draft_confirmation"
string="RMA draft notification Email"
/>
<span
class="fa fa-lg fa-building-o"
title="Values set here are company-specific."
groups="base.group_multi_company"
/>
<div
class="text-muted"
>When customers themselves place an RMA from the portal, send an automatic notification acknowleging it.</div>
<div
class="row mt16"
invisible="not send_rma_draft_confirmation"
>
<label
for="rma_mail_draft_confirmation_template_id"
string="Email Template"
class="col-lg-4 o_light_label"
/>
<field
name="rma_mail_draft_confirmation_template_id"
class="oe_inline"
required="send_rma_draft_confirmation"
/>
</div>
</div>
</div>
</div>
</app>
</xpath>
</field>
</record>
<record id="action_rma_config_settings" model="ir.actions.act_window">
<field name="name">Settings</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.config.settings</field>
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context">{'module' : 'rma', 'bin_size': False}</field>
</record>
<menuitem
id="menu_rma_general_settings"
name="Settings"
parent="rma_configuration_menu"
sequence="0"
action="action_rma_config_settings"
groups="base.group_system"
/>
</odoo>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
Copyright 2023 Tecnativa - Pedro M. Baeza
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_partner_form" model="ir.ui.view">
<field name="name">res.partner.form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="arch" type="xml">
<div name="button_box">
<button
name="action_view_rma"
type="object"
class="oe_stat_button"
icon="fa-reply"
invisible="rma_count == 0"
groups="rma.rma_group_user_own"
>
<field name="rma_count" widget="statinfo" string="RMA" />
</button>
</div>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="rma_finalization_view_search" model="ir.ui.view">
<field name="model">rma.finalization</field>
<field name="arch" type="xml">
<search string="RMA Finalization Reasons">
<field name="name" />
<filter
string="Archived"
name="inactive"
domain="[('active','=',False)]"
/>
<filter
string="Active"
name="active"
domain="[('active','!=',False)]"
/>
</search>
</field>
</record>
<record id="view_rma_finalization_form" model="ir.ui.view">
<field name="name">Rma Finalization Reasons</field>
<field name="model">rma.finalization</field>
<field name="arch" type="xml">
<form string="RMA Finalization">
<sheet>
<widget
name="web_ribbon"
title="Archived"
bg_color="bg-danger"
invisible="active"
/>
<group>
<field name="name" />
<field name="company_id" groups="base.group_multi_company" />
<field name="company_id" invisible="1" />
<field name="active" invisible="1" />
</group>
</sheet>
</form>
</field>
</record>
<record id="view_rma_finalization_list" model="ir.ui.view">
<field name="name">RMA Finalization Reasons</field>
<field name="model">rma.finalization</field>
<field eval="6" name="priority" />
<field name="arch" type="xml">
<list>
<field name="name" />
<field name="company_id" groups="base.group_multi_company" />
<field name="company_id" invisible="1" />
</list>
</field>
</record>
<record id="action_rma_finalization" model="ir.actions.act_window">
<field name="name">RMA Finalization Reasons</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">rma.finalization</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new RMA finalization
</p>
<p>
Manage RMA finalization reasons to better classify them for tracking and analysis purposes.
</p>
</field>
</record>
<menuitem
id="rma_configuration_rma_finalization_menu"
name="RMA Finalization Reasons"
parent="rma_configuration_menu"
action="action_rma_finalization"
groups="rma.group_rma_manual_finalization"
/>
</odoo>

View File

@@ -0,0 +1,471 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<template
id="portal_my_home_menu_rma"
name="Portal layout : RMA menu entries"
inherit_id="portal.portal_breadcrumbs"
priority="35"
>
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
<li
t-if="page_name == 'RMA'"
t-attf-class="breadcrumb-item #{'active ' if not rma else ''}"
>
<a t-if="rma" t-attf-href="/my/rmas?{{ keep_query() }}">RMA Orders</a>
<t t-else="">RMA Orders</t>
</li>
<li t-if="rma" class="breadcrumb-item active">
<t t-esc="rma.name" />
</li>
</xpath>
</template>
<template
id="portal_my_home_rma"
name="Portal My Home : RMA entries"
inherit_id="portal.portal_my_home"
priority="30"
>
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
<t t-call="portal.portal_docs_entry">
<t t-set="title">RMA Orders</t>
<t t-set="url" t-value="'/my/rmas'" />
<t t-set="placeholder_count" t-value="'rma_count'" />
</t>
</xpath>
</template>
<template id="portal_my_rmas" name="My RMA Orders">
<t t-call="portal.portal_layout">
<t t-set="breadcrumbs_searchbar" t-value="True" />
<t t-call="portal.portal_searchbar">
<t t-set="title">RMA Orders</t>
</t>
<t t-if="rmas" t-call="portal.portal_table">
<thead>
<tr class="active">
<th>RMA #</th>
<th class='d-none d-md-table-cell'>Date</th>
<th name="th_product">Product</th>
<th class='text-right'>Quantity</th>
<th class='d-none d-md-table-cell'>Status</th>
</tr>
</thead>
<tbody>
<t t-foreach="rmas" t-as="rma">
<tr>
<td>
<a
t-att-href="rma.get_portal_url()"
t-att-title="rma.name"
>
<t t-esc="rma.name" />
</a>
</td>
<td class="d-none d-md-table-cell">
<span t-field="rma.date" />
</td>
<!-- Portal users don't have access to unpublished products -->
<td name="td_product">
<span t-esc="rma.sudo().product_id.display_name" />
</td>
<td class='text-right'>
<span t-field="rma.product_uom_qty" />
</td>
<td class="d-none d-md-table-cell tx_status">
<span class="badge rounded-pill text-bg-info">
<span t-field="rma.state" />
</span>
</td>
</tr>
</t>
</tbody>
</t>
</t>
</template>
<template id="portal_rma_page" name="My RMA">
<t t-call="portal.portal_layout">
<t t-set="o_portal_fullwidth_alert" groups="rma.rma_group_user_own">
<t t-call="portal.portal_back_in_edit_mode">
<t
t-set="backend_url"
t-value="'/web#return_label=Website&amp;model=rma&amp;id=%s&amp;view_type=form' % (rma.id)"
/>
</t>
</t>
<h5 class="mb-0">
<span>
RMA Order -
<span t-field="rma.name" />
</span>
<span style="position: absolute; left: 50%;" class="d-none d-sm-inline">
<a
t-att-href="rma.get_portal_url(report_type='pdf', download=True)"
>
<i
class="fa fa-download"
role="img"
aria-label="Download"
title="Download"
/>
</a>
</span>
<span class="float-right">
<!-- Tags -->
<t t-set="tags" t-value="rma.tag_ids.filtered('is_public')" />
<!-- We don't have the color o_tag_color_# classes available in the frontend -->
<t t-foreach="tags" t-as="tag">
<span
class="badge badge-pill badge-info label-text-align"
t-esc="tag.name"
/>
</t>
<t t-if="rma.state in ['refunded', 'returned', 'replaced']">
<span class="small text-success orders_label_text_align">
<i class="fa fa-fw fa-check" />
<b>
<span t-field="rma.state" />
</b>
</span>
</t>
<t t-elif="rma.state in ['cancelled', 'locked']">
<span class="small text-danger orders_label_text_align">
<i class="fa fa-fw fa-times" />
<b>
<span t-field="rma.state" />
</b>
</span>
</t>
<t t-else="">
<span class="small text-info orders_label_text_align">
<i class="fa fa-fw fa-clock-o" />
<b>
<span t-field="rma.state" />
</b>
</span>
</t>
</span>
</h5>
<div id="general_information">
<div class="row mt4">
<!-- Customer -->
<div t-if="rma.partner_id" class="col-12 col-md-6 mb-4 mb-md-0">
<h6>
<strong>Customer:</strong>
</h6>
<div class="row">
<div class="col flex-grow-0 pr-3">
<img
t-if="rma.partner_id.avatar_1024"
class="o_avatar o_portal_contact_img rounded"
t-att-src="image_data_uri(rma.partner_id.avatar_512)"
alt="Contact"
/>
</div>
<div class="col pl-sm-0">
<address
t-field="rma.partner_id"
t-options='{"widget": "contact", "fields": ["name", "email", "phone"]}'
/>
</div>
</div>
</div>
<!-- Shipping Address -->
<div
t-if="rma.partner_shipping_id"
class="col-12 col-md-6 mb-4 mb-md-0"
>
<h6>
<strong>Shipping address:</strong>
</h6>
<div class="row">
<div class="col flex-grow-0 pr-3">
<img
t-if="rma.partner_shipping_id.avatar_1024"
class="o_avatar o_portal_contact_img rounded"
t-att-src="image_data_uri(rma.partner_shipping_id.avatar_512)"
alt="Contact"
/>
</div>
<div class="col pl-sm-0">
<address
t-field="rma.partner_shipping_id"
t-options='{"widget": "contact", "fields": ["name", "email", "phone", "address"]}'
/>
</div>
</div>
</div>
<!-- RMA Info -->
<div t-if="rma.user_id" class="col-12 col-md-6">
<h6>
<strong>Responsible:</strong>
</h6>
<div class="row">
<div class="col flex-grow-0 pr-3">
<img
t-if="rma.user_id.avatar_1024"
class="o_avatar o_portal_contact_img rounded"
t-att-src="image_data_uri(rma.user_id.avatar_512)"
alt="Contact"
/>
</div>
<div class="col pl-sm-0">
<address
t-field="rma.user_id"
t-options='{"widget": "contact", "fields": ["name", "email", "phone"]}'
/>
</div>
</div>
</div>
</div>
<div class="row mt32" id="product_information">
<div class="col-12 col-md-6 mb-4 mb-md-0">
<div t-if="rma.picking_id" class="row mb-2 mb-sm-1">
<div class="col-12 col-sm-4">
<strong>Origin delivery</strong>
</div>
<div class="col-12 col-sm-8">
<span t-field="rma.picking_id" />
</div>
</div>
<!-- We need to prevent access errors if the product is
unpublished-->
<div t-if="rma.sudo().product_id" class="row mb-2 mb-sm-1">
<div class="col-12 col-sm-4">
<strong>Product</strong>
</div>
<div class="col-12 col-sm-8">
<span t-esc="rma.sudo().product_id.display_name" />
</div>
</div>
<div t-if="rma.product_uom_qty" class="row mb-2 mb-sm-1">
<div class="col-12 col-sm-4">
<strong>Quantity</strong>
</div>
<div class="col-12 col-sm-8">
<span t-field="rma.product_uom_qty" />
</div>
</div>
<div t-if="rma.delivered_qty" class="row">
<div class="col-12 col-sm-4">
<strong>Delivered quantity</strong>
</div>
<div class="col-12 col-sm-8">
<span t-field="rma.delivered_qty" />
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div t-if="rma.date" class="row mb-2 mb-sm-1">
<div class="col-12 col-sm-4">
<strong>RMA Date</strong>
</div>
<div class="col-12 col-sm-8">
<span
t-field="rma.date"
t-options='{"widget": "date"}'
/>
</div>
</div>
<div t-if="rma.deadline" class="row mb-2 mb-sm-1">
<div class="col-12 col-sm-4">
<strong>Deadline</strong>
</div>
<div class="col-12 col-sm-8">
<span
t-field="rma.deadline"
t-options='{"widget": "date"}'
/>
</div>
</div>
<div t-if="rma.origin" class="row">
<div class="col-12 col-sm-4">
<strong>Origin</strong>
</div>
<div class="col-12 col-sm-8">
<span t-field="rma.origin" />
</div>
</div>
</div>
</div>
</div>
<section
t-if="rma.reception_move_id"
id="reception_section"
style="page-break-inside: auto;"
class="mt32"
>
<strong class="d-block mb-1">Reception</strong>
<t t-set="picking" t-value="rma.reception_move_id.picking_id" />
<t
t-set="report_url"
t-value="'/my/rma/picking/pdf/%s/%s?%s' % (rma.id, picking.id, keep_query())"
/>
<a
class="list-group-item list-group-item-action d-flex flex-wrap align-items-center justify-content-between py-2 px-3"
t-att-href="report_url"
>
<div>
<i
class="fa fa-truck mr-1"
role="img"
aria-label="Download"
title="Download"
/>
<span t-esc="picking.name" class="mr-lg-3" />
<div class="d-lg-inline-block">
Date:
<span class="text-muted" t-field="picking.date" />
</div>
</div>
<t t-if="picking.state == 'done'">
<span class="badge badge-success label-text-align">
<i class="fa fa-fw fa-truck" />
Shipped
</span>
</t>
<t t-if="picking.state == 'partially_available'">
<span class="badge badge-warning label-text-align">
<i class="fa fa-fw fa-clock-o" />
Partially Available
</span>
</t>
<t t-if="picking.state == 'cancel'">
<span class="badge badge-danger label-text-align">
<i class="fa fa-fw fa-times" />
Cancelled
</span>
</t>
<t
t-if="picking.state in ['draft', 'waiting', 'confirmed', 'assigned']"
>
<span class="badge badge-info label-text-align">
<i class="fa fa-fw fa-clock-o" />
Preparation
</span>
</t>
</a>
</section>
<section
t-if="rma.refund_id"
id="refund_section"
style="page-break-inside: auto;"
class="mt32"
>
<strong class="d-block mb-1">Refund</strong>
<t t-set="refund" t-value="rma.refund_id" />
<t
t-set="report_url"
t-value="refund.get_portal_url(report_type='pdf')"
/>
<a
class="list-group-item list-group-item-action d-flex flex-wrap align-items-center justify-content-between py-2 px-3"
t-att-href="report_url"
>
<div>
<i
class="fa fa-pencil-square-o mr-1"
role="img"
aria-label="Download"
title="Download"
/>
<span t-esc="refund.name" class="mr-lg-3" />
<div class="d-lg-inline-block">
Date:
<span class="text-muted" t-field="refund.invoice_date" />
</div>
</div>
<span
t-if="refund.state == 'paid'"
class="small text-success orders_label_text_align"
>
<i class="fa fa-fw fa-check" />
<b>Paid</b>
</span>
<span t-else="" class="small text-info orders_label_text_align">
<i class="fa fa-fw fa-clock-o" />
<b>Waiting Payment</b>
</span>
</a>
</section>
<section
t-if="rma.delivery_move_ids"
id="reception_section"
style="page-break-inside: auto;"
class="mt32"
>
<strong class="d-block mb-1">Delivery</strong>
<ul class="list-group mb-4">
<t
t-foreach="rma.delivery_move_ids.mapped('picking_id')"
t-as="picking"
>
<t
t-set="report_url"
t-value="'/my/rma/picking/pdf/%s/%s?%s' % (rma.id, picking.id, keep_query())"
/>
<a
class="list-group-item list-group-item-action d-flex flex-wrap align-items-center justify-content-between py-2 px-3"
t-att-href="report_url"
>
<div>
<i
class="fa fa-truck mr-1"
role="img"
aria-label="Download"
title="Download"
/>
<span t-esc="picking.name" class="mr-lg-3" />
<div class="d-lg-inline-block">
Date:
<span class="text-muted" t-field="picking.date" />
</div>
</div>
<t t-if="picking.state == 'done'">
<span class="badge badge-success label-text-align">
<i class="fa fa-fw fa-truck" />
Shipped
</span>
</t>
<t t-if="picking.state == 'partially_available'">
<span class="badge badge-warning label-text-align">
<i class="fa fa-fw fa-clock-o" />
Partially Available
</span>
</t>
<t t-if="picking.state == 'cancel'">
<span class="badge badge-danger label-text-align">
<i class="fa fa-fw fa-times" />
Cancelled
</span>
</t>
<t
t-if="picking.state in ['draft', 'waiting', 'confirmed', 'assigned']"
>
<span class="badge badge-info label-text-align">
<i class="fa fa-fw fa-clock-o" />
Preparation
</span>
</t>
</a>
</t>
</ul>
</section>
<section id="description" class="mt-5" t-if="rma.description">
<h3 class="">Description</h3>
<hr class="mt-0 mb-1" />
<t t-out="rma.description" />
</section>
<!-- chatter -->
<div id="rma_communication" class="mt-4">
<h2>Communication</h2>
<t t-call="portal.message_thread">
<t t-set="object" t-value="rma" />
<t t-set="token" t-value="rma.access_token" />
<t t-set="pid" t-value="pid" />
<t t-set="hash" t-value="hash" />
</t>
</div>
</t>
</template>
</odoo>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="rma_tag_view_search" model="ir.ui.view">
<field name="model">rma.tag</field>
<field name="arch" type="xml">
<search string="RMA Tags">
<field name="name" />
<filter
string="Archived"
name="inactive"
domain="[('active','=',False)]"
/>
<filter
string="Active"
name="active"
domain="[('active','!=',False)]"
/>
</search>
</field>
</record>
<record id="view_rma_tag_form" model="ir.ui.view">
<field name="name">Rma Tags</field>
<field name="model">rma.tag</field>
<field name="arch" type="xml">
<form string="RMA Tag">
<sheet>
<group>
<field name="name" />
<field name="is_public" />
</group>
</sheet>
</form>
</field>
</record>
<record id="view_rma_tag_list" model="ir.ui.view">
<field name="name">RMA Tags</field>
<field name="model">rma.tag</field>
<field eval="6" name="priority" />
<field name="arch" type="xml">
<list>
<field name="name" />
<field name="is_public" />
<field name="active" />
</list>
</field>
</record>
<record id="action_rma_tag" model="ir.actions.act_window">
<field name="name">RMA Tags</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">rma.tag</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new RMA tag
</p>
<p>
Manage RMA tags to better classify them for tracking and analysis purposes.
</p>
</field>
</record>
<menuitem
id="rma_configuration_rma_tag_menu"
name="RMA Tags"
parent="rma_configuration_menu"
action="action_rma_tag"
/>
</odoo>

View File

@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="rma_team_view_tree" model="ir.ui.view">
<field name="model">rma.team</field>
<field name="arch" type="xml">
<list>
<field name="sequence" widget="handle" />
<field name="name" />
<field name="user_id" />
<field name="company_id" groups="base.group_multi_company" />
<field name="company_id" column_invisible="1" />
</list>
</field>
</record>
<record id="rma_team_view_form" model="ir.ui.view">
<field name="name">rma.team.view.form</field>
<field name="model">rma.team</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only" string="RMA Team" />
<h1>
<field name="name" />
</h1>
</div>
<group>
<group>
<field name="user_id" />
<field
name="company_id"
options="{'no_create': True}"
groups="base.group_multi_company"
/>
<field name="company_id" invisible="1" />
</group>
</group>
<notebook>
<page name="members" string="Team Members">
<field
name="member_ids"
widget="many2many"
options="{'not_delete': True}"
>
<kanban
quick_create="false"
create="true"
delete="true"
>
<field name="id" />
<field name="name" />
<templates>
<t t-name="card" class="flex-row">
<aside>
<field
name="avatar_128"
widget="image"
class="o_image_64_cover"
alt="Avatar"
/>
</aside>
<main class="ms-3">
<field
name="name"
class="fw-bold fs-5"
/>
<div
class="d-flex align-items-baseline text-break"
>
<i
class="fa fa-envelope me-1"
role="img"
aria-label="Email"
title="Email"
/>
<field name="email" />
</div>
</main>
</t>
</templates>
</kanban>
</field>
</page>
<page name="emails" string="Email" invisible="not alias_domain">
<group name="group_alias">
<label for="alias_name" string="Email Alias" />
<div name="alias_def">
<field
name="alias_id"
class="oe_read_only oe_inline"
string="Email Alias"
required="0"
/>
<div
class="oe_edit_only oe_inline"
name="edit_alias"
style="display: inline;"
>
<field
name="alias_name"
class="oe_inline"
/>@<field
name="alias_domain"
class="oe_inline"
readonly="1"
/>
</div>
</div>
<field
name="alias_contact"
class="oe_inline oe_edit_only"
string="Accept Emails From"
/>
</group>
</page>
</notebook>
</sheet>
<chatter />
</form>
</field>
</record>
<record id="rma_team_action" model="ir.actions.act_window">
<field name="name">RMA team</field>
<field name="res_model">rma.team</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Click to add a new RMA.
</p>
</field>
</record>
<menuitem
id="rma_configuration_rma_team_menu"
name="RMA Team"
parent="rma_configuration_menu"
action="rma_team_action"
/>
</odoo>

452
rma/views/rma_views.xml Normal file
View File

@@ -0,0 +1,452 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
Copyright 2023 Tecnativa - Pedro M. Baeza
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="rma_view_search" model="ir.ui.view">
<field name="name">rma.view.search</field>
<field name="model">rma</field>
<field name="arch" type="xml">
<search>
<field name="name" />
<field name="origin" />
<field name="user_id" />
<field name="product_id" />
<field name="tag_ids" />
<filter
string="Awaiting Action"
name="waiting_action"
domain="[('state', 'in', ['waiting_return', 'waiting_replacement', 'confirmed'])]"
/>
<filter
string="Processed"
name="processed"
domain="[('state', 'in', ['received', 'refunded', 'replaced', 'finished'])]"
/>
<filter
string="Closed"
name="closed"
domain="[('state', 'in', ['locked', 'cancelled'])]"
/>
<separator />
<filter
name="draft_filter"
string="Draft"
domain="[('state','=', 'draft')]"
/>
<filter
name="confirmed_filter"
string="Confirmed"
domain="[('state','=', 'confirmed')]"
/>
<filter
name="received_filter"
string="Received"
domain="[('state','=', 'received')]"
/>
<separator />
<filter
string="Unresolved RMAs"
name="undone_rma"
domain="[('state', 'not in', ['refunded', 'returned', 'replaced', 'locked', 'cancelled', 'finished'])]"
help="RMAs yet to be fully processed"
/>
<filter
string="Late RMAs"
name="late_rma"
domain="[('deadline', '&lt;', context_today().strftime('%Y-%m-%d')), ('state', 'not in', ['refunded', 'returned', 'replaced', 'locked', 'cancelled', 'finished'])]"
help="RMAs which deadline has passed"
/>
<separator />
<filter string="RMA Date" name="filter_rma_date" date="date" />
<filter
string="RMA Deadline"
name="filter_rma_deadline"
date="deadline"
/>
<filter
name="no_user_id_filter"
string="Unassigned RMAs"
domain="[('user_id','=', False)]"
/>
<group string="Group By" name="group_by">
<filter
string="Partner"
name="partner_id_group_by"
context="{'group_by':'partner_id'}"
/>
<filter
string="Responsible"
name="user_id_group_by"
context="{'group_by':'user_id'}"
/>
<filter
string="State"
name="state_group_by"
context="{'group_by':'state'}"
/>
<filter
string="Date"
name="date_group_by"
context="{'group_by':'date'}"
/>
<filter
string="Deadline"
name="deadline_group_by"
context="{'group_by':'deadline'}"
/>
</group>
</search>
</field>
</record>
<record id="rma_view_tree" model="ir.ui.view">
<field name="name">rma.view.tree</field>
<field name="model">rma</field>
<field name="arch" type="xml">
<list
decoration-muted="state in ['cancelled', 'locked']"
decoration-bf="state == 'draft' and product_id == False"
decoration-danger="deadline and (deadline &lt; current_date)"
>
<field name="name" width="100px" />
<field name="origin" />
<field name="user_id" />
<field name="partner_id" />
<field name="product_id" />
<field name="product_uom_qty" />
<field name="product_uom" groups="uom.group_uom" />
<field name="date" />
<field name="deadline" />
<field name="finalization_id" optional="hide" />
<field name="state" />
</list>
</field>
</record>
<record id="rma_view_form" model="ir.ui.view">
<field name="name">rma.view.form</field>
<field name="model">rma</field>
<field name="arch" type="xml">
<form>
<header>
<button
name="%(portal.portal_share_action)d"
string="Share"
type="action"
class="oe_highlight oe_read_only"
/>
<button
type="object"
string="Send by Email"
name="action_rma_send"
invisible="sent or state not in ['draft', 'confirmed', 'received']"
class="btn-primary"
/>
<button
type="object"
string="Send by Mail"
name="action_rma_send"
invisible="not sent or state not in ['draft', 'confirmed', 'received']"
/>
<button
type="object"
string="Confirm"
name="action_confirm"
invisible="state != 'draft'"
class="btn-primary"
/>
<button
type="object"
string="To Refund"
name="action_refund"
invisible="not can_be_refunded"
class="btn-primary"
/>
<button
type="object"
string="Replace"
name="action_replace"
invisible="not can_be_replaced"
class="btn-primary"
/>
<button
type="object"
string="Return to customer"
name="action_return"
invisible="not can_be_returned"
class="btn-primary"
/>
<button
type="object"
string="Split"
name="action_split"
invisible="not can_be_split"
/>
<button
type="object"
string="Cancel"
name="action_cancel"
confirm="Are you sure you want to cancel this RMA"
invisible="state not in ['draft', 'confirmed']"
/>
<button
type="object"
string="Set to draft"
name="action_draft"
invisible="state != 'cancelled'"
/>
<button
type="object"
string="Lock"
name="action_lock"
invisible="not can_be_locked"
/>
<button
type="object"
string="Unlock"
name="action_unlock"
invisible="state != 'locked'"
/>
<button type="object" string="Preview" name="action_preview" />
<field
name="state"
widget="statusbar"
statusbar_visible="draft,confirmed,received"
/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button
type="object"
name="action_view_receipt"
string="Receipt"
class="oe_stat_button"
icon="fa-truck"
invisible="not reception_move_id"
/>
<button
type="object"
name="action_view_delivery"
class="oe_stat_button"
icon="fa-truck"
invisible="delivery_picking_count == 0"
>
<field
name="delivery_picking_count"
widget="statinfo"
string="Delivery"
/>
</button>
<button
type="object"
string="Refund"
name="action_view_refund"
class="oe_stat_button"
icon="fa-pencil-square-o"
invisible="not refund_id"
/>
</div>
<div class="oe_title">
<h1>
<field name="name" readonly="state != 'draft'" />
</h1>
</div>
<group>
<group>
<field
name="partner_id"
widget="res_partner_many2one"
context="{'search_default_customer':1, 'show_address': 1, 'show_vat': True}"
readonly="state != 'draft'"
options="{'always_reload': True}"
/>
<field
name="partner_shipping_id"
readonly="state != 'draft'"
force_save="1"
/>
<field
name="partner_invoice_id"
readonly="state not in ['draft', 'confirmed', 'received']"
force_save="1"
/>
<field
name="picking_id"
options="{'no_create': True}"
readonly="state != 'draft'"
/>
<field
name="move_id"
required="picking_id"
readonly="not picking_id or state != 'draft'"
options="{'no_create': True}"
force_save="1"
/>
<field
name="product_id"
force_save="1"
readonly="picking_id or state != 'draft'"
/>
<field name="uom_category_id" invisible="1" />
<label for="product_uom_qty" />
<div class="o_row">
<field
name="product_uom_qty"
readonly="state != 'draft'"
force_save="1"
/>
<field
name="product_uom"
groups="uom.group_uom"
domain="[('category_id', '=', uom_category_id)]"
readonly="state != 'draft'"
force_save="1"
/>
<field name="product_uom" invisible="1" />
</div>
<field
name="delivered_qty"
invisible="delivered_qty == 0.0"
/>
</group>
<group>
<field name="date" readonly="state != 'draft'" />
<field
name="user_id"
readonly="state in ['locked', 'cancelled']"
/>
<field
name="team_id"
readonly="state in ['locked', 'cancelled']"
/>
<field
name="tag_ids"
widget="many2many_tags"
options="{'color_field': 'color', 'no_create_edit': True}"
placeholder="Tags..."
/>
<field
name="origin"
readonly="state in ['locked', 'cancelled']"
/>
<field name="operation_id" />
<field
name="finalization_id"
readonly="1"
invisible="state != 'finished'"
/>
<field
name="company_id"
options="{'no_create': True}"
groups="base.group_multi_company"
readonly="state in ['locked', 'cancelled']"
/>
<field name="company_id" invisible="1" />
</group>
</group>
<notebook>
<page name="page_other" string="Other Information">
<group>
<group>
<field
name="procurement_group_id"
readonly="state not in ['draft', 'confirmed', 'received']"
/>
<field
name="location_id"
options="{'no_create': True, 'no_open': True}"
groups="stock.group_stock_multi_locations"
readonly="state != 'draft'"
/>
<field name="location_id" invisible="1" />
</group>
<group>
<field
name="deadline"
readonly="state in ['locked', 'cancelled']"
/>
<field
name="priority"
widget="priority"
readonly="state != 'draft'"
/>
</group>
</group>
<group>
<field
name="description"
widget="html"
colspan="4"
readonly="state in ['locked', 'cancelled']"
/>
</group>
</page>
</notebook>
</sheet>
<chatter />
</form>
</field>
</record>
<record id="rma_finalization_form" model="ir.ui.view">
<field name="model">rma</field>
<field name="inherit_id" ref="rma.rma_view_form" />
<field name="arch" type="xml">
<xpath
expr="//form//header//button[@name='action_cancel']"
position="before"
>
<button
type="object"
string="Finish"
name="action_finish"
class="btn-primary"
invisible="not can_be_finished"
groups="rma.group_rma_manual_finalization"
/>
</xpath>
</field>
</record>
<record id="rma_view_pivot" model="ir.ui.view">
<field name="name">rma.pivot</field>
<field name="model">rma</field>
<field name="arch" type="xml">
<pivot>
<field name="date" type="row" />
<field name="product_uom_qty" type="measure" />
<field name="delivered_qty" type="measure" />
</pivot>
</field>
</record>
<record id="rma_view_calendar" model="ir.ui.view">
<field name="name">rma.calendar</field>
<field name="model">rma</field>
<field name="arch" type="xml">
<calendar date_start="date" mode="month" color="state">
<field name="name" />
<field name="partner_id" />
<field name="product_id" />
<field name="product_uom_qty" widget="monetary" />
</calendar>
</field>
</record>
<record id="rma_refund_action_server" model="ir.actions.server">
<field name="name">To Refund</field>
<field name="model_id" ref="model_rma" />
<field name="binding_model_id" ref="model_rma" />
<field name="state">code</field>
<field name="code">records.action_refund()</field>
</record>
<record id="rma_action" model="ir.actions.act_window">
<field name="name">RMA</field>
<field name="res_model">rma</field>
<field name="view_mode">list,form,pivot,calendar,activity</field>
<field name="context">{}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Click to add a new RMA.
</p>
</field>
</record>
<record id="rma_orders_menu" model="ir.ui.menu">
<field name="action" ref="rma_action" />
</record>
</odoo>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_picking_form" model="ir.ui.view">
<field name="name">stock.picking.form</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form" />
<field name="arch" type="xml">
<div name="button_box">
<button
name="action_view_rma"
type="object"
class="oe_stat_button"
icon="fa-reply"
invisible="rma_count == 0"
groups="rma.rma_group_user_own"
>
<field name="rma_count" widget="statinfo" string="RMA" />
</button>
</div>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_warehouse_inherit_mrp" model="ir.ui.view">
<field name="name">Stock Warehouse Inherit MRP</field>
<field name="model">stock.warehouse</field>
<field name="inherit_id" ref="stock.view_warehouse" />
<field name="arch" type="xml">
<xpath expr="//field[@name='wh_output_stock_loc_id']/..">
<field name="rma_loc_id" groups="rma.rma_group_user_own" />
</xpath>
<xpath expr="//field[@name='out_type_id']/..">
<field name="rma_in_type_id" groups="rma.rma_group_user_own" />
<field name="rma_out_type_id" groups="rma.rma_group_user_own" />
</xpath>
</field>
</record>
</odoo>

6
rma/wizard/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import rma_delivery
from . import rma_finalization_wizard
from . import rma_split
from . import stock_picking_return

View File

@@ -0,0 +1,95 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
from odoo.exceptions import ValidationError
class RmaReDeliveryWizard(models.TransientModel):
_name = "rma.delivery.wizard"
_description = "RMA Delivery Wizard"
rma_count = fields.Integer()
type = fields.Selection(
selection=[("replace", "Replace"), ("return", "Return to customer")],
required=True,
)
product_id = fields.Many2one(
comodel_name="product.product",
string="Replace Product",
)
product_uom_qty = fields.Float(
string="Product qty",
digits="Product Unit of Measure",
)
product_uom = fields.Many2one(comodel_name="uom.uom", string="Unit of measure")
scheduled_date = fields.Datetime(required=True, default=fields.Datetime.now)
warehouse_id = fields.Many2one(
comodel_name="stock.warehouse",
string="Warehouse",
required=True,
)
uom_category_id = fields.Many2one(related="product_id.uom_id.category_id")
rma_return_grouping = fields.Boolean(
string="Group RMA returns by customer address and warehouse",
default=lambda self: self.env.company.rma_return_grouping,
)
@api.constrains("product_uom_qty")
def _check_product_uom_qty(self):
self.ensure_one()
rma_ids = self.env.context.get("active_ids")
if len(rma_ids) == 1 and self.product_uom_qty <= 0:
raise ValidationError(self.env._("Quantity must be greater than 0."))
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
rma_ids = self.env.context.get("active_ids")
rma = self.env["rma"].browse(rma_ids)
warehouse_id = (
self.env["stock.warehouse"]
.search([("company_id", "=", rma[0].company_id.id)], limit=1)
.id
)
delivery_type = self.env.context.get("rma_delivery_type")
product_id = False
if len(rma) == 1 and delivery_type == "return":
product_id = rma.product_id.id
product_uom_qty = 0.0
if len(rma) == 1 and rma.remaining_qty > 0.0:
product_uom_qty = rma.remaining_qty
res.update(
rma_count=len(rma),
warehouse_id=warehouse_id,
type=delivery_type,
product_id=product_id,
product_uom_qty=product_uom_qty,
)
return res
@api.onchange("product_id")
def _onchange_product_id(self):
if self.product_id:
if not self.product_uom or self.product_id.uom_id.id != self.product_uom.id:
self.product_uom = self.product_id.uom_id
def action_deliver(self):
self.ensure_one()
rma_ids = self.env.context.get("active_ids")
rma = self.env["rma"].browse(rma_ids)
if self.type == "replace":
rma.create_replace(
self.scheduled_date,
self.warehouse_id,
self.product_id,
self.product_uom_qty,
self.product_uom,
)
elif self.type == "return":
qty = uom = None
if self.rma_count == 1:
qty, uom = self.product_uom_qty, self.product_uom
rma.with_context(
rma_return_grouping=self.rma_return_grouping
).create_return(self.scheduled_date, qty, uom)

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
Copyright 2023 Tecnativa - Pedro M. Baeza
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="rma_redelivery_wizard_view_form" model="ir.ui.view">
<field name="name">rma.delivery.wizard.form</field>
<field name="model">rma.delivery.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<group>
<field name="scheduled_date" />
<field name="warehouse_id" invisible="type != 'replace'" />
<field
name="rma_return_grouping"
invisible="type == 'replace' or rma_count == 1"
/>
</group>
<group>
<field
name="product_id"
invisible="type != 'replace' or rma_count > 1"
required="type == 'replace' and rma_count == 1"
/>
<label for="product_uom_qty" invisible="rma_count > 1" />
<div class="o_row" invisible="rma_count > 1">
<field name="product_uom_qty" required="rma_count == 1" />
<field
name="product_uom"
groups="uom.group_uom"
required="rma_count == 1"
domain="[('category_id', '=', uom_category_id)]"
/>
<field name="product_uom" invisible="1" />
</div>
</group>
</group>
<footer>
<button
name="action_deliver"
string="Deliver"
type="object"
class="btn-primary"
/>
<button string="Cancel" class="btn-secondary" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="rma_delivery_wizard_action" model="ir.actions.act_window">
<field name="name">Return to customer</field>
<field name="res_model">rma.delivery.wizard</field>
<field name="view_mode">form</field>
<field name="binding_model_id" ref="rma.model_rma" />
<field name="binding_view_types">list</field>
<field name="target">new</field>
<field name="context">{'rma_delivery_type': 'return'}</field>
</record>
</odoo>

View File

@@ -0,0 +1,18 @@
# Copyright 2022 Tecnativa - David Vidal
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class RmaFinalizationWizard(models.TransientModel):
_name = "rma.finalization.wizard"
_description = "RMA Finalization Wizard"
finalization_id = fields.Many2one(
comodel_name="rma.finalization", string="Reason", required=True
)
def action_finish(self):
self.ensure_one()
rma_ids = self.env.context.get("active_ids")
rma = self.env["rma"].browse(rma_ids)
rma.write({"finalization_id": self.finalization_id, "state": "finished"})

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2022 Tecnativa - David Vidal
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="rma_finalization_wizard_view_form" model="ir.ui.view">
<field name="model">rma.finalization.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<group>
<field name="finalization_id" />
</group>
</group>
<footer>
<button
name="action_finish"
string="Finish RMA"
type="object"
class="btn-primary"
/>
<button string="Cancel" class="btn-secondary" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="rma_finalization_wizard_action" model="ir.actions.act_window">
<field name="name">Finish RMA Manualy</field>
<field name="res_model">rma.finalization.wizard</field>
<field name="view_mode">form</field>
<field name="binding_model_id" ref="rma.model_rma" />
<field name="binding_view_types">list</field>
<field name="target">new</field>
</record>
</odoo>

70
rma/wizard/rma_split.py Normal file
View File

@@ -0,0 +1,70 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class RmaReSplitWizard(models.TransientModel):
_name = "rma.split.wizard"
_description = "RMA Split Wizard"
rma_id = fields.Many2one(
comodel_name="rma",
string="RMA",
)
product_uom_qty = fields.Float(
string="Quantity to extract",
digits="Product Unit of Measure",
required=True,
help="Quantity to extract to a new RMA.",
)
product_uom = fields.Many2one(
comodel_name="uom.uom",
string="Unit of measure",
required=True,
)
_sql_constraints = [
(
"check_product_uom_qty_positive",
"CHECK(product_uom_qty > 0)",
"Quantity must be greater than 0.",
),
]
@api.model
def fields_get(self, allfields=None, attributes=None):
res = super().fields_get(allfields, attributes=attributes)
rma_id = self.env.context.get("active_id")
rma = self.env["rma"].browse(rma_id)
res["product_uom"]["domain"] = [
("category_id", "=", rma.product_uom.category_id.id)
]
return res
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
rma_id = self.env.context.get("active_id")
rma = self.env["rma"].browse(rma_id)
res.update(
rma_id=rma.id,
product_uom_qty=rma.remaining_qty,
product_uom=rma.product_uom.id,
)
return res
def action_split(self):
self.ensure_one()
extracted_rma = self.rma_id.extract_quantity(
self.product_uom_qty, self.product_uom
)
return {
"name": self.env._("Extracted RMA"),
"type": "ir.actions.act_window",
"view_type": "form",
"view_mode": "form",
"res_model": "rma",
"views": [(self.env.ref("rma.rma_view_form").id, "form")],
"res_id": extracted_rma.id,
}

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
Copyright 2023 Tecnativa - Pedro M. Baeza
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="rma_split_wizard_view_form2" model="ir.ui.view">
<field name="name">rma.split.wizard.form</field>
<field name="model">rma.split.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<group>
<label for="product_uom_qty" />
<div class="o_row">
<field name="product_uom_qty" />
<field name="product_uom" groups="uom.group_uom" />
</div>
</group>
</group>
<footer>
<button
name="action_split"
string="Split"
type="object"
class="btn-primary"
/>
<button string="Cancel" class="btn-secondary" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="rma_split_wizard_action" model="ir.actions.act_window">
<field name="name">Split RMA</field>
<field name="res_model">rma.split.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View File

@@ -0,0 +1,145 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
# Copyright 2025 Tecnativa - Víctor Martínez
# 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_is_zero
class ReturnPickingLine(models.TransientModel):
_inherit = "stock.return.picking.line"
rma_operation_id = fields.Many2one(
comodel_name="rma.operation",
string="Operation",
compute="_compute_rma_operation_id",
store=True,
readonly=False,
)
@api.depends("wizard_id.rma_operation_id")
def _compute_rma_operation_id(self):
for rec in self:
if rec.wizard_id.rma_operation_id:
rec.rma_operation_id = rec.wizard_id.rma_operation_id
def _prepare_rma_vals(self):
self.ensure_one()
warehouse = self.move_id.picking_id.picking_type_id.warehouse_id
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": warehouse.rma_loc_id.id,
"operation_id": self.rma_operation_id.id,
}
class ReturnPicking(models.TransientModel):
_inherit = "stock.return.picking"
create_rma = fields.Boolean(string="Create RMAs")
picking_type_code = fields.Selection(related="picking_id.picking_type_id.code")
rma_operation_id = fields.Many2one(
comodel_name="rma.operation",
string="Requested operation",
)
@api.onchange("create_rma")
def _onchange_create_rma(self):
if self.create_rma:
# We want to avoid setting the return move `to_refund` as it will change
# the delivered quantities in the sale and set them to invoice.
self.product_return_moves.to_refund = False
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_is_zero(
line.quantity, precision_rounding=line.uom_id.rounding
):
continue
vals = deepcopy(global_vals)
vals.update(line._prepare_rma_vals())
vals_list.append(vals)
return vals_list
def action_create_returns(self):
"""Override create_returns method for creating one or more
'confirmed' RMAs after return a delivery picking in case
'Create RMAs' checkbox is checked in this wizard.
New RMAs will be linked to the delivery picking as the origin
delivery and also RMAs will be linked to the returned picking
as the 'Receipt'.
"""
if self.create_rma:
if not self.picking_id.partner_id:
raise ValidationError(
self.env._(
"You must specify the 'Customer' in the "
"'Stock Picking' from which RMAs will be created"
)
)
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": self.env._("Returned Picking"),
"view_mode": "form,list,calendar",
"res_model": "stock.picking",
"res_id": picking.id,
"type": "ir.actions.act_window",
"context": ctx,
}
return super().action_create_returns()

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
Copyright 2023 Tecnativa - Pedro M. Baeza
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_stock_return_picking_form" model="ir.ui.view">
<field name="name">Return lines inherit RMA</field>
<field name="model">stock.return.picking</field>
<field name="inherit_id" ref="stock.view_stock_return_picking_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='product_return_moves']//list" position="inside">
<field
name="rma_operation_id"
required="parent.create_rma and quantity>0"
column_invisible="not parent.create_rma"
/>
</xpath>
<field name="product_return_moves" position="before">
<group name="group_rma">
<field
name="create_rma"
invisible="picking_type_code != 'outgoing'"
/>
<field name="rma_operation_id" invisible="not create_rma" />
</group>
</field>
</field>
</record>
</odoo>