Merge PR #294 into 15.0

Signed-off-by pedrobaeza
This commit is contained in:
OCA-git-bot
2022-08-18 18:09:58 +00:00
65 changed files with 23844 additions and 0 deletions

189
rma/README.rst Normal file
View File

@@ -0,0 +1,189 @@
===========================================
Return Merchandise Authorization Management
===========================================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/15.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-15-0/rma-15-0-rma
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/145/15.0
:alt: Try me on Runbot
|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:
#. Go to *Settings > General Settings*.
#. Check 'External Email Servers' checkbox under *Discuss* section.
#. Set an 'alias domain' and an incoming server.
#. Go to *RMA > Configuration > RMA Team* and select a team or create a new
one.
#. Go to 'Email' tab and set an 'Email Alias'.
If you want to manually finish RMAs, you need to:
#. Go to *Settings > Inventory*.
#. 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:
#. Go to *Settings > Inventory*.
#. 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:
#. Go to *RMA > Orders* and create a new RMA.
#. 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.
#. You will see an smart button labeled 'Receipt'. Click on that button to see
the reception operation form.
#. If everything is right, validate the operation and go back to the RMA to
see it in a 'received' state.
#. 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 '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.
#. 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:
#. Select a delivery order and click on 'Return' button to create a return.
#. Check "Create RMAs" checkbox in the returning wizard, select the RMA
stock location and click on 'Return' button.
#. 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):
#. Go to *RMA > Configuration > RMA Teams*
#. Create a new team and assign a name, a responsible and members.
#. 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.
#. 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.
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 smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/rma/issues/new?body=module:%20rma%0Aversion:%2015.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>
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-ernestotejeda| image:: https://github.com/ernestotejeda.png?size=40px
:target: https://github.com/ernestotejeda
:alt: ernestotejeda
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-ernestotejeda|
This module is part of the `OCA/rma <https://github.com/OCA/rma/tree/15.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

39
rma/__manifest__.py Normal file
View File

@@ -0,0 +1,39 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Return Merchandise Authorization Management",
"summary": "Return Merchandise Authorization (RMA)",
"version": "15.0.1.0.0",
"development_status": "Production/Stable",
"category": "RMA",
"website": "https://github.com/OCA/rma",
"author": "Tecnativa, Odoo Community Association (OCA)",
"maintainers": ["ernestotejeda"],
"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

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

@@ -0,0 +1,136 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# 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, pager as portal_pager
class PortalRma(CustomerPortal):
def _prepare_portal_layout_values(self):
values = super()._prepare_portal_layout_values()
if request.env["rma"].check_access_rights("read", raise_exception=False):
values["rma_count"] = request.env["rma"].search_count([])
else:
values["rma_count"] = 0
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"]
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([picking_sudo.id])[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_rights("read")
picking.check_access_rule("read")
except exceptions.AccessError:
if not access_token or not consteq(rma.access_token, access_token):
raise
return picking_sudo

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

@@ -0,0 +1,145 @@
<?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" ref="report_rma_action" />
<field name="report_name">{{(object.name or '')}}</field>
<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" ref="report_rma_action" />
<field name="report_name">{{(object.name or '')}}</field>
<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" ref="report_rma_action" />
<field name="report_name">{{(object.name or '')}}</field>
<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>

71
rma/hooks.py Normal file
View File

@@ -0,0 +1,71 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import SUPERUSER_ID, api
def post_init_hook(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
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()
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
# 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 sequence per company
for company in env["res.company"].search([]):
company.create_rma_index()

2306
rma/i18n/es.po Normal file

File diff suppressed because it is too large Load Diff

2257
rma/i18n/it.po Normal file

File diff suppressed because it is too large Load Diff

2118
rma/i18n/nl.po Normal file

File diff suppressed because it is too large Load Diff

2190
rma/i18n/pt.po Normal file

File diff suppressed because it is too large Load Diff

2185
rma/i18n/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

2119
rma/i18n/rma.pot Normal file

File diff suppressed because it is too large Load Diff

2176
rma/i18n/ro.po Normal file

File diff suppressed because it is too large Load Diff

2120
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(
_(
"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",
)

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

@@ -0,0 +1,87 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
class Company(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
def create(self, vals):
company = super(Company, self).create(vals)
company.create_rma_index()
return company
def create_rma_index(self):
return (
self.env["ir.sequence"]
.sudo()
.create(
{
"name": _("RMA Code"),
"prefix": "RMA",
"code": "rma",
"padding": 4,
"company_id": self.id,
}
)
)

View File

@@ -0,0 +1,41 @@
# 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",
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",
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",
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.sudo().env.ref("rma.rma_action").read()[0]
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.",
)

1432
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 !"),
]

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

@@ -0,0 +1,64 @@
# 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):
self.ensure_one()
if default is None:
default = {}
if not default.get("name"):
default["name"] = _("%s (copy)") % self.name
team = super().copy(default)
for follower in self.message_follower_ids:
team.message_subscribe(
partner_ids=follower.partner_id.ids,
subtype_ids=follower.subtype_ids.ids,
)
return team
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

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

@@ -0,0 +1,138 @@
# 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 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 create the delivery movement to the customer
rma_id = fields.Many2one(
comodel_name="rma",
string="RMA return",
copy=False,
)
def unlink(self):
# A stock user could have no RMA permissions, so the ids wouldn't
# be accessible due to record rules.
rma_receiver = self.sudo().mapped("rma_receiver_ids")
rma = self.sudo().mapped("rma_id")
res = super().unlink()
rma_receiver.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
if rma_receiver and move.quantity_done != rma_receiver.product_uom_qty:
raise ValidationError(
_(
"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_return_rma_vals(self, original_picking):
"""hook method for preparing an RMA from the 'return picking wizard'."""
self.ensure_one()
partner = original_picking.partner_id
if hasattr(original_picking, "sale_id") and original_picking.sale_id:
partner_invoice_id = original_picking.sale_id.partner_invoice_id.id
partner_shipping_id = original_picking.sale_id.partner_shipping_id.id
else:
partner_invoice_id = partner.address_get(["invoice"]).get("invoice", False)
partner_shipping_id = partner.address_get(["delivery"]).get(
"delivery", False
)
return {
"user_id": self.env.user.id,
"partner_id": partner.id,
"partner_shipping_id": partner_shipping_id,
"partner_invoice_id": partner_invoice_id,
"origin": original_picking.name,
"picking_id": original_picking.id,
"move_id": self.origin_returned_move_id.id,
"product_id": self.origin_returned_move_id.product_id.id,
"product_uom_qty": self.product_uom_qty,
"product_uom": self.product_uom.id,
"reception_move_id": self.id,
"company_id": self.company_id.id,
"location_id": self.location_dest_id.id,
"state": "confirmed",
}
class StockRule(models.Model):
_inherit = "stock.rule"
def _get_custom_move_fields(self):
return super()._get_custom_move_fields() + ["rma_id"]

View File

@@ -0,0 +1,43 @@
# 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_lines.mapped("rma_ids"))
def copy(self, default=None):
self.ensure_one()
if self.env.context.get("set_rma_picking_type"):
location_dest_id = default["location_dest_id"]
warehouse = self.env["stock.warehouse"].search(
[("rma_loc_id", "parent_of", location_dest_id)], limit=1
)
if warehouse:
default["picking_type_id"] = warehouse.rma_in_type_id.id
return super().copy(default)
def action_view_rma(self):
self.ensure_one()
action = self.sudo().env.ref("rma.rma_action").read()[0]
rma = self.move_lines.mapped("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,140 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, 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",
)
rma_out_type_id = fields.Many2one(
comodel_name="stock.picking.type",
string="RMA Out Type",
)
rma_loc_id = fields.Many2one(
comodel_name="stock.location",
string="RMA Location",
)
@api.model_create_multi
def create(self, vals_list):
"""To create an RMA location and link it with a new warehouse,
this method is overridden instead of '_get_locations_values'
method because the locations that are created with the
values returned by that method are forced to be children
of view_location_id, and we don't want that.
"""
res = super().create(vals_list)
stock_location = self.env["stock.location"]
for record in res:
rma_location_vals = record._get_rma_location_values()
record.rma_loc_id = stock_location.create(rma_location_vals).id
return res
def _get_rma_location_values(self):
"""this method is intended to be used by 'create' method
to create a new RMA location to be linked to a new warehouse.
"""
return {
"name": self.view_location_id.name,
"active": True,
"return_location": True,
"usage": "internal",
"company_id": self.company_id.id,
"location_id": self.env.ref("rma.stock_location_rma").id,
}
def _get_sequence_values(self):
values = super()._get_sequence_values()
values.update(
{
"rma_in_type_id": {
"name": self.name + " " + _("Sequence RMA in"),
"prefix": self.code + "/RMA/IN/",
"padding": 5,
"company_id": self.company_id.id,
},
"rma_out_type_id": {
"name": self.name + " " + _("Sequence RMA out"),
"prefix": self.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):
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"]
)
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": _("RMA Receipts"),
"code": "incoming",
"use_create_lots": False,
"use_existing_lots": True,
"default_location_src_id": False,
"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": _("RMA Delivery Orders"),
"code": "outgoing",
"use_create_lots": False,
"use_existing_lots": True,
"default_location_src_id": self.rma_loc_id.id,
"default_location_dest_id": False,
"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()
data.update(
{
"rma_in_type_id": {"default_location_dest_id": self.rma_loc_id.id},
"rma_out_type_id": {"default_location_src_id": self.rma_loc_id.id},
}
)
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

21
rma/readme/CONFIGURE.rst Normal file
View File

@@ -0,0 +1,21 @@
If you want RMAs to be created from incoming emails, you need to:
#. Go to *Settings > General Settings*.
#. Check 'External Email Servers' checkbox under *Discuss* section.
#. Set an 'alias domain' and an incoming server.
#. Go to *RMA > Configuration > RMA Team* and select a team or create a new
one.
#. Go to 'Email' tab and set an 'Email Alias'.
If you want to manually finish RMAs, you need to:
#. Go to *Settings > Inventory*.
#. 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:
#. Go to *Settings > Inventory*.
#. 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,9 @@
* `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>

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.

3
rma/readme/ROADMAP.rst Normal file
View File

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

57
rma/readme/USAGE.rst Normal file
View File

@@ -0,0 +1,57 @@
To use this module, you need to:
#. Go to *RMA > Orders* and create a new RMA.
#. 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.
#. You will see an smart button labeled 'Receipt'. Click on that button to see
the reception operation form.
#. If everything is right, validate the operation and go back to the RMA to
see it in a 'received' state.
#. 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 '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.
#. 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:
#. Select a delivery order and click on 'Return' button to create a return.
#. Check "Create RMAs" checkbox in the returning wizard, select the RMA
stock location and click on 'Return' button.
#. 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):
#. Go to *RMA > Configuration > RMA Teams*
#. Create a new team and assign a name, a responsible and members.
#. 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.
#. 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,115 @@
<?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','=',False),('company_id','in',company_ids)]</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','=',False),('company_id','in',company_ids)]</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), ('company_id', '=', 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,537 @@
<?xml version="1.0" encoding="utf-8" ?>
<!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: http://docutils.sourceforge.net/" />
<title>Return Merchandise Authorization Management</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/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: grey; } /* 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 {
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. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" 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" 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" href="https://github.com/OCA/rma/tree/15.0/rma"><img alt="OCA/rma" src="https://img.shields.io/badge/github-OCA%2Frma-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/rma-15-0/rma-15-0-rma"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/145/15.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-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="id1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="id2">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id3">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id1">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="#id2">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 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="#id3">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>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id4">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 smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/rma/issues/new?body=module:%20rma%0Aversion:%2015.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="#id5">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id6">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id7">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>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id8">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">maintainer</a>:</p>
<p><a class="reference external" href="https://github.com/ernestotejeda"><img alt="ernestotejeda" src="https://github.com/ernestotejeda.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/rma/tree/15.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

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

@@ -0,0 +1,803 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.exceptions import UserError, ValidationError
from odoo.tests import Form, TransactionCase, new_test_user, users
class TestRma(TransactionCase):
@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": "product"}
)
account_type = cls.env["account.account.type"].create(
{
"name": "RCV type",
"type": "receivable",
"internal_group": "income",
}
)
cls.account_receiv = cls.env["account.account"].create(
{
"name": "Receivable",
"code": "RCV00",
"user_type_id": account_type.id,
"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
# Ensure grouping
cls.env.company.rma_return_grouping = True
def _create_rma(self, partner=None, product=None, qty=None, location=None):
rma_form = Form(self.env["rma"])
if partner:
rma_form.partner_id = partner
if product:
rma_form.product_id = product
if qty:
rma_form.product_uom_qty = qty
if location:
rma_form.location_id = location
return rma_form.save()
def _create_confirm_receive(
self, partner=None, product=None, qty=None, location=None
):
rma = self._create_rma(partner, product, qty, location)
rma.action_confirm()
rma.reception_move_id.quantity_done = rma.product_uom_qty
rma.reception_move_id.picking_id._action_done()
return rma
def _test_readonly_fields(self, rma):
with Form(rma) as rma_form:
with self.assertRaises(AssertionError):
rma_form.partner_id = self.env["res.partner"]
with self.assertRaises(AssertionError):
rma_form.partner_invoice_id = self.env["res.partner"]
with self.assertRaises(AssertionError):
rma_form.picking_id = self.env["stock.picking"]
with self.assertRaises(AssertionError):
rma_form.move_id = self.env["stock.move"]
with self.assertRaises(AssertionError):
rma_form.product_id = self.env["product.product"]
with self.assertRaises(AssertionError):
rma_form.product_uom_qty = 0
with self.assertRaises(AssertionError):
rma_form.product_uom = self.env["uom.uom"]
with self.assertRaises(AssertionError):
rma_form.location_id = self.env["stock.location"]
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(
recordp=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": "product"}
)
move.product_uom_qty = 20
picking = picking_form.save()
picking.action_confirm()
for move in picking.move_lines:
move.quantity_done = move.product_uom_qty
picking.button_validate()
return picking
class TestRmaCase(TestRma):
def test_onchange(self):
rma_form = Form(self.env["rma"])
# If partner changes, the invoice address is set
rma_form.partner_id = self.partner
self.assertEqual(rma_form.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": "product", "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(
recordp=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
move.product_uom = uom_ten
picking = picking_form.save()
picking._action_done()
rma_form.picking_id = picking
rma_form.move_id = picking.move_lines
self.assertEqual(rma_form.product_id, product_2)
self.assertEqual(rma_form.product_uom_qty, 15)
self.assertEqual(rma_form.product_uom, uom_ten)
# If product changes, unit of measure changes
rma_form.picking_id = self.env["stock.picking"]
rma_form.product_id = self.product
self.assertEqual(rma_form.product_id, self.product)
self.assertEqual(rma_form.product_uom_qty, 15)
self.assertNotEqual(rma_form.product_uom, uom_ten)
self.assertEqual(rma_form.product_uom, self.product.uom_id)
def test_ensure_required_fields_on_confirm(self):
rma = self._create_rma()
with self.assertRaises(ValidationError) as e:
rma.action_confirm()
self.assertEqual(
e.exception.args[0],
"Required field(s):\nCustomer\nShipping Address\nInvoice Address\n"
"Product\nLocation",
)
with Form(rma) as rma_form:
rma_form.partner_id = self.partner
with self.assertRaises(ValidationError) as e:
rma.action_confirm()
self.assertEqual(e.exception.args[0], "Required field(s):\nProduct\nLocation")
with Form(rma) as rma_form:
rma_form.product_id = self.product
rma_form.location_id = self.rma_loc
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")
self._test_readonly_fields(rma)
rma.reception_move_id.quantity_done = 9
with self.assertRaises(ValidationError):
rma.reception_move_id.picking_id._action_done()
rma.reception_move_id.quantity_done = 10
rma.reception_move_id.picking_id._action_done()
self.assertEqual(rma.reception_move_id.picking_id.state, "done")
self.assertEqual(rma.reception_move_id.quantity_done, 10)
self.assertEqual(rma.state, "received")
self._test_readonly_fields(rma)
def test_cancel(self):
# cancel a draft RMA
rma = self._create_rma(self.partner, self.product)
rma.action_cancel()
self.assertEqual(rma.state, "cancelled")
self._test_readonly_fields(rma)
# 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")
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)
self._test_readonly_fields(rma)
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": "product"}
)
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.mapped("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": "product"}
)
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_lines), 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)
self.assertEqual(rma.delivered_qty_done, 0)
self.assertEqual(rma.remaining_qty_to_done, 10)
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": "product"}
)
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)
self.assertEqual(rma.delivered_qty_done, 0)
self.assertEqual(rma.remaining_qty_to_done, 10)
# remaining_qty is 0 but rma is not set to 'replaced' until
# remaining_qty_to_done is less than or equal to 0
first_move.quantity_done = 2
second_move.quantity_done = 8
picking.button_validate()
self.assertEqual(picking.state, "done")
self.assertEqual(rma.delivered_qty, 10)
self.assertEqual(rma.remaining_qty, 0)
self.assertEqual(rma.delivered_qty_done, 10)
self.assertEqual(rma.remaining_qty_to_done, 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)
self._test_readonly_fields(rma)
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_lines), 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)
self.assertEqual(rma.delivered_qty_done, 0)
self.assertEqual(rma.remaining_qty_to_done, 10)
first_move = rma.delivery_move_ids
picking = first_move.picking_id
# Validate the picking
first_move.quantity_done = 2
picking.button_validate()
self.assertEqual(picking.state, "done")
self.assertEqual(rma.delivered_qty, 2)
self.assertEqual(rma.remaining_qty, 8)
self.assertEqual(rma.delivered_qty_done, 2)
self.assertEqual(rma.remaining_qty_to_done, 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_done = 8
self.assertEqual(rma.delivered_qty, 10)
self.assertEqual(rma.remaining_qty, 0)
self.assertEqual(rma.delivered_qty_done, 2)
self.assertEqual(rma.remaining_qty_to_done, 8)
self.assertEqual(rma.state, "waiting_return")
# remaining_qty is 0 but rma is not set to 'returned' until
# remaining_qty_to_done is less than or equal to 0
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)
self.assertEqual(rma.delivered_qty_done, 10)
self.assertEqual(rma.remaining_qty_to_done, 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)
self._test_readonly_fields(rma)
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": "product"}
)
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()
# 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(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_lines), 3)
self.assertEqual(
pick_1.move_lines.mapped("rma_id"),
(rma_1 | rma_2 | rma_3),
)
self.assertEqual(
(rma_1 | rma_2 | rma_3).mapped("delivery_move_ids"),
pick_1.move_lines,
)
# rma_4 is linked with the unique move of pick_2
self.assertEqual(len(pick_2.move_lines), 1)
self.assertEqual(pick_2.move_lines.rma_id, rma_4)
self.assertEqual(rma_4.delivery_move_ids, pick_2.move_lines)
# 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_done = 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": "product"}
)
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
return_wizard = stock_return_picking_form.save()
picking_action = return_wizard.create_returns()
# Each origin move is linked to a different RMA
origin_moves = origin_delivery.move_lines
self.assertTrue(origin_moves[0].rma_ids)
self.assertTrue(origin_moves[1].rma_ids)
rmas = origin_moves.mapped("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_lines
self.assertTrue(reception_moves[0].rma_receiver_ids)
self.assertTrue(reception_moves[1].rma_receiver_ids)
self.assertEqual(reception_moves.mapped("rma_receiver_ids"), rmas)
# Validate the reception picking to set rmas to 'received' state
reception_moves[0].quantity_done = reception_moves[0].product_uom_qty
reception_moves[1].quantity_done = reception_moves[1].product_uom_qty
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_lines.filtered(
lambda r: r.product_id == self.product
)
rma = rma_form.save()
rma.action_confirm()
rma.reception_move_id.quantity_done = 10
rma.reception_move_id.picking_id._action_done()
# 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_done = 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.delivered_qty_done, 0)
self.assertEqual(new_rma.remaining_qty_to_done, 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_done, 10)
self.assertEqual(new_rma.reception_move_id.quantity_done, 10)
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_done = 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>

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

@@ -0,0 +1,137 @@
<?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>
<div class="row mt32 mb32" id="product_information">
<div t-if="doc.picking_id" class="col-auto mw-100 mb-2">
<strong>Origin delivery:</strong>
<p class="m-0" t-field="doc.picking_id" />
</div>
<div t-if="doc.move_id" class="col-auto mw-100 mb-2">
<strong>Move:</strong>
<p class="m-0" t-field="doc.move_id" />
</div>
<div t-if="doc.product_id" class="col-auto mw-100 mb-2">
<strong>Product:</strong>
<p class="m-0" t-field="doc.product_id" />
</div>
<div t-if="doc.product_id" class="col-auto mw-100 mb-2">
<strong>Quantity:</strong>
<p class="m-0" t-field="doc.product_uom_qty">
<span t-field="doc.product_uom_qty" />
<span t-field="doc.uom_id" groups="uom.group_uom" />
</p>
</div>
<div t-if="doc.delivered_qty" class="col-auto mw-100 mb-2">
<strong>Delivered qty:</strong>
<p class="m-0" t-field="doc.delivered_qty" />
</div>
</div>
<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,159 @@
<?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="stock.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath
expr="//div[@data-key='stock']/div[hasclass('o_settings_container')]"
position="inside"
>
<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"
attrs="{'invisible': [('send_rma_confirmation', '=', False)]}"
>
<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"
attrs="{'required': [('send_rma_confirmation', '=', True)]}"
context="{'default_model': 'rma'}"
/>
</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"
attrs="{'invisible': [('send_rma_receipt_confirmation', '=', False)]}"
>
<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"
attrs="{'required': [('send_rma_receipt_confirmation', '=', True)]}"
context="{'default_model': 'rma'}"
/>
</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"
attrs="{'invisible': [('send_rma_draft_confirmation', '=', False)]}"
>
<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"
attrs="{'required': [('send_rma_draft_confirmation', '=', True)]}"
context="{'default_model': 'rma'}"
/>
</div>
</div>
</div>
</xpath>
</field>
</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_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="groups_id" eval="[(4, ref('rma.rma_group_user_own'))]" />
<field name="arch" type="xml">
<div name="button_box">
<button
name="action_view_rma"
type="object"
class="oe_stat_button"
icon="fa-reply"
attrs="{'invisible': [('rma_count', '=', 0)]}"
>
<field name="rma_count" widget="statinfo" string="RMA" />
</button>
</div>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,73 @@
<?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"
attrs="{'invisible': [('active', '=', True)]}"
/>
<group>
<field name="name" />
<field name="company_id" groups="base.group_multi_company" />
<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">
<tree>
<field name="name" />
<field name="company_id" groups="base.group_multi_company" />
</tree>
</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,529 @@
<?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="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 badge-pill badge-secondary">
<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>
<t t-call="portal.portal_record_layout">
<t t-set="card_header">
<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>
</t>
<t t-set="card_body">
<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.image_1024"
class="rounded-circle mt-1 o_portal_contact_img"
t-att-src="image_data_uri(rma.partner_id.image_1024)"
alt="Contact"
/>
<img
t-else=""
class="rounded-circle mt-1 o_portal_contact_img"
src="/web/static/src/img/user_menu_avatar.png"
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.image_1024"
class="rounded-circle mt-1 o_portal_contact_img"
t-att-src="image_data_uri(rma.partner_shipping_id.image_1024)"
alt="Contact"
/>
<img
t-else=""
class="rounded-circle mt-1 o_portal_contact_img"
src="/web/static/src/img/user_menu_avatar.png"
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.image_1024"
class="rounded-circle mt-1 o_portal_contact_img"
t-att-src="image_data_uri(rma.user_id.image_1024)"
alt="Contact"
/>
<img
t-else=""
class="rounded-circle mt-1 o_portal_contact_img"
src="/web/static/src/img/user_menu_avatar.png"
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>
</t>
</t>
<!-- 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">
<tree>
<field name="name" />
<field name="is_public" />
<field name="active" />
</tree>
</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,143 @@
<?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">
<tree>
<field name="sequence" widget="handle" />
<field name="name" />
<field name="user_id" />
<field name="company_id" groups="base.group_multi_company" />
</tree>
</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"
/>
</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="kanban-box">
<div
class="oe_kanban_global_click"
style="max-width: 200px"
>
<div class="o_kanban_record_top">
<img
t-att-src="kanban_image('res.users', 'image_small', record.id.raw_value)"
height="40"
width="40"
class="oe_avatar oe_kanban_avatar_smallbox mb0"
alt="Avatar"
/>
<div
class="o_kanban_record_headings ml8"
>
<strong
class="o_kanban_record_title"
>
<field name="name" />
</strong>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</page>
<page
name="emails"
string="Email"
attrs="{'invisible': [('alias_domain', '=', False)]}"
>
<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>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" />
<field name="message_ids" widget="mail_thread" />
</div>
</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">tree,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>

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

@@ -0,0 +1,407 @@
<?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_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="user_id" />
<field name="tag_ids" />
<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'])]"
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'])]"
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">
<tree
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" />
</tree>
</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"
attrs="{'invisible':['|', ('sent','=',True), ('state', 'not in', ['draft', 'confirmed', 'received'])]}"
class="btn-primary"
/>
<button
type="object"
string="Send by Mail"
name="action_rma_send"
attrs="{'invisible':['|', ('sent','=',False), ('state', 'not in', ['draft', 'confirmed', 'received'])]}"
/>
<button
type="object"
string="Confirm"
name="action_confirm"
states="draft"
class="btn-primary"
/>
<button
type="object"
string="To Refund"
name="action_refund"
attrs="{'invisible': [('can_be_refunded', '=', False)]}"
class="btn-primary"
/>
<button
type="object"
string="Replace"
name="action_replace"
attrs="{'invisible': [('can_be_replaced', '=', False)]}"
class="btn-primary"
/>
<button
type="object"
string="Return to customer"
name="action_return"
attrs="{'invisible': [('can_be_returned', '=', False)]}"
class="btn-primary"
/>
<button
type="object"
string="Split"
name="action_split"
attrs="{'invisible': [('can_be_split', '=', False)]}"
/>
<button
type="object"
string="Cancel"
name="action_cancel"
confirm="Are you sure you want to cancel this RMA"
states="draft,confirmed"
/>
<button
type="object"
string="Set to draft"
name="action_draft"
states="cancelled"
/>
<button
type="object"
string="Lock"
name="action_lock"
attrs="{'invisible': [('can_be_locked', '=', False)]}"
/>
<button
type="object"
string="Unlock"
name="action_unlock"
states="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"
attrs="{'invisible': [('reception_move_id', '=', False)]}"
>
</button>
<button
type="object"
name="action_view_delivery"
class="oe_stat_button"
icon="fa-truck"
attrs="{'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"
attrs="{'invisible': [('refund_id', '=', False)]}"
>
</button>
</div>
<div class="oe_title">
<h1>
<field name="name" readonly="1" />
</h1>
</div>
<group>
<group>
<field
name="partner_id"
widget="res_partner_many2one"
context="{'search_default_customer':1, 'show_address': 1, 'show_vat': True}"
options="{'always_reload': True}"
/>
<field name="partner_shipping_id" />
<field name="partner_invoice_id" />
<field name="picking_id" options="{'no_create': True}" />
<field
name="move_id"
attrs="{'required': [('picking_id', '!=', False)], 'readonly': ['|', ('picking_id', '=', False), ('state', '!=', 'draft')]}"
options="{'no_create': True}"
force_save="1"
/>
<field
name="product_id"
force_save="1"
attrs="{'readonly': ['|', ('picking_id', '!=', False), ('state', '!=', 'draft')]}"
/>
<field name="uom_category_id" invisible="1" />
<label for="product_uom_qty" />
<div class="o_row">
<field name="product_uom_qty" />
<field
name="product_uom"
groups="uom.group_uom"
domain="[('category_id', '=', uom_category_id)]"
/>
</div>
<field
name="delivered_qty"
attrs="{'invisible': [('delivered_qty', '=', 0.0)]}"
/>
</group>
<group>
<field name="date" />
<field name="user_id" />
<field name="team_id" />
<field
name="tag_ids"
widget="many2many_tags"
options="{'color_field': 'color', 'no_create_edit': True}"
placeholder="Tags..."
/>
<field name="origin" />
<field name="operation_id" />
<field
name="finalization_id"
attrs="{'invisible': [('state', '!=', 'finished')]}"
/>
<field
name="company_id"
options="{'no_create': True}"
groups="base.group_multi_company"
/>
</group>
</group>
<notebook>
<page name="page_other" string="Other Information">
<group>
<group>
<field name="procurement_group_id" />
<field
name="location_id"
options="{'no_create': True, 'no_open': True}"
groups="stock.group_stock_multi_locations"
/>
</group>
<group>
<field name="deadline" />
<field name="priority" widget="priority" />
<field
name="origin_split_rma_id"
attrs="{'invisible': [('origin_split_rma_id', '=', False)]}"
/>
</group>
</group>
<group>
<field name="description" widget="html" colspan="4" />
</group>
</page>
</notebook>
<field name="sent" invisible="1" />
<field name="reception_move_id" invisible="1" />
<field name="refund_id" invisible="1" />
<field name="can_be_refunded" invisible="1" />
<field name="can_be_returned" invisible="1" />
<field name="can_be_replaced" invisible="1" />
<field name="can_be_split" invisible="1" />
<field name="can_be_locked" invisible="1" />
<field name="can_be_finished" invisible="1" />
<field name="commercial_partner_id" invisible="1" />
<field name="remaining_qty" invisible="1" />
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" />
<field name="activity_ids" widget="mail_activity" />
<field name="message_ids" widget="mail_thread" />
</div>
</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="groups_id"
eval="[(4, ref('rma.group_rma_manual_finalization'))]"
/>
<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"
attrs="{'invisible': [('can_be_finished', '=', False)]}"
/>
</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" quick_add="False">
<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">tree,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="groups_id" eval="[(4, ref('rma.rma_group_user_own'))]" />
<field name="arch" type="xml">
<div name="button_box">
<button
name="action_view_rma"
type="object"
class="oe_stat_button"
icon="fa-reply"
attrs="{'invisible': [('rma_count', '=', 0)]}"
>
<field name="rma_count" widget="statinfo" string="RMA" />
</button>
</div>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,18 @@
<?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="groups_id" eval="[(4, ref('rma.rma_group_user_own'))]" />
<field name="arch" type="xml">
<xpath expr="//field[@name='wh_output_stock_loc_id']/..">
<field name="rma_loc_id" />
</xpath>
<xpath expr="//field[@name='out_type_id']/..">
<field name="rma_in_type_id" />
<field name="rma_out_type_id" />
</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(_("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,73 @@
<?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_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"
attrs="{'invisible': [('type', '!=', 'replace')]}"
/>
<field
name="rma_return_grouping"
attrs="{'invisible': ['|', ('type', '=', 'replace'), ('rma_count', '=', 1)]}"
/>
</group>
<group>
<field name="uom_category_id" invisible="1" />
<field
name="product_id"
attrs="{'invisible': ['|', ('type', '!=', 'replace'), ('rma_count', '>', 1)], 'required': [('type', '=', 'replace'), ('rma_count', '=', 1)]}"
/>
<label
for="product_uom_qty"
attrs="{'invisible': [('rma_count', '>', 1)]}"
/>
<div
class="o_row"
attrs="{'invisible': [('rma_count', '>', 1)]}"
>
<field
name="product_uom_qty"
attrs="{'required': [('rma_count', '=', 1)]}"
/>
<field
name="product_uom"
groups="uom.group_uom"
attrs="{'required': [('rma_count', '=', 1)]}"
domain="[('category_id', '=', uom_category_id)]"
/>
</div>
</group>
</group>
<field name="rma_count" invisible="1" />
<field name="type" invisible="1" />
<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": _("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,37 @@
<?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_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,72 @@
# 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 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")
@api.onchange("create_rma")
def _onchange_create_rma(self):
if self.create_rma:
warehouse = self.picking_id.picking_type_id.warehouse_id
self.location_id = warehouse.rma_loc_id.id
rma_loc = warehouse.search(
[("company_id", "=", self.picking_id.company_id.id)]
).mapped("rma_loc_id")
rma_loc_domain = [("id", "child_of", rma_loc.ids)]
else:
# If self.create_rma is not True, the value of the location and
# the location domain will be the same as assigned by default.
location_id = self.picking_id.location_id.id
return_picking_type = self.picking_id.picking_type_id.return_picking_type_id
if return_picking_type.default_location_dest_id.return_location:
location_id = return_picking_type.default_location_dest_id.id
self.location_id = location_id
rma_loc_domain = [
"|",
("id", "=", self.picking_id.location_id.id),
"|",
"&",
("return_location", "=", True),
("company_id", "=", False),
"&",
("return_location", "=", True),
("company_id", "=", self.picking_id.company_id.id),
]
return {"domain": {"location_id": rma_loc_domain}}
def 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:
# set_rma_picking_type is to override the copy() method of stock
# picking and change the default picking type to rma picking type
self_with_context = self.with_context(set_rma_picking_type=True)
res = super(ReturnPicking, self_with_context).create_returns()
if not self.picking_id.partner_id:
raise ValidationError(
_(
"You must specify the 'Customer' in the "
"'Stock Picking' from which RMAs will be created"
)
)
returned_picking = self.env["stock.picking"].browse(res["res_id"])
vals_list = [
move._prepare_return_rma_vals(self.picking_id)
for move in returned_picking.move_lines
]
self.env["rma"].create(vals_list)
return res
else:
return super().create_returns()

View File

@@ -0,0 +1,25 @@
<?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_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="//group[.//field[@name='product_return_moves']]"
position="before"
>
<group name="group_rma">
<field
name="create_rma"
attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}"
/>
<field name="picking_id" invisible="1" />
<field name="picking_type_code" invisible="1" />
</group>
</xpath>
</field>
</record>
</odoo>

1
setup/rma/odoo/addons/rma Symbolic link
View File

@@ -0,0 +1 @@
../../../../rma

6
setup/rma/setup.py Normal file
View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)