Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,46 @@
# Return Merchandise Authorization Management
Odoo addon: rma
## Installation
```bash
pip install odoo-bringout-oca-rma-rma
```
## Dependencies
This addon depends on:
- stock_account
## Manifest Information
- **Name**: Return Merchandise Authorization Management
- **Version**: 16.0.5.1.0
- **Category**: RMA
- **License**: AGPL-3
- **Installable**: False
## Source
Based on [OCA/rma](https://github.com/OCA/rma) branch 16.0, addon `rma`.
## License
This package maintains the original AGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Rma Module - rma
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for rma. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,17 @@
# Controllers
HTTP routes provided by this module.
```mermaid
sequenceDiagram
participant U as User/Client
participant C as Module Controllers
participant O as ORM/Views
U->>C: HTTP GET/POST (routes)
C->>O: ORM operations, render templates
O-->>U: HTML/JSON/PDF
```
Notes
- See files in controllers/ for route definitions.

View file

@ -0,0 +1,5 @@
# Dependencies
This addon depends on:
- [stock_account](../../odoo-bringout-oca-ocb-stock_account)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon rma or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-rma-rma"
# or
uv pip install odoo-bringout-oca-rma-rma"
```

View file

@ -0,0 +1,26 @@
# Models
Detected core models and extensions in rma.
```mermaid
classDiagram
class rma
class rma_finalization
class rma_operation
class rma_tag
class rma_team
class account_move
class account_move_line
class res_company
class res_config_settings
class res_partner
class res_users
class stock_move
class stock_picking
class stock_rule
class stock_warehouse
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: rma. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon rma
- License: LGPL-3

View file

@ -0,0 +1,23 @@
# Reports
Report definitions and templates in rma.
```mermaid
classDiagram
```
## Available Reports
### PDF/Document Reports
- **RMA Report** (PDF/Print)
## Report Files
- **report.xml** (XML template/definition)
## Notes
- Named reports above are accessible through Odoo's reporting menu
- Python files define report logic and data processing
- XML files contain report templates, definitions, and formatting
- Reports are integrated with Odoo's printing and email systems

View file

@ -0,0 +1,42 @@
# Security
Access control and security definitions in rma.
## Access Control Lists (ACLs)
Model access permissions defined in:
- **[ir.model.access.csv](../rma/security/ir.model.access.csv)**
- 17 model access rules
## Record Rules
Row-level security rules defined in:
## Security Groups & Configuration
Security groups and permissions defined in:
- **[rma_security.xml](../rma/security/rma_security.xml)**
- 4 security groups defined
```mermaid
graph TB
subgraph "Security Layers"
A[Users] --> B[Groups]
B --> C[Access Control Lists]
C --> D[Models]
B --> E[Record Rules]
E --> F[Individual Records]
end
```
Security files overview:
- **[ir.model.access.csv](../rma/security/ir.model.access.csv)**
- Model access permissions (CRUD rights)
- **[rma_security.xml](../rma/security/rma_security.xml)**
- Security groups, categories, and XML-based rules
Notes
- Access Control Lists define which groups can access which models
- Record Rules provide row-level security (filter records by user/group)
- Security groups organize users and define permission sets
- All security is enforced at the ORM level by Odoo

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon rma
```

View file

@ -0,0 +1,12 @@
# Wizards
Transient models exposed as UI wizards in rma.
```mermaid
classDiagram
class ReturnPicking
class ReturnPickingLine
class RmaFinalizationWizard
class RmaReDeliveryWizard
class RmaReSplitWizard
```

View file

@ -0,0 +1,42 @@
[project]
name = "odoo-bringout-oca-rma-rma"
version = "16.0.0"
description = "Return Merchandise Authorization Management - Return Merchandise Authorization (RMA)"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-rma-stock_account>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["rma"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]

View file

@ -0,0 +1,205 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
===========================================
Return Merchandise Authorization Management
===========================================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:ee42142efd7393e2b2a8fa941b46827ae484f0116729f557a09de20fb9868658
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/license-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/16.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-16-0/rma-16-0-rma
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/rma&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module allows you to manage `Return Merchandise Authorization (RMA)
<https://en.wikipedia.org/wiki/Return_merchandise_authorization>`_.
RMA documents can be created from scratch, from a delivery order or from
an incoming email. Product receptions and returning delivery operations
of the RMA module are fully integrated with the Receipts and Deliveries
Operations of Odoo inventory core module. It also allows you to generate
refunds in the same way as Odoo generates it.
Besides, you have full integration of the RMA documents in the customer portal.
**Table of contents**
.. contents::
:local:
Configuration
=============
If you want RMAs to be created from incoming emails, you need to:
#. 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.
* Add RMA reception and/or RMA delivery on several steps - 2 or 3 - like
normal receptions/deliveries. It should be a separate option inside the
warehouse definition.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/rma/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/rma/issues/new?body=module:%20rma%0Aversion:%2016.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>
* Michael Tietz (MT Software) <mtietz@mt-software.de>
* Jacques-Etienne Baudoux - BCIM <je@bcim.be>
* Souheil Bejaoui - ACSONE SA/NV <souheil.bejaoui@acsone.eu>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
.. |maintainer-pedrobaeza| image:: https://github.com/pedrobaeza.png?size=40px
:target: https://github.com/pedrobaeza
:alt: pedrobaeza
.. |maintainer-chienandalu| image:: https://github.com/chienandalu.png?size=40px
:target: https://github.com/chienandalu
:alt: chienandalu
Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-pedrobaeza| |maintainer-chienandalu|
This module is part of the `OCA/rma <https://github.com/OCA/rma/tree/16.0/rma>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

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

View file

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

View file

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

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>

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>

View file

@ -0,0 +1,82 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import SUPERUSER_ID, api
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(
{"company_id": warehouse.company_id.id}, warehouse.code
)
warehouse.rma_loc_id = (
stock_location.with_context(active_test=False)
.create(rma_location_vals)
.id
)
def create_rma_picking_types(whs):
ir_sequence_sudo = env["ir.sequence"].sudo()
stock_picking_type = env["stock.picking.type"]
color = _get_next_picking_type_color()
stock_picking = stock_picking_type.search(
[("sequence", "!=", False)], limit=1, order="sequence desc"
)
max_sequence = stock_picking.sequence or 0
create_data = whs._get_picking_type_create_values(max_sequence)[0]
sequence_data = whs._get_sequence_values()
data = {}
for picking_type, values in create_data.items():
if (
picking_type in ["rma_in_type_id", "rma_out_type_id"]
and not whs[picking_type]
):
picking_sequence = sequence_data[picking_type]
sequence = ir_sequence_sudo.create(picking_sequence)
values.update(
warehouse_id=whs.id,
color=color,
sequence_id=sequence.id,
)
data[picking_type] = stock_picking_type.create(values).id
if data:
whs.write(data)
whs.rma_in_type_id.return_picking_type_id = whs.rma_out_type_id.id
whs.rma_out_type_id.return_picking_type_id = whs.rma_in_type_id.id
def create_rma_routes(warehouses):
"""Create initially rma in/out stock.location.routes and stock.rules"""
warehouses = warehouses.with_context(rma_post_init_hook=True)
for wh in warehouses:
route_vals = wh._create_or_update_route()
wh.write(route_vals)
# Create rma locations and picking types
warehouses = env["stock.warehouse"].search([])
for warehouse in warehouses:
create_rma_locations(warehouse)
create_rma_picking_types(warehouse)
create_rma_routes(warehouses)
# Create rma sequence per company
for company in env["res.company"].search([]):
company.create_rma_index()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
# Copyright 2024 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openupgradelib import openupgrade
@openupgrade.migrate()
def migrate(env, version):
"""Similar behavior to create_rma_routes of post_init_hook."""
warehouses = env["stock.warehouse"].search([])
warehouses = warehouses.with_context(rma_post_init_hook=True)
for wh in warehouses:
if not wh.rma_in_type_id or not wh.rma_out_type_id:
data = wh._create_or_update_sequences_and_picking_types()
wh.write(data)
route_vals = wh._create_or_update_route()
wh.write(route_vals)

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",
)

View file

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

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

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.",
)

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,163 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from ast import literal_eval
from collections import defaultdict
from odoo import _, api, fields, models
from odoo.osv.expression import AND
PROCESSED_STATES = ["received", "refunded", "replaced", "finished"]
AWAITING_ACTION_STATES = ["waiting_return", "waiting_replacement", "confirmed"]
class RmaOperation(models.Model):
_name = "rma.operation"
_description = "RMA requested operation"
active = fields.Boolean(default=True)
name = fields.Char(required=True, translate=True)
color = fields.Integer()
count_rma_draft = fields.Integer(compute="_compute_count_rma")
count_rma_awaiting_action = fields.Integer(compute="_compute_count_rma")
count_rma_processed = fields.Integer(compute="_compute_count_rma")
action_create_receipt = fields.Selection(
[
("manual_on_confirm", "Manually on Confirm"),
("automatic_on_confirm", "Automatically on Confirm"),
],
string="Create Receipt",
default="automatic_on_confirm",
help="Define how the receipt action should be handled.",
)
different_return_product = fields.Boolean(
help="If checked, allows the return of a product different from the one "
"originally ordered. Used if the delivery is created automatically",
)
auto_confirm_reception = fields.Boolean(
help="Enable this option to automatically confirm the reception when the RMA is"
" confirmed."
)
action_create_delivery = fields.Selection(
[
("manual_on_confirm", "Manually on Confirm"),
("automatic_on_confirm", "Automatically on Confirm"),
("manual_after_receipt", "Manually After Receipt"),
("automatic_after_receipt", "Automatically After Receipt"),
],
string="Delivery Action",
help="Define how the delivery action should be handled.",
default="manual_after_receipt",
)
action_create_refund = fields.Selection(
[
("manual_on_confirm", "Manually on Confirm"),
("automatic_on_confirm", "Automatically on Confirm"),
("manual_after_receipt", "Manually After Receipt"),
("automatic_after_receipt", "Automatically After Receipt"),
("update_quantity", "Update Quantities"),
],
string="Refund Action",
default="manual_after_receipt",
help="Define how the refund action should be handled.",
)
_sql_constraints = [
("name_uniq", "unique (name)", "That operation name already exists !"),
]
@api.model
def _get_rma_draft_domain(self):
return [("state", "=", "draft")]
@api.model
def _get_rma_awaiting_action_domain(self):
return [("state", "in", AWAITING_ACTION_STATES)]
@api.model
def _get_rma_processed_domain(self):
return [("state", "in", PROCESSED_STATES)]
def _compute_count_rma(self):
self.update(
{
"count_rma_draft": 0,
"count_rma_processed": 0,
"count_rma_awaiting_action": 0,
}
)
state_by_op = defaultdict(int)
for group in self.env["rma"].read_group(
AND([[("operation_id", "!=", False)]]),
groupby=["operation_id", "state"],
fields=["id"],
lazy=False,
):
operation_id = group.get("operation_id")[0]
state = group.get("state")
count = group.get("__count")
if state == "draft":
state_by_op[(operation_id, "count_rma_draft")] += count
if state in PROCESSED_STATES:
state_by_op[(operation_id, "count_rma_processed")] += count
if state in AWAITING_ACTION_STATES:
state_by_op[(operation_id, "count_rma_awaiting_action")] += count
for (operation_id, field), count in state_by_op.items():
self.browse(operation_id).update({field: count})
def _get_action(self, name, domain):
action = self.env["ir.actions.actions"]._for_xml_id("rma.rma_action")
action["display_name"] = name
context = {
"search_default_operation_id": [self.id],
"default_operation_id": self.id,
}
action_context = literal_eval(action["context"])
context = {**action_context, **context}
action["context"] = context
action["domain"] = domain
return action
def get_action_rma_tree_draft(self):
self.ensure_one()
name = self.display_name + ": " + _("Draft")
return self._get_action(
name,
domain=AND(
[
[("operation_id", "=", self.id)],
self._get_rma_draft_domain(),
]
),
)
def get_action_rma_tree_awaiting_action(self):
self.ensure_one()
name = self.display_name + ": " + _("Awaiting Action")
return self._get_action(
name,
domain=AND(
[
[("operation_id", "=", self.id)],
self._get_rma_awaiting_action_domain(),
]
),
)
def get_action_rma_tree_processed(self):
self.ensure_one()
name = self.display_name + ": " + _("Processed")
return self._get_action(
name,
domain=AND(
[
[("operation_id", "=", self.id)],
self._get_rma_processed_domain(),
]
),
)
def get_action_all_rma(self):
self.ensure_one()
name = self.display_name
return self._get_action(name, domain=[("operation_id", "=", self.id)])

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

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

View file

@ -0,0 +1,138 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import float_compare
class StockMove(models.Model):
_inherit = "stock.move"
# RMAs that were created from the delivery move
rma_ids = fields.One2many(
comodel_name="rma",
inverse_name="move_id",
string="RMAs",
copy=False,
)
# RMAs linked to the incoming movement from client
rma_receiver_ids = fields.One2many(
comodel_name="rma",
inverse_name="reception_move_id",
string="RMA receivers",
copy=False,
)
# RMA that creates the out move
rma_id = fields.Many2one(
comodel_name="rma", string="RMA return", copy=False, index=True
)
def unlink(self):
# A stock user could have no RMA permissions, so the ids wouldn't
# be accessible due to record rules.
rma_receiver = self.sudo().rma_receiver_ids
rma = self.sudo().rma_id
res = super().unlink()
rma_receiver.filtered(lambda x: x.state != "cancelled").write(
{"state": "draft"}
)
rma.update_received_state()
rma.update_replaced_state()
return res
def _action_cancel(self):
res = super()._action_cancel()
# A stock user could have no RMA permissions, so the ids wouldn't
# be accessible due to record rules.
cancelled_moves = self.filtered(lambda r: r.state == "cancel").sudo()
cancelled_moves.mapped("rma_receiver_ids").write({"state": "draft"})
cancelled_moves.mapped("rma_id").update_received_state()
cancelled_moves.mapped("rma_id").update_replaced_state()
return res
def _action_done(self, cancel_backorder=False):
"""Avoids to validate stock.move with less quantity than the
quantity in the linked receiver RMA. It also set the appropriated
linked RMA to 'received' or 'delivered'.
"""
for move in self.filtered(lambda r: r.state not in ("done", "cancel")):
rma_receiver = move.sudo().rma_receiver_ids
qty_prec = self.env["decimal.precision"].precision_get(
"Product Unit of Measure"
)
if (
rma_receiver
and float_compare(
move.quantity_done,
rma_receiver.product_uom_qty,
precision_digits=qty_prec,
)
!= 0
):
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",
"rma_receiver_ids",
]
def _prepare_move_split_vals(self, qty):
"""Intended to the backport of picking linked to RMAs propagates the
RMA link id.
"""
res = super()._prepare_move_split_vals(qty)
res["rma_id"] = self.sudo().rma_id.id
return res
def _prepare_procurement_values(self):
res = super()._prepare_procurement_values()
if self.rma_id:
res["rma_id"] = self.rma_id.id
return res
class StockRule(models.Model):
_inherit = "stock.rule"
def _get_custom_move_fields(self):
move_fields = super()._get_custom_move_fields()
move_fields += [
"rma_id",
"origin_returned_move_id",
"move_orig_ids",
"rma_receiver_ids",
]
return move_fields

View file

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

View file

@ -0,0 +1,216 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, fields, models
class StockWarehouse(models.Model):
_inherit = "stock.warehouse"
# This is a strategic field used to create an rma location
# and rma operation types in existing warehouses when
# installing this module.
rma = fields.Boolean(
"RMA",
default=True,
help="RMA related products can be stored in this warehouse.",
)
rma_in_type_id = fields.Many2one(
comodel_name="stock.picking.type",
string="RMA In Type",
)
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",
)
rma_in_route_id = fields.Many2one("stock.route", "RMA in Route")
rma_out_route_id = fields.Many2one("stock.route", "RMA out Route")
rma_out_replace_route_id = fields.Many2one("stock.route", "RMA out Replace Route")
def _get_rma_location_values(self, vals, code=False):
"""this method is intended to be used by 'create' method
to create a new RMA location to be linked to a new warehouse.
"""
company_id = vals.get(
"company_id", self.default_get(["company_id"])["company_id"]
)
code = vals.get("code") or code or ""
code = code.replace(" ", "").upper()
view_location_id = vals.get("view_location_id")
view_location = (
view_location_id
and self.view_location_id.browse(view_location_id)
or self.view_location_id
)
return {
"name": view_location.name,
"active": True,
"return_location": True,
"usage": "internal",
"company_id": company_id,
"location_id": self.env.ref("rma.stock_location_rma").id,
"barcode": self._valid_barcode(code + "-RMA", company_id),
}
def _get_locations_values(self, vals, code=False):
res = super()._get_locations_values(vals, code)
res["rma_loc_id"] = self._get_rma_location_values(vals, code)
return res
def _get_sequence_values(self, name=False, code=False):
values = super()._get_sequence_values(name=name, code=code)
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):
res = super()._update_name_and_code(new_name, new_code)
for warehouse in self:
sequence_data = warehouse._get_sequence_values()
warehouse.rma_in_type_id.sequence_id.write(sequence_data["rma_in_type_id"])
warehouse.rma_out_type_id.sequence_id.write(
sequence_data["rma_out_type_id"]
)
return res
def _get_picking_type_create_values(self, max_sequence):
data, next_sequence = super()._get_picking_type_create_values(max_sequence)
data.update(
{
"rma_in_type_id": {
"name": _("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()
picking_types = {
"rma_in_type_id": {"default_location_dest_id": self.rma_loc_id.id},
"rma_out_type_id": {"default_location_src_id": self.rma_loc_id.id},
}
if self.env.context.get("rma_post_init_hook"):
return picking_types
data.update(picking_types)
return data
def _create_or_update_sequences_and_picking_types(self):
data = super()._create_or_update_sequences_and_picking_types()
stock_picking_type = self.env["stock.picking.type"]
if "out_type_id" in data:
rma_out_type = stock_picking_type.browse(data["rma_out_type_id"])
rma_out_type.write(
{"return_picking_type_id": data.get("rma_in_type_id", False)}
)
if "rma_in_type_id" in data:
rma_in_type = stock_picking_type.browse(data["rma_in_type_id"])
rma_in_type.write(
{"return_picking_type_id": data.get("rma_out_type_id", False)}
)
return data
def _get_routes_values(self):
res = super()._get_routes_values()
rma_routes = {
"rma_in_route_id": {
"routing_key": "rma_in",
"depends": ["active"],
"route_update_values": {
"name": self._format_routename("RMA In"),
"active": self.active,
},
"route_create_values": {
"warehouse_selectable": True,
"company_id": self.company_id.id,
"sequence": 100,
},
"rules_values": {
"active": True,
},
},
"rma_out_route_id": {
"routing_key": "rma_out",
"depends": ["active"],
"route_update_values": {
"name": self._format_routename("RMA Out"),
"active": self.active,
},
"route_create_values": {
"warehouse_selectable": True,
"company_id": self.company_id.id,
"sequence": 110,
},
"rules_values": {
"active": True,
},
},
}
if self.env.context.get("rma_post_init_hook"):
return rma_routes
res.update(rma_routes)
return res
def get_rules_dict(self):
res = super().get_rules_dict()
customer_loc, supplier_loc = self._get_partner_locations()
for warehouse in self:
res[warehouse.id].update(
{
"rma_in": [
self.Routing(
customer_loc,
warehouse.rma_loc_id,
warehouse.rma_in_type_id,
"pull",
)
],
"rma_out": [
self.Routing(
warehouse.rma_loc_id,
customer_loc,
warehouse.rma_out_type_id,
"pull",
)
],
}
)
return res

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,12 @@
* `Tecnativa <https://www.tecnativa.com>`_:
* Ernesto Tejeda
* Pedro M. Baeza
* David Vidal
* Víctor Martínez
* Chafique Delli <chafique.delli@akretion.com>
* Giovanni Serra - Ooops <giovanni@ooops404.com>
* Michael Tietz (MT Software) <mtietz@mt-software.de>
* Jacques-Etienne Baudoux - BCIM <je@bcim.be>
* Souheil Bejaoui - ACSONE SA/NV <souheil.bejaoui@acsone.eu>

View file

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

View file

@ -0,0 +1,6 @@
* As soon as the picking is selected, the user should select the move,
but perhaps stock.move _rec_name could be improved to better show what
the product of that move is.
* Add RMA reception and/or RMA delivery on several steps - 2 or 3 - like
normal receptions/deliveries. It should be a separate option inside the
warehouse definition.

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.

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,553 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="return-merchandise-authorization-management">
<h1>Return Merchandise Authorization Management</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:ee42142efd7393e2b2a8fa941b46827ae484f0116729f557a09de20fb9868658
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/rma/tree/16.0/rma"><img alt="OCA/rma" src="https://img.shields.io/badge/github-OCA%2Frma-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/rma-16-0/rma-16-0-rma"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/rma&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows you to manage <a class="reference external" href="https://en.wikipedia.org/wiki/Return_merchandise_authorization">Return Merchandise Authorization (RMA)</a>.
RMA documents can be created from scratch, from a delivery order or from
an incoming email. Product receptions and returning delivery operations
of the RMA module are fully integrated with the Receipts and Deliveries
Operations of Odoo inventory core module. It also allows you to generate
refunds in the same way as Odoo generates it.
Besides, you have full integration of the RMA documents in the customer portal.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-3">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h2><a class="toc-backref" href="#toc-entry-1">Configuration</a></h2>
<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">
<h2><a class="toc-backref" href="#toc-entry-2">Usage</a></h2>
<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">
<h2><a class="toc-backref" href="#toc-entry-3">Known issues / Roadmap</a></h2>
<ul class="simple">
<li>As soon as the picking is selected, the user should select the move,
but perhaps stock.move _rec_name could be improved to better show what
the product of that move is.</li>
<li>Add RMA reception and/or RMA delivery on several steps - 2 or 3 - like
normal receptions/deliveries. It should be a separate option inside the
warehouse definition.</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-4">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/rma/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/rma/issues/new?body=module:%20rma%0Aversion:%2016.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">
<h2><a class="toc-backref" href="#toc-entry-5">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-6">Authors</a></h3>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-7">Contributors</a></h3>
<ul class="simple">
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>Ernesto Tejeda</li>
<li>Pedro M. Baeza</li>
<li>David Vidal</li>
<li>Víctor Martínez</li>
</ul>
</li>
<li>Chafique Delli &lt;<a class="reference external" href="mailto:chafique.delli&#64;akretion.com">chafique.delli&#64;akretion.com</a>&gt;</li>
<li>Giovanni Serra - Ooops &lt;<a class="reference external" href="mailto:giovanni&#64;ooops404.com">giovanni&#64;ooops404.com</a>&gt;</li>
<li>Michael Tietz (MT Software) &lt;<a class="reference external" href="mailto:mtietz&#64;mt-software.de">mtietz&#64;mt-software.de</a>&gt;</li>
<li>Jacques-Etienne Baudoux - BCIM &lt;<a class="reference external" href="mailto:je&#64;bcim.be">je&#64;bcim.be</a>&gt;</li>
<li>Souheil Bejaoui - ACSONE SA/NV &lt;<a class="reference external" href="mailto:souheil.bejaoui&#64;acsone.eu">souheil.bejaoui&#64;acsone.eu</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h3>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainers</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/pedrobaeza"><img alt="pedrobaeza" src="https://github.com/pedrobaeza.png?size=40px" /></a> <a class="reference external image-reference" href="https://github.com/chienandalu"><img alt="chienandalu" src="https://github.com/chienandalu.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/rma/tree/16.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>
</div>
</body>
</html>

View file

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

View file

@ -0,0 +1,928 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import Command
from odoo.exceptions import UserError, ValidationError
from odoo.tests import Form, new_test_user, tagged, users
from odoo.tools import mute_logger
from odoo.addons.base.tests.common import BaseCommon
from .. import hooks
class TestRma(BaseCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
if not cls.env.company.chart_template_id:
# Load a CoA if there's none in current company
coa = cls.env.ref("l10n_generic_coa.configurable_chart_template", False)
if not coa:
# Load the first available CoA
coa = cls.env["account.chart.template"].search(
[("visible", "=", True)], limit=1
)
coa.try_loading(company=cls.env.company, install_demo=False)
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"}
)
cls.product_2 = cls.product_product.create(
{"name": "Product test 2", "type": "product"}
)
cls.account_receiv = cls.env["account.account"].create(
{
"name": "Receivable",
"code": "RCV00",
"account_type": "asset_receivable",
"reconcile": True,
}
)
cls.partner = cls.res_partner.create(
{
"name": "Partner test",
"property_account_receivable_id": cls.account_receiv.id,
"property_payment_term_id": cls.env.ref(
"account.account_payment_term_30days"
).id,
}
)
cls.partner_invoice = cls.res_partner.create(
{
"name": "Partner invoice test",
"parent_id": cls.partner.id,
"type": "invoice",
}
)
cls.partner_shipping = cls.res_partner.create(
{
"name": "Partner shipping test",
"parent_id": cls.partner.id,
"type": "delivery",
}
)
cls.finalization_reason_1 = cls.env["rma.finalization"].create(
{"name": ("[Test] It can't be repaired and customer doesn't want it")}
)
cls.finalization_reason_2 = cls.env["rma.finalization"].create(
{"name": "[Test] It's out of warranty. To be scrapped"}
)
cls.env.ref("rma.group_rma_manual_finalization").users |= cls.env.user
cls.warehouse = cls.env.ref("stock.warehouse0")
# Ensure grouping
cls.env.company.rma_return_grouping = True
cls.operation = cls.env.ref("rma.rma_operation_replace")
def _create_rma(
self, partner=None, product=None, qty=None, location=None, operation=None
):
vals = {}
if partner:
vals["partner_id"] = partner.id
if product:
vals["product_id"] = product.id
if qty:
vals["product_uom_qty"] = qty
if location:
vals["location_id"] = location.id
if operation:
vals["operation_id"] = operation.id
elif operation is None:
vals["operation_id"] = self.operation.id
return self.env["rma"].create(vals)
def _create_confirm_receive(
self, partner=None, product=None, qty=None, location=None, operation=None
):
rma = self._create_rma(partner, product, qty, location, operation)
rma.action_confirm()
rma.reception_move_id.quantity_done = rma.product_uom_qty
rma.reception_move_id.picking_id._action_done()
return rma
def _receive_and_replace(self, partner, product, qty, location):
rma = self._create_confirm_receive(partner, product, qty, location)
delivery_form = Form(
self.env["rma.delivery.wizard"].with_context(
active_ids=rma.ids,
rma_delivery_type="replace",
)
)
delivery_form.product_id = rma.product_id
delivery_form.product_uom_qty = qty
delivery_wizard = delivery_form.save()
delivery_wizard.action_deliver()
return rma
def _create_delivery(self):
picking_type = self.env["stock.picking.type"].search(
[
("code", "=", "outgoing"),
"|",
("warehouse_id.company_id", "=", self.company.id),
("warehouse_id", "=", False),
],
limit=1,
)
picking_form = Form(
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_ids:
move.quantity_done = move.product_uom_qty
picking.button_validate()
return picking
@tagged("-at_install", "post_install")
class TestRmaCase(TestRma):
def test_post_init_hook(self):
warehouse = self.env["stock.warehouse"].create(
{
"name": "Test warehouse",
"code": "code",
"company_id": self.env.company.id,
}
)
hooks.post_init_hook(self.env.cr, self.registry)
self.assertTrue(warehouse.rma_in_type_id)
self.assertEqual(
warehouse.rma_in_type_id.default_location_dest_id, warehouse.rma_loc_id
)
self.assertEqual(
warehouse.rma_out_type_id.default_location_src_id, warehouse.rma_loc_id
)
self.assertTrue(warehouse.rma_loc_id)
self.assertTrue(warehouse.rma_in_route_id)
self.assertTrue(warehouse.rma_out_route_id)
def test_rma_replace_pick_ship(self):
self.warehouse.write({"delivery_steps": "pick_ship"})
rma = self._create_rma(self.partner, self.product, 1, self.rma_loc)
rma.action_confirm()
rma.reception_move_id.quantity_done = 1
rma.reception_move_id.picking_id._action_done()
self.assertEqual(rma.reception_move_id.picking_id.state, "done")
self.assertEqual(rma.state, "received")
res = rma.action_replace()
wizard_form = Form(self.env[res["res_model"]].with_context(**res["context"]))
wizard_form.product_id = self.product
wizard_form.product_uom_qty = rma.product_uom_qty
wizard = wizard_form.save()
wizard.action_deliver()
self.assertEqual(rma.delivery_picking_count, 2)
out_pickings = rma.mapped("delivery_move_ids.picking_id")
self.assertIn(
self.warehouse.pick_type_id, out_pickings.mapped("picking_type_id")
)
self.assertIn(
self.warehouse.out_type_id, out_pickings.mapped("picking_type_id")
)
def test_computed(self):
# If partner changes, the invoice address is set
rma = self.env["rma"].new()
rma.partner_id = self.partner
self.assertEqual(rma.partner_invoice_id, self.partner_invoice)
# If origin move changes, the product is set
uom_ten = self.env["uom.uom"].create(
{
"name": "Ten",
"category_id": self.env.ref("uom.product_uom_unit").id,
"factor_inv": 10,
"uom_type": "bigger",
}
)
product_2 = self.product_product.create(
{"name": "Product test 2", "type": "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
picking = picking_form.save()
picking._action_done()
rma.picking_id = picking
rma.move_id = picking.move_ids
self.assertEqual(rma.product_id, product_2)
self.assertEqual(rma.product_uom_qty, 15)
self.assertEqual(rma.product_uom, uom_ten)
# If product changes, unit of measure changes
rma.move_id = False
rma.product_id = self.product
self.assertEqual(rma.product_uom, self.product.uom_id)
def test_ensure_required_fields_on_confirm(self):
rma = self._create_rma(operation=False)
with self.assertRaises(ValidationError) as e:
rma.action_confirm()
self.assertEqual(
e.exception.args[0],
"Required field(s):\nCustomer\nShipping Address\nInvoice Address\nProduct"
"\nRequested operation",
)
rma.partner_id = self.partner.id
with self.assertRaises(ValidationError) as e:
rma.action_confirm()
self.assertEqual(
e.exception.args[0], "Required field(s):\nProduct\nRequested operation"
)
rma.product_id = self.product.id
rma.location_id = self.rma_loc.id
with self.assertRaises(ValidationError) as e:
rma.action_confirm()
self.assertEqual(e.exception.args[0], "Required field(s):\nRequested operation")
rma.operation_id = self.operation
rma.action_confirm()
self.assertEqual(rma.state, "confirmed")
def test_confirm_and_receive(self):
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma.action_confirm()
self.assertEqual(rma.reception_move_id.picking_id.state, "assigned")
self.assertEqual(rma.reception_move_id.product_id, rma.product_id)
self.assertEqual(rma.reception_move_id.product_uom_qty, 10)
self.assertEqual(rma.reception_move_id.product_uom, rma.product_uom)
self.assertEqual(rma.state, "confirmed")
rma.reception_move_id.quantity_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")
def test_cancel(self):
# cancel a draft RMA
rma = self._create_rma(self.partner, self.product)
rma.action_cancel()
self.assertEqual(rma.state, "cancelled")
# cancel a confirmed RMA
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma.action_confirm()
rma.action_cancel()
self.assertEqual(rma.state, "cancelled")
# A RMA is only cancelled from draft and confirmed states
rma = self._create_confirm_receive(self.partner, self.product, 10, self.rma_loc)
with self.assertRaises(UserError):
rma.action_cancel()
def test_lock_unlock(self):
# A RMA is only locked from 'received' state
rma_1 = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma_2 = self._create_confirm_receive(
self.partner, self.product, 10, self.rma_loc
)
self.assertEqual(rma_1.state, "draft")
self.assertEqual(rma_2.state, "received")
(rma_1 | rma_2).action_lock()
self.assertEqual(rma_1.state, "draft")
self.assertEqual(rma_2.state, "locked")
# A RMA is only unlocked from 'lock' state and it will be set
# to 'received' state
(rma_1 | rma_2).action_unlock()
self.assertEqual(rma_1.state, "draft")
self.assertEqual(rma_2.state, "received")
@users("__system__", "user_rma")
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)
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.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_ids), 1)
self.assertEqual(rma.delivery_move_ids.product_id, product_2)
self.assertEqual(rma.delivery_move_ids.product_uom_qty, 2)
self.assertTrue(rma.delivery_move_ids.picking_id.state, "waiting")
self.assertEqual(rma.state, "waiting_replacement")
self.assertFalse(rma.can_be_refunded)
self.assertFalse(rma.can_be_returned)
self.assertTrue(rma.can_be_replaced)
self.assertEqual(rma.delivered_qty, 2)
self.assertEqual(rma.remaining_qty, 8)
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)
def test_return_to_customer(self):
# Create, confirm and receive an RMA
rma = self._create_confirm_receive(self.partner, self.product, 10, self.rma_loc)
# Return the same product with quantity 2 to the customer.
delivery_form = Form(
self.env["rma.delivery.wizard"].with_context(
active_ids=rma.ids,
rma_delivery_type="return",
)
)
delivery_form.product_uom_qty = 2
delivery_wizard = delivery_form.save()
delivery_wizard.action_deliver()
picking = rma.delivery_move_ids.picking_id
self.assertEqual(len(picking.move_ids), 1)
self.assertEqual(rma.delivery_move_ids.product_id, self.product)
self.assertEqual(rma.delivery_move_ids.product_uom_qty, 2)
self.assertTrue(picking.state, "waiting")
self.assertEqual(rma.state, "waiting_return")
self.assertFalse(rma.can_be_refunded)
self.assertFalse(rma.can_be_replaced)
self.assertTrue(rma.can_be_returned)
self.assertEqual(rma.delivered_qty, 2)
self.assertEqual(rma.remaining_qty, 8)
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)
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)
all_in_pickings = all_rmas.mapped("reception_move_id.picking_id")
self.assertEqual(
all_in_pickings.mapped("picking_type_id"), self.warehouse.rma_in_type_id
)
self.assertEqual(
all_in_pickings.mapped("location_dest_id"), self.warehouse.rma_loc_id
)
# Mass return of those four RMAs
delivery_wizard = (
self.env["rma.delivery.wizard"]
.with_context(active_ids=all_rmas.ids, rma_delivery_type="return")
.create({})
)
delivery_wizard.action_deliver()
# Two pickings were created
pick_1 = (rma_1 | rma_2 | rma_3).mapped("delivery_move_ids.picking_id")
pick_2 = rma_4.delivery_move_ids.picking_id
self.assertEqual(pick_1.picking_type_id, self.warehouse.rma_out_type_id)
self.assertEqual(pick_1.location_id, self.warehouse.rma_loc_id)
self.assertEqual(pick_2.picking_type_id, self.warehouse.rma_out_type_id)
self.assertEqual(pick_2.location_id, self.warehouse.rma_loc_id)
self.assertEqual(len(pick_1), 1)
self.assertEqual(len(pick_2), 1)
self.assertNotEqual(pick_1, pick_2)
self.assertEqual((pick_1 | pick_2).mapped("state"), ["assigned"] * 2)
# One picking per partner
self.assertNotEqual(pick_1.partner_id, pick_2.partner_id)
self.assertEqual(
pick_1.partner_id,
(rma_1 | rma_2 | rma_3).mapped("partner_shipping_id"),
)
self.assertEqual(pick_2.partner_id, rma_4.partner_id)
# Each RMA of (rma_1, rma_2 and rma_3) is linked to a different
# line of picking_1
self.assertEqual(len(pick_1.move_ids), 3)
self.assertEqual(
pick_1.move_ids.rma_id,
(rma_1 | rma_2 | rma_3),
)
self.assertEqual(
(rma_1 | rma_2 | rma_3).mapped("delivery_move_ids"),
pick_1.move_ids,
)
# rma_4 is linked with the unique move of pick_2
self.assertEqual(len(pick_2.move_ids), 1)
self.assertEqual(pick_2.move_ids.rma_id, rma_4)
self.assertEqual(rma_4.delivery_move_ids, pick_2.move_ids)
# Assert product and quantities are propagated correctly
for rma in all_rmas:
self.assertEqual(rma.product_id, rma.delivery_move_ids.product_id)
self.assertEqual(rma.product_uom_qty, rma.delivery_move_ids.product_uom_qty)
self.assertEqual(rma.product_uom, rma.delivery_move_ids.product_uom)
rma.delivery_move_ids.quantity_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
stock_return_picking_form.rma_operation_id = self.operation
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_ids
self.assertTrue(origin_moves[0].rma_ids)
self.assertTrue(origin_moves[1].rma_ids)
rmas = origin_moves.rma_ids
self.assertEqual(rmas.mapped("state"), ["confirmed"] * 2)
# Each reception move is linked one of the generated RMAs
reception = self.env["stock.picking"].browse(picking_action["res_id"])
reception_moves = reception.move_ids
self.assertTrue(reception_moves[0].rma_receiver_ids)
self.assertTrue(reception_moves[1].rma_receiver_ids)
self.assertEqual(reception_moves.rma_receiver_ids, rmas)
# Validate the reception picking to set rmas to 'received' state
reception_moves[0].quantity_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_ids.filtered(
lambda r: r.product_id == self.product
)
rma_form.operation_id = self.operation
rma = rma_form.save()
rma.action_confirm()
rma.reception_move_id.quantity_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)
@mute_logger("odoo.models.unlink")
def test_rma_to_receive_on_delete_invoice(self):
rma = self._create_confirm_receive(self.partner, self.product, 10, self.rma_loc)
rma.action_refund()
self.assertEqual(rma.state, "refunded")
rma.refund_id.unlink()
self.assertFalse(rma.refund_id)
self.assertEqual(rma.state, "received")
self.assertTrue(rma.can_be_refunded)
self.assertTrue(rma.can_be_returned)
self.assertTrue(rma.can_be_replaced)
def test_rma_picking_type_default_values(self):
warehouse = self.env["stock.warehouse"].create(
{"name": "Stock - RMA Test", "code": "SRT"}
)
self.assertFalse(warehouse.rma_in_type_id.use_create_lots)
self.assertTrue(warehouse.rma_in_type_id.use_existing_lots)
def test_quantities_on_hand(self):
rma = self._create_confirm_receive(self.partner, self.product, 10, self.rma_loc)
self.assertEqual(rma.product_id.qty_available, 0)
def test_autoconfirm_email(self):
self.company.send_rma_confirmation = True
self.company.send_rma_receipt_confirmation = True
self.company.send_rma_draft_confirmation = True
self.company.rma_mail_confirmation_template_id = self.env.ref(
"rma.mail_template_rma_notification"
)
self.company.rma_mail_receipt_confirmation_template_id = self.env.ref(
"rma.mail_template_rma_receipt_notification"
)
self.company.rma_mail_draft_confirmation_template_id = self.env.ref(
"rma.mail_template_rma_draft_notification"
)
previous_mails = self.env["mail.mail"].search(
[("partner_ids", "in", self.partner.ids)]
)
self.assertFalse(previous_mails)
# Force the context to mock an RMA created from the portal, which is
# feature that we get on `rma_sale`. We drop it after the RMA creation
# to avoid uncontrolled side effects
ctx = self.env.context
self.env.context = dict(ctx, from_portal=True)
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
self.env.context = ctx
mail_draft = self.env["mail.message"].search(
[("partner_ids", "in", self.partner.ids)]
)
rma.action_confirm()
mail_confirm = (
self.env["mail.message"].search([("partner_ids", "in", self.partner.ids)])
- mail_draft
)
self.assertTrue(rma.name in mail_confirm.subject)
self.assertTrue(rma.name in mail_confirm.body)
self.assertEqual(
self.env.ref("rma.mt_rma_notification"), mail_confirm.subtype_id
)
# Now we'll confirm the incoming goods picking and the automatic
# reception notification should be sent
rma.reception_move_id.quantity_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)
def test_replace_picking_type(self):
"""
Test that by default, the replace operation uses the default delivery route,
meaning the warehouse's default delivery picking type is applied.
RMA replacement orders are not separated from regular deliveries, and both use
the same picking type.
"""
rma = self._receive_and_replace(self.partner, self.product, 2, self.rma_loc)
rma_in_type = self.warehouse.rma_in_type_id
out_type = self.warehouse.out_type_id
self.assertEqual(rma.reception_move_id.picking_type_id, rma_in_type)
self.assertEqual(rma.delivery_move_ids.picking_type_id, out_type)
def test_replace_picking_type_custom_picking_type(self):
"""
Test that when configured to use a custom route, the replace operation uses a
custom picking type, separating RMA replacement orders from regular deliveries.
The custom picking type is applied specifically for RMA replacements, instead
of the default delivery picking type.
"""
rma_in_type = self.warehouse.rma_in_type_id
rma_out_type = self.warehouse.rma_out_type_id
route = self.env["stock.route"].create(
{
"name": "RMA OUT replace",
"active": True,
"sequence": 100,
"product_selectable": True,
"rule_ids": [
Command.create(
{
"name": "RMA OUT",
"action": "pull",
"picking_type_id": rma_out_type.id,
"location_src_id": self.warehouse.lot_stock_id.id,
"location_dest_id": self.env.ref(
"stock.stock_location_customers"
).id,
},
)
],
}
)
self.warehouse.rma_out_replace_route_id = route
rma = self._receive_and_replace(self.partner, self.product, 2, self.rma_loc)
self.assertEqual(rma.reception_move_id.picking_type_id, rma_in_type)
self.assertEqual(rma.delivery_move_ids.picking_type_id, rma_out_type)
def test_grouping_reception_enabled(self):
rma_1 = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma_2 = self._create_rma(self.partner, self.product_2, 10, self.rma_loc)
(rma_1 | rma_2).action_confirm()
self.assertEqual(
rma_1.reception_move_id.picking_id, rma_2.reception_move_id.picking_id
)

View file

@ -0,0 +1,73 @@
# Copyright 2024 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from .test_rma import TestRma
PROCESSED_STATES = ["received", "refunded", "replaced", "finished"]
AWAITING_ACTION_STATES = ["waiting_return", "waiting_replacement", "confirmed"]
class TestRmaDashboard(TestRma):
def test_0(self):
operation_replace = self.env.ref("rma.rma_operation_replace")
operation_return = self.env.ref("rma.rma_operation_return")
operation_refund = self.env.ref("rma.rma_operation_refund")
replace_draft_1 = self._create_rma(
self.partner, self.product, 1, self.rma_loc, operation=operation_replace
)
self._create_rma(
self.partner, self.product, 1, self.rma_loc, operation=operation_replace
) # replace_draft_2
replace_draft_1.copy({"state": "confirmed"}) # replace_confirmed
replace_draft_1.copy({"state": "received"}) # replace_received
replace_draft_1.copy({"state": "waiting_return"}) # replace_waiting_return
replace_draft_1.copy( # replace_waiting_replacement
{"state": "waiting_replacement"}
)
return_draft = self._create_rma(
self.partner, self.product, 1, self.rma_loc, operation=operation_return
)
return_draft.copy({"state": "confirmed"}) # return_confirmed
return_draft.copy({"state": "waiting_return"}) # return_waiting_return
return_draft.copy({"state": "returned"}) # return_returned
return_draft.copy({"state": "finished"}) # return_finished
refund_draft = self._create_rma(
self.partner, self.product, 1, self.rma_loc, operation=operation_refund
)
refund_draft.copy({"state": "finished"}) # refund_refunded
self.assertEqual(operation_replace.count_rma_draft, 2)
self.assertEqual(operation_replace.count_rma_awaiting_action, 3)
self.assertEqual(operation_replace.count_rma_processed, 1)
self.assertEqual(operation_return.count_rma_draft, 1)
self.assertEqual(operation_return.count_rma_awaiting_action, 2)
self.assertEqual(operation_return.count_rma_processed, 1)
self.assertEqual(operation_refund.count_rma_draft, 1)
self.assertEqual(operation_refund.count_rma_awaiting_action, 0)
self.assertEqual(operation_refund.count_rma_processed, 1)
action = operation_replace.get_action_rma_tree_draft()
self.assertListEqual(
["&", ("operation_id", "=", operation_replace.id), ("state", "=", "draft")],
action.get("domain"),
)
action = operation_replace.get_action_rma_tree_awaiting_action()
self.assertListEqual(
[
"&",
("operation_id", "=", operation_replace.id),
("state", "in", AWAITING_ACTION_STATES),
],
action.get("domain"),
)
action = operation_replace.get_action_rma_tree_processed()
self.assertListEqual(
[
"&",
("operation_id", "=", operation_replace.id),
("state", "in", PROCESSED_STATES),
],
action.get("domain"),
)

View file

@ -0,0 +1,335 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.exceptions import ValidationError
from odoo.tests.common import Form
from .test_rma import TestRma
class TestRmaOperation(TestRma):
def test_01(self):
"""
ensure that the receipt creation behaves correctly according to the
action_create_receipt setting.
- "automatic_on_confirm":
- receipts are created automatically
- the manual button is hidden
- "manual_on_confirm"
- manual button is visible after confirmation
- disappears once a receipt is manually created
"""
self.assertEqual(self.operation.action_create_receipt, "automatic_on_confirm")
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
self.assertFalse(rma.show_create_receipt)
rma.action_confirm()
self.assertTrue(rma.reception_move_id)
self.assertFalse(rma.show_create_receipt)
self.operation.action_create_receipt = "manual_on_confirm"
rma2 = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma2.action_confirm()
self.assertTrue(rma2.show_create_receipt)
self.assertFalse(rma2.reception_move_id)
rma2.action_create_receipt()
self.assertFalse(rma2.show_create_receipt)
def test_02(self):
"""
test delivery button visibility based on operation settings.
No deliver possible
"""
self.operation.action_create_delivery = False
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
self.assertFalse(rma.can_be_returned)
self.assertFalse(rma.can_be_replaced)
rma.action_confirm()
self.assertEqual(rma.state, "confirmed")
self.assertFalse(rma.can_be_returned)
self.assertFalse(rma.show_create_return)
self.assertFalse(rma.can_be_replaced)
self.assertFalse(rma.show_create_replace)
def test_03(self):
"""
test delivery button visibility based on operation settings.
deliver manually after confirm
"""
self.operation.action_create_delivery = "manual_on_confirm"
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
self.assertFalse(rma.can_be_returned)
self.assertFalse(rma.can_be_replaced)
rma.action_confirm()
self.assertEqual(rma.state, "confirmed")
self.assertTrue(rma.can_be_returned)
self.assertTrue(rma.show_create_return)
self.assertTrue(rma.can_be_replaced)
self.assertTrue(rma.show_create_replace)
def test_04(self):
"""
test delivery button visibility based on operation settings.
deliver automatically after confirm, return same product
"""
self.operation.action_create_delivery = "automatic_on_confirm"
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
self.assertFalse(rma.can_be_returned)
self.assertFalse(rma.can_be_replaced)
rma.action_confirm()
self.assertEqual(rma.state, "waiting_replacement")
self.assertFalse(rma.can_be_returned)
self.assertFalse(rma.show_create_return)
self.assertFalse(rma.can_be_replaced)
self.assertFalse(rma.show_create_replace)
self.assertTrue(rma.delivery_move_ids)
self.assertEqual(rma.delivery_move_ids.product_id, self.product)
self.assertEqual(rma.delivery_move_ids.product_uom_qty, 10)
def test_05(self):
"""
test delivery button visibility based on operation settings.
deliver manually after receipt
"""
self.operation.action_create_delivery = "manual_after_receipt"
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
self.assertFalse(rma.can_be_returned)
self.assertFalse(rma.can_be_replaced)
rma.action_confirm()
self.assertEqual(rma.state, "confirmed")
self.assertFalse(rma.can_be_returned)
self.assertFalse(rma.show_create_return)
self.assertFalse(rma.can_be_replaced)
self.assertFalse(rma.show_create_replace)
rma.reception_move_id.quantity_done = rma.product_uom_qty
rma.reception_move_id.picking_id._action_done()
self.assertEqual(rma.state, "received")
self.assertTrue(rma.can_be_returned)
self.assertTrue(rma.show_create_return)
self.assertTrue(rma.can_be_replaced)
self.assertTrue(rma.show_create_replace)
def test_06(self):
"""
test delivery button visibility based on operation settings.
deliver automatically after receipt
"""
self.operation.action_create_delivery = "automatic_after_receipt"
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
self.assertFalse(rma.can_be_returned)
self.assertFalse(rma.can_be_replaced)
rma.action_confirm()
self.assertEqual(rma.state, "confirmed")
self.assertFalse(rma.can_be_returned)
self.assertFalse(rma.show_create_return)
self.assertFalse(rma.can_be_replaced)
self.assertFalse(rma.show_create_replace)
self.assertFalse(rma.delivery_move_ids)
rma.reception_move_id.quantity_done = rma.product_uom_qty
rma.reception_move_id.picking_id._action_done()
self.assertEqual(rma.delivery_move_ids.product_id, self.product)
self.assertEqual(rma.delivery_move_ids.product_uom_qty, 10)
self.assertEqual(rma.state, "waiting_replacement")
self.assertFalse(rma.can_be_returned)
self.assertFalse(rma.show_create_return)
self.assertTrue(rma.can_be_replaced)
self.assertFalse(rma.show_create_replace)
def test_07(self):
"""
test delivery button visibility based on operation settings.
deliver automatically after confirm, different product
"""
self.operation.action_create_delivery = "automatic_on_confirm"
self.operation.different_return_product = True
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
with self.assertRaises(AssertionError, msg="Replacement fields are required"):
with Form(rma) as rma_form:
rma_form.save()
with self.assertRaises(
ValidationError, msg="Complete the replacement information"
):
rma.action_confirm()
rma.return_product_id = self.product_product.create(
{"name": "return Product test 1", "type": "product"}
)
rma.action_confirm()
self.assertEqual(rma.delivery_move_ids.product_id, rma.product_id)
self.assertEqual(rma.reception_move_id.product_id, rma.return_product_id)
self.assertEqual(rma.state, "waiting_replacement")
def test_08(self):
"""test refund, manually after confirm"""
self.operation.action_create_refund = "manual_on_confirm"
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma.action_confirm()
self.assertEqual(rma.state, "confirmed")
self.assertTrue(rma.can_be_refunded)
self.assertTrue(rma.show_create_refund)
def test_09(self):
"""test refund, manually after receipt"""
self.operation.action_create_refund = "manual_after_receipt"
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma.action_confirm()
self.assertEqual(rma.state, "confirmed")
self.assertFalse(rma.can_be_refunded)
self.assertFalse(rma.show_create_refund)
rma.reception_move_id.quantity_done = rma.product_uom_qty
rma.reception_move_id.picking_id._action_done()
self.assertEqual(rma.state, "received")
self.assertTrue(rma.can_be_refunded)
self.assertTrue(rma.show_create_refund)
def test_10(self):
"""test refund, automatic after confirm"""
self.operation.action_create_refund = "automatic_on_confirm"
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma.action_confirm()
self.assertEqual(rma.state, "refunded")
self.assertTrue(rma.refund_id)
self.assertFalse(rma.can_be_refunded)
self.assertFalse(rma.show_create_refund)
def test_11(self):
"""test refund, automatic after confirm"""
self.operation.action_create_refund = "automatic_after_receipt"
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma.action_confirm()
self.assertEqual(rma.state, "confirmed")
rma.reception_move_id.quantity_done = rma.product_uom_qty
rma.reception_move_id.picking_id._action_done()
self.assertEqual(rma.state, "refunded")
self.assertTrue(rma.refund_id)
self.assertFalse(rma.can_be_refunded)
self.assertFalse(rma.show_create_refund)
def test_12(self):
"""
Refund without product return
Some companies may offer refunds without requiring the return of the product,
often in cases of low-value items or when the cost of return shipping is
prohibitive.
- no receipt
- no return
- automatically refund on confirm
"""
self.operation.action_create_receipt = False
self.operation.action_create_refund = "automatic_on_confirm"
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma.action_confirm()
self.assertEqual(rma.state, "refunded")
self.assertFalse(rma.reception_move_id)
self.assertTrue(rma.refund_id)
def test_13(self):
"""
Return of non-ordered product
Occasionally, customers receive items they did not order and need a process for
returning these products. The delivered product don't figure on the sale order
- receipt
- no return
- no refund
"""
self.operation.action_create_receipt = "automatic_on_confirm"
self.operation.action_create_delivery = False
self.operation.action_create_refund = False
rma = self._create_rma(self.partner, self.product, 10, self.rma_loc)
rma.action_confirm()
rma.reception_move_id.quantity_done = rma.product_uom_qty
rma.reception_move_id.picking_id._action_done()
self.assertEqual(rma.state, "received")
self.assertFalse(rma.delivery_move_ids)
def test_14(self):
"""if the refund action is not ment to update quantity, return picking line
to_refund field should be False"""
self.operation.action_create_refund = "manual_after_receipt"
origin_delivery = self._create_delivery()
stock_return_picking_form = Form(
self.env["stock.return.picking"].with_context(
active_ids=origin_delivery.ids,
active_id=origin_delivery.id,
active_model="stock.picking",
)
)
stock_return_picking_form.create_rma = True
stock_return_picking_form.rma_operation_id = self.operation
return_wizard = stock_return_picking_form.save()
return_line = return_wizard.product_return_moves.filtered(
lambda m, p=self.product: m.product_id == p
)
self.assertEqual(return_line.rma_operation_id, self.operation)
picking_action = return_wizard.create_returns()
reception = self.env["stock.picking"].browse(picking_action["res_id"])
move = reception.move_ids.filtered(lambda m, p=self.product: m.product_id == p)
self.assertFalse(move.to_refund)
def test_15(self):
"""if the refund action is ment to update quantity, return picking line
to_refund field should be True"""
self.operation.action_create_refund = "update_quantity"
origin_delivery = self._create_delivery()
stock_return_picking_form = Form(
self.env["stock.return.picking"].with_context(
active_ids=origin_delivery.ids,
active_id=origin_delivery.id,
active_model="stock.picking",
)
)
stock_return_picking_form.create_rma = True
stock_return_picking_form.rma_operation_id = self.operation
return_wizard = stock_return_picking_form.save()
return_line = return_wizard.product_return_moves.filtered(
lambda m, p=self.product: m.product_id == p
)
self.assertEqual(return_line.rma_operation_id, self.operation)
picking_action = return_wizard.create_returns()
reception = self.env["stock.picking"].browse(picking_action["res_id"])
move = reception.move_ids.filtered(lambda m, p=self.product: m.product_id == p)
self.assertTrue(move.to_refund)
def test_rma_replace_pick_ship(self):
self.operation.action_create_delivery = "automatic_on_confirm"
self.warehouse.write({"delivery_steps": "pick_ship"})
rma = self._create_rma(self.partner, self.product, 1, self.rma_loc)
rma.action_confirm()
self.assertEqual(rma.state, "waiting_replacement")
out_pickings = rma.mapped("delivery_move_ids.picking_id")
self.assertEqual(rma.delivery_picking_count, 2)
self.assertIn(
self.warehouse.pick_type_id, out_pickings.mapped("picking_type_id")
)
self.assertIn(
self.warehouse.out_type_id, out_pickings.mapped("picking_type_id")
)
def test_16(self):
rma = self._create_rma(self.partner, self.product, 1, self.rma_loc)
rma.action_confirm()
self.assertEqual(rma.reception_move_id.state, "assigned")
self.assertEqual(rma.reception_move_id.picking_id.state, "assigned")
def test_17(self):
self.operation.auto_confirm_reception = True
rma = self._create_rma(self.partner, self.product, 1, self.rma_loc)
rma.action_confirm()
self.assertEqual(rma.reception_move_id.state, "done")
self.assertEqual(rma.reception_move_id.picking_id.state, "done")
def test_manual_finish_if_no_required_action_flag(self):
self.operation.action_create_receipt = False
self.operation.action_create_delivery = False
self.operation.action_create_refund = False
rma = self._create_rma(self.partner, self.product, 1, self.rma_loc)
rma.action_confirm()
self.assertFalse(rma.requires_action)
self.assertEqual(rma.state, "confirmed")
rma.action_finish()
self.assertEqual(rma.state, "finished")
self.operation.action_create_receipt = "manual_on_confirm"
rma2 = self._create_rma(self.partner, self.product, 1, self.rma_loc)
rma2.action_confirm()
self.assertTrue(rma2.requires_action)
self.assertEqual(rma2.state, "confirmed")
with self.assertRaises(ValidationError):
rma2.action_finish()

View file

@ -0,0 +1,188 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2024 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="rma_operation_kanban" model="ir.ui.view">
<field name="model">rma.operation</field>
<field name="arch" type="xml">
<kanban
class="oe_background_grey o_kanban_dashboard o_emphasize_colors"
create="0"
group_create="false"
>
<field name="color" />
<field name="count_rma_draft" />
<field name="count_rma_awaiting_action" />
<field name="count_rma_processed" />
<templates>
<t t-name="kanban-box">
<div
t-attf-class="#{!selection_mode ? kanban_color(record.color.raw_value) : ''}"
name="rma_operation"
>
<div t-attf-class="o_kanban_card_header">
<div class="o_kanban_card_header_title">
<div class="o_primary" t-if="!selection_mode">
<a type="object" name="get_action_all_rma">
<field name="name" />
</a>
</div>
<div class="o_primary" t-if="selection_mode">
<field name="name" />
</div>
</div>
<div
class="o_kanban_manage_button_section"
t-if="!selection_mode"
>
<a class="o_kanban_manage_toggle_button" href="#"><i
class="fa fa-ellipsis-v"
role="img"
aria-label="Manage"
title="Manage"
/></a>
</div>
</div>
<div
class="container o_kanban_card_content"
t-if="!selection_mode"
>
<div class="row">
<div class="col-9 o_kanban_primary_left">
<button
class="btn btn-link"
name="get_action_rma_tree_draft"
type="object"
>
<span>Draft</span>
</button>
</div>
<div class="col-3 o_kanban_primary_right">
<button
class="btn btn-link"
name="get_action_rma_tree_draft"
type="object"
>
<span
t-esc="record.count_rma_draft.value"
/>
</button>
</div>
<div class="col-9 o_kanban_primary_left">
<button
class="btn btn-link"
name="get_action_rma_tree_awaiting_action"
type="object"
>
<span>Awaiting action</span>
</button>
</div>
<div class="col-3 o_kanban_primary_right">
<button
class="btn btn-link"
name="get_action_rma_tree_awaiting_action"
type="object"
>
<span
t-esc="record.count_rma_awaiting_action.value"
/>
</button>
</div>
<div class="col-9 o_kanban_primary_left">
<button
class="btn btn-link"
name="get_action_rma_tree_processed"
type="object"
>
<span>Processed</span>
</button>
</div>
<div class="col-3 o_kanban_primary_right">
<button
class="btn btn-link"
name="get_action_rma_tree_processed"
type="object"
>
<span
t-esc="record.count_rma_processed.value"
/>
</button>
</div>
</div>
</div>
<div
class="container o_kanban_card_manage_pane dropdown-menu"
role="menu"
>
<div class="row">
<div
class="col-6 o_kanban_card_manage_section o_kanban_manage_new"
>
<div
role="menuitem"
class="o_kanban_card_manage_title"
>
<span>New</span>
</div>
<div role="menuitem">
<a
name="%(action_rma_form)d"
type="action"
>RMA</a>
</div>
</div>
</div>
<div
t-if="widget.editable"
class="o_kanban_card_manage_settings row"
>
<div
class="col-8"
role="menuitem"
aria-haspopup="true"
>
<ul
class="oe_kanban_colorpicker"
data-field="color"
role="menu"
/>
</div>
<div role="menuitem" class="col-4">
<a
class="dropdown-item"
role="menuitem"
type="edit"
>Configuration</a>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="rma_dashboard_action" model="ir.actions.act_window">
<field name="name">RMA Overview</field>
<field name="res_model">rma.operation</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">kanban,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> Create a new RMA operation </p>
<p>The RMA operation system allows you to configure each return operation
with specific settings that will adjust its behavior.</p>
</field>
</record>
<menuitem
action="rma_dashboard_action"
id="rma_dashboard_menu"
parent="rma_menu"
sequence="0"
name="Overview"
/>
</odoo>

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>

View file

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

View file

@ -0,0 +1,188 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div
class="app_settings_block"
data-string="RMA"
string="RMA"
data-key="rma"
groups="rma.rma_group_manager"
>
<h2>Return Merchandise Authorization Management</h2>
<div
class="row mt16 o_settings_container"
name="operations_setting_container"
>
<div class="col-12 col-lg-6 o_setting_box" title="Finish RMAs manually">
<div class="o_setting_left_pane">
<field name="group_rma_manual_finalization" />
</div>
<div class="o_setting_right_pane">
<label
for="group_rma_manual_finalization"
string="RMA Manual Finalization"
/>
<div class="text-muted">
When the RMA is receive, allow to finsish it manually choosing
a finalization reason.
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="rma_return_grouping" />
</div>
<div class="o_setting_right_pane">
<label for="rma_return_grouping" />
<span
class="fa fa-lg fa-building-o"
title="Values set here are company-specific."
groups="base.group_multi_company"
/>
<div class="text-muted">
Group RMA returns by customer and warehouse.
</div>
</div>
</div>
<div
class="col-12 col-lg-6 o_setting_box"
title="Send automatic RMA info to customer"
>
<div class="o_setting_left_pane">
<field name="send_rma_confirmation" />
</div>
<div class="o_setting_right_pane">
<label
for="send_rma_confirmation"
string="RMA Confirmation Email"
/>
<span
class="fa fa-lg fa-building-o"
title="Values set here are company-specific."
groups="base.group_multi_company"
/>
<div class="text-muted">
When the RMA is confirmed, send an automatic information email.
</div>
<div
class="row mt16"
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>
</div>
</div>
</xpath>
</field>
</record>
<record id="action_rma_config_settings" model="ir.actions.act_window">
<field name="name">Settings</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.config.settings</field>
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context">{'module' : 'rma', 'bin_size': False}</field>
</record>
<menuitem
id="menu_rma_general_settings"
name="Settings"
parent="rma_configuration_menu"
sequence="0"
action="action_rma_config_settings"
groups="base.group_system"
/>
</odoo>

View file

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

View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="rma_finalization_view_search" model="ir.ui.view">
<field name="model">rma.finalization</field>
<field name="arch" type="xml">
<search string="RMA Finalization Reasons">
<field name="name" />
<filter
string="Archived"
name="inactive"
domain="[('active','=',False)]"
/>
<filter
string="Active"
name="active"
domain="[('active','!=',False)]"
/>
</search>
</field>
</record>
<record id="view_rma_finalization_form" model="ir.ui.view">
<field name="name">Rma Finalization Reasons</field>
<field name="model">rma.finalization</field>
<field name="arch" type="xml">
<form string="RMA Finalization">
<sheet>
<widget
name="web_ribbon"
title="Archived"
bg_color="bg-danger"
attrs="{'invisible': [('active', '=', True)]}"
/>
<group>
<field name="name" />
<field name="company_id" groups="base.group_multi_company" />
<field name="company_id" invisible="1" />
<field name="active" invisible="1" />
</group>
</sheet>
</form>
</field>
</record>
<record id="view_rma_finalization_list" model="ir.ui.view">
<field name="name">RMA Finalization Reasons</field>
<field name="model">rma.finalization</field>
<field eval="6" name="priority" />
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="company_id" groups="base.group_multi_company" />
<field name="company_id" invisible="1" />
</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,85 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2024 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="rma_operation_form_view">
<field name="model">rma.operation</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name" />
<field name="active" widget="boolean_toggle" />
</group>
<group string="Settings" name="settings">
<group>
<field name="action_create_receipt" />
</group>
<group>
<field
name="different_return_product"
attrs="{'invisible': [('action_create_receipt', '=', False)]}"
/>
<field
name="auto_confirm_reception"
attrs="{'invisible': [('action_create_receipt', '=', False)]}"
/>
</group>
<group>
<field name="action_create_delivery" />
</group>
<group />
<group>
<field name="action_create_refund" />
</group>
<group />
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="rma_operation_search_view">
<field name="model">rma.operation</field>
<field name="arch" type="xml">
<search>
<field name="name" />
</search>
</field>
</record>
<record model="ir.ui.view" id="rma_operation_tree_view">
<field name="model">rma.operation</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="action_create_receipt" />
<field name="action_create_delivery" />
<field name="action_create_refund" />
<field name="active" widget="boolean_toggle" />
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="rma_operation_act_window">
<field name="name">Operations</field>
<field name="res_model">rma.operation</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<record model="ir.ui.menu" id="rma_operation_menu">
<field name="name">Operations</field>
<field name="parent_id" ref="rma_configuration_menu" />
<field name="action" ref="rma_operation_act_window" />
<field name="sequence" eval="16" />
</record>
</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="placeholder_count" t-value="'rma_count'" />
</t>
</xpath>
</template>
<template id="portal_my_rmas" name="My RMA Orders">
<t t-call="portal.portal_layout">
<t t-set="breadcrumbs_searchbar" t-value="True" />
<t t-call="portal.portal_searchbar">
<t t-set="title">RMA Orders</t>
</t>
<t t-if="rmas" t-call="portal.portal_table">
<thead>
<tr class="active">
<th>RMA #</th>
<th class='d-none d-md-table-cell'>Date</th>
<th name="th_product">Product</th>
<th class='text-right'>Quantity</th>
<th class='d-none d-md-table-cell'>Status</th>
</tr>
</thead>
<tbody>
<t t-foreach="rmas" t-as="rma">
<tr>
<td>
<a
t-att-href="rma.get_portal_url()"
t-att-title="rma.name"
>
<t t-esc="rma.name" />
</a>
</td>
<td class="d-none d-md-table-cell">
<span t-field="rma.date" />
</td>
<!-- Portal users don't have access to unpublished products -->
<td name="td_product">
<span t-esc="rma.sudo().product_id.display_name" />
</td>
<td class='text-right'>
<span t-field="rma.product_uom_qty" />
</td>
<td class="d-none d-md-table-cell tx_status">
<span class="badge 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,145 @@
<?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" />
<field name="company_id" invisible="1" />
</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"
/>
<field name="company_id" invisible="1" />
</group>
</group>
<notebook>
<page name="members" string="Team Members">
<field
name="member_ids"
widget="many2many"
options="{'not_delete': True}"
>
<kanban
quick_create="false"
create="true"
delete="true"
>
<field name="id" />
<field name="name" />
<templates>
<t t-name="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', 'avatar_128', 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>

View file

@ -0,0 +1,468 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
Copyright 2023 Tecnativa - Pedro M. Baeza
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="rma_view_search" model="ir.ui.view">
<field name="name">rma.view.search</field>
<field name="model">rma</field>
<field name="arch" type="xml">
<search>
<field name="name" />
<field name="origin" />
<field name="user_id" />
<field name="product_id" />
<field name="tag_ids" />
<filter
string="Awaiting Action"
name="waiting_action"
domain="[('state', 'in', ['waiting_return', 'waiting_replacement', 'confirmed'])]"
/>
<filter
string="Processed"
name="processed"
domain="[('state', 'in', ['received', 'refunded', 'replaced', 'finished'])]"
/>
<filter
string="Closed"
name="closed"
domain="[('state', 'in', ['locked', 'cancelled'])]"
/>
<separator />
<filter
name="draft_filter"
string="Draft"
domain="[('state','=', 'draft')]"
/>
<filter
name="confirmed_filter"
string="Confirmed"
domain="[('state','=', 'confirmed')]"
/>
<filter
name="received_filter"
string="Received"
domain="[('state','=', 'received')]"
/>
<separator />
<filter
string="Unresolved RMAs"
name="undone_rma"
domain="[('state', 'not in', ['refunded', 'returned', 'replaced', 'locked', 'cancelled', 'finished'])]"
help="RMAs yet to be fully processed"
/>
<filter
string="Late RMAs"
name="late_rma"
domain="[('deadline', '&lt;', context_today().strftime('%Y-%m-%d')), ('state', 'not in', ['refunded', 'returned', 'replaced', 'locked', 'cancelled', 'finished'])]"
help="RMAs which deadline has passed"
/>
<separator />
<filter string="RMA Date" name="filter_rma_date" date="date" />
<filter
string="RMA Deadline"
name="filter_rma_deadline"
date="deadline"
/>
<filter
name="no_user_id_filter"
string="Unassigned RMAs"
domain="[('user_id','=', False)]"
/>
<group string="Group By" name="group_by">
<filter
string="Partner"
name="partner_id_group_by"
context="{'group_by':'partner_id'}"
/>
<filter
string="Responsible"
name="user_id_group_by"
context="{'group_by':'user_id'}"
/>
<filter
string="State"
name="state_group_by"
context="{'group_by':'state'}"
/>
<filter
string="Date"
name="date_group_by"
context="{'group_by':'date'}"
/>
<filter
string="Deadline"
name="deadline_group_by"
context="{'group_by':'deadline'}"
/>
</group>
</search>
</field>
</record>
<record id="rma_view_tree" model="ir.ui.view">
<field name="name">rma.view.tree</field>
<field name="model">rma</field>
<field name="arch" type="xml">
<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
name="action_create_receipt"
type="object"
string="Create Receipt"
attrs="{'invisible': [('show_create_receipt', '=', False)]}"
class="btn-primary"
/>
<button
type="object"
string="To Refund"
name="action_refund"
attrs="{'invisible': [('show_create_refund', '=', False)]}"
class="btn-primary"
/>
<button
type="object"
string="Replace"
name="action_replace"
attrs="{'invisible': [('show_create_replace', '=', False)]}"
class="btn-primary"
/>
<button
type="object"
string="Return to customer"
name="action_return"
attrs="{'invisible': [('show_create_return', '=', 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}"
attrs="{'readonly': [('state', '!=', 'draft')]}"
options="{'always_reload': True}"
/>
<field
name="partner_shipping_id"
attrs="{'readonly': [('state', '!=', 'draft')]}"
force_save="1"
/>
<field
name="partner_invoice_id"
attrs="{'readonly': [('state', 'not in', ['draft', 'confirmed', 'received'])]}"
force_save="1"
/>
<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="return_product_id"
force_save="1"
attrs="{'readonly': ['|', ('picking_id', '!=', False), ('state', '!=', 'draft')], 'invisible': [('different_return_product', '=', False)], 'required': [('different_return_product', '=', True)]}"
/>
<field name="uom_category_id" invisible="1" />
<label for="product_uom_qty" />
<div class="o_row">
<field
name="product_uom_qty"
attrs="{'readonly': [('state', '!=', 'draft')]}"
force_save="1"
/>
<field
name="product_uom"
groups="uom.group_uom"
domain="[('category_id', '=', uom_category_id)]"
attrs="{'readonly': [('state', '!=', 'draft')]}"
force_save="1"
/>
<field name="product_uom" invisible="1" />
</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"
/>
<field name="company_id" invisible="1" />
</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"
attrs="{'readonly': [('state', '!=', 'draft')]}"
/>
<field name="location_id" invisible="1" />
</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="show_create_receipt" invisible="1" />
<field name="show_create_refund" invisible="1" />
<field name="show_create_return" invisible="1" />
<field name="show_create_replace" invisible="1" />
<field name="different_return_product" 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="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)]}"
groups="rma.group_rma_manual_finalization"
/>
</xpath>
</field>
</record>
<record id="rma_view_pivot" model="ir.ui.view">
<field name="name">rma.pivot</field>
<field name="model">rma</field>
<field name="arch" type="xml">
<pivot>
<field name="date" type="row" />
<field name="product_uom_qty" type="measure" />
<field name="delivered_qty" type="measure" />
</pivot>
</field>
</record>
<record id="rma_view_calendar" model="ir.ui.view">
<field name="name">rma.calendar</field>
<field name="model">rma</field>
<field name="arch" type="xml">
<calendar date_start="date" mode="month" color="state" 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>
<record id="action_rma_form" model="ir.actions.act_window">
<field name="name">New RMA</field>
<field name="res_model">rma</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="context">{
'search_operation_id': [active_id],
'default_operation_id': active_id,
}
</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_picking_form" model="ir.ui.view">
<field name="name">stock.picking.form</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form" />
<field name="arch" type="xml">
<div name="button_box">
<button
name="action_view_rma"
type="object"
class="oe_stat_button"
icon="fa-reply"
attrs="{'invisible': [('rma_count', '=', 0)]}"
groups="rma.rma_group_user_own"
>
<field name="rma_count" widget="statinfo" string="RMA" />
</button>
</div>
</field>
</record>
</odoo>

View file

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

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,75 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
Copyright 2023 Tecnativa - Pedro M. Baeza
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="rma_redelivery_wizard_view_form" model="ir.ui.view">
<field name="name">rma.delivery.wizard.form</field>
<field name="model">rma.delivery.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<group>
<field name="scheduled_date" />
<field
name="warehouse_id"
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)]"
/>
<field name="product_uom" invisible="1" />
</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>

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,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
Copyright 2023 Tecnativa - Pedro M. Baeza
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="rma_split_wizard_view_form2" model="ir.ui.view">
<field name="name">rma.split.wizard.form</field>
<field name="model">rma.split.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<group>
<label for="product_uom_qty" />
<div class="o_row">
<field name="product_uom_qty" />
<field name="product_uom" groups="uom.group_uom" />
<field name="product_uom" invisible="1" />
</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,179 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from copy import deepcopy
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import float_compare
class ReturnPickingLine(models.TransientModel):
_inherit = "stock.return.picking.line"
rma_operation_id = fields.Many2one(
comodel_name="rma.operation",
string="Operation",
compute="_compute_rma_operation_id",
store=True,
readonly=False,
)
return_product_id = fields.Many2one(
"product.product",
help="Product to be returned if it's different from the originally delivered "
"item.",
)
different_return_product = fields.Boolean(
related="rma_operation_id.different_return_product"
)
@api.depends("wizard_id.rma_operation_id")
def _compute_rma_operation_id(self):
for rec in self:
if rec.wizard_id.rma_operation_id:
rec.rma_operation_id = rec.wizard_id.rma_operation_id
def _prepare_rma_vals(self):
self.ensure_one()
return {
"move_id": self.move_id.id,
"product_id": self.move_id.product_id.id,
"product_uom_qty": self.quantity,
"product_uom": self.product_id.uom_id.id,
"location_id": self.wizard_id.location_id.id or self.move_id.location_id.id,
"operation_id": self.rma_operation_id.id,
"return_product_id": self.return_product_id.id,
}
class ReturnPicking(models.TransientModel):
_inherit = "stock.return.picking"
create_rma = fields.Boolean(string="Create RMAs")
picking_type_code = fields.Selection(related="picking_id.picking_type_id.code")
rma_location_ids = fields.Many2many(
comodel_name="stock.location", compute="_compute_rma_location_id"
)
rma_operation_id = fields.Many2one(
comodel_name="rma.operation",
string="Requested operation",
)
# Expand domain for RMAs
location_id = fields.Many2one(
domain="create_rma and [('id', 'child_of', rma_location_ids)]"
"or "
"['|', ('id', '=', original_location_id), '|', '&', "
"('return_location', '=', True), ('company_id', '=', False), '&', "
"('return_location', '=', True), ('company_id', '=', company_id)]"
)
@api.depends("picking_id")
def _compute_rma_location_id(self):
for record in self:
record.rma_location_ids = (
self.env["stock.warehouse"]
.search([("company_id", "=", record.picking_id.company_id.id)])
.rma_loc_id
)
@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
# We want to avoid setting the return move `to_refund` as it will change
# the delivered quantities in the sale and set them to invoice.
self.product_return_moves.to_refund = False
else:
# If self.create_rma is not True, the value of the location 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
def _prepare_rma_partner_values(self):
self.ensure_one()
partner = self.picking_id.partner_id
partner_address = partner.address_get(["invoice", "delivery"])
partner_invoice_id = partner_address.get("invoice", False)
partner_shipping_id = partner_address.get("delivery", False)
return (
partner,
partner_invoice_id and partner.browse(partner_invoice_id) or partner,
partner_shipping_id and partner.browse(partner_shipping_id) or partner,
)
def _prepare_rma_vals(self):
partner, partner_invoice, partner_shipping = self._prepare_rma_partner_values()
origin = self.picking_id.name
return {
"user_id": self.env.user.id,
"partner_id": partner.id,
"partner_shipping_id": partner_shipping.id,
"partner_invoice_id": partner_invoice.id,
"origin": origin,
"picking_id": self.picking_id.id,
"company_id": self.company_id.id,
}
def _prepare_rma_vals_list(self):
vals_list = []
for return_picking in self:
global_vals = return_picking._prepare_rma_vals()
for line in return_picking.product_return_moves:
if (
not line.move_id
or float_compare(line.quantity, 0, line.product_id.uom_id.rounding)
<= 0
):
continue
vals = deepcopy(global_vals)
vals.update(line._prepare_rma_vals())
vals_list.append(vals)
return vals_list
def create_returns(self):
"""Override create_returns method for creating one or more
'confirmed' RMAs after return a delivery picking in case
'Create RMAs' checkbox is checked in this wizard.
New RMAs will be linked to the delivery picking as the origin
delivery and also RMAs will be linked to the returned picking
as the 'Receipt'.
"""
if self.create_rma:
if not self.picking_id.partner_id:
raise ValidationError(
_(
"You must specify the 'Customer' in the "
"'Stock Picking' from which RMAs will be created"
)
)
vals_list = self._prepare_rma_vals_list()
rmas = self.env["rma"].create(vals_list)
rmas.action_confirm()
picking = rmas.reception_move_id.picking_id
picking = picking and picking[0] or picking
ctx = dict(self.env.context)
ctx.update(
{
"default_partner_id": picking.partner_id.id,
"search_default_picking_type_id": picking.picking_type_id.id,
"search_default_draft": False,
"search_default_assigned": False,
"search_default_confirmed": False,
"search_default_ready": False,
"search_default_planning_issues": False,
"search_default_available": False,
}
)
return {
"name": _("Returned Picking"),
"view_mode": "form,tree,calendar",
"res_model": "stock.picking",
"res_id": picking.id,
"type": "ir.actions.act_window",
"context": ctx,
}
return super().create_returns()

View file

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