From eb0f8c23ee51a22733e0d1310a78264bf0917cbf Mon Sep 17 00:00:00 2001 From: Ernesto Tejeda Date: Wed, 13 May 2020 00:27:46 -0400 Subject: [PATCH] [ADD] rma: new module [UPD] Update rma.pot Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: rma-12.0/rma-12.0-rma Translate-URL: https://translation.odoo-community.org/projects/rma-12-0/rma-12-0-rma/ --- rma/README.rst | 152 ++ rma/__init__.py | 6 + rma/__manifest__.py | 37 + rma/controllers/__init__.py | 3 + rma/controllers/main.py | 132 ++ rma/data/mail_data.xml | 46 + rma/data/rma_operation_data.xml | 12 + rma/hooks.py | 70 + rma/i18n/es.po | 1635 +++++++++++++++++++++ rma/i18n/rma.pot | 1485 +++++++++++++++++++ rma/models/__init__.py | 12 + rma/models/account_invoice.py | 35 + rma/models/res_company.py | 23 + rma/models/res_partner.py | 41 + rma/models/res_users.py | 14 + rma/models/rma.py | 1144 ++++++++++++++ rma/models/rma_operation.py | 15 + rma/models/rma_team.py | 56 + rma/models/stock_move.py | 96 ++ rma/models/stock_picking.py | 41 + rma/models/stock_warehouse.py | 118 ++ rma/readme/CONFIGURE.rst | 8 + rma/readme/CONTRIBUTORS.rst | 5 + rma/readme/DESCRIPTION.rst | 8 + rma/readme/ROADMAP.rst | 3 + rma/readme/USAGE.rst | 37 + rma/report/report.xml | 14 + rma/security/ir.model.access.csv | 8 + rma/security/rma_security.xml | 60 + rma/static/description/icon.png | Bin 0 -> 7450 bytes rma/static/description/index.html | 496 +++++++ rma/tests/__init__.py | 3 + rma/tests/test_rma.py | 638 ++++++++ rma/views/menus.xml | 24 + rma/views/report_rma.xml | 96 ++ rma/views/res_partner_views.xml | 23 + rma/views/rma_portal_templates.xml | 269 ++++ rma/views/rma_team_views.xml | 90 ++ rma/views/rma_views.xml | 285 ++++ rma/views/stock_picking_views.xml | 23 + rma/views/stock_warehouse_views.xml | 17 + rma/wizard/__init__.py | 5 + rma/wizard/rma_delivery.py | 102 ++ rma/wizard/rma_delivery_views.xml | 50 + rma/wizard/rma_split.py | 70 + rma/wizard/rma_split_views.xml | 33 + rma/wizard/stock_picking_return.py | 83 ++ rma/wizard/stock_picking_return_views.xml | 20 + 48 files changed, 7643 insertions(+) create mode 100644 rma/README.rst create mode 100644 rma/__init__.py create mode 100644 rma/__manifest__.py create mode 100644 rma/controllers/__init__.py create mode 100644 rma/controllers/main.py create mode 100644 rma/data/mail_data.xml create mode 100644 rma/data/rma_operation_data.xml create mode 100644 rma/hooks.py create mode 100644 rma/i18n/es.po create mode 100644 rma/i18n/rma.pot create mode 100644 rma/models/__init__.py create mode 100644 rma/models/account_invoice.py create mode 100644 rma/models/res_company.py create mode 100644 rma/models/res_partner.py create mode 100644 rma/models/res_users.py create mode 100644 rma/models/rma.py create mode 100644 rma/models/rma_operation.py create mode 100644 rma/models/rma_team.py create mode 100644 rma/models/stock_move.py create mode 100644 rma/models/stock_picking.py create mode 100644 rma/models/stock_warehouse.py create mode 100644 rma/readme/CONFIGURE.rst create mode 100644 rma/readme/CONTRIBUTORS.rst create mode 100644 rma/readme/DESCRIPTION.rst create mode 100644 rma/readme/ROADMAP.rst create mode 100644 rma/readme/USAGE.rst create mode 100644 rma/report/report.xml create mode 100644 rma/security/ir.model.access.csv create mode 100644 rma/security/rma_security.xml create mode 100644 rma/static/description/icon.png create mode 100644 rma/static/description/index.html create mode 100644 rma/tests/__init__.py create mode 100644 rma/tests/test_rma.py create mode 100644 rma/views/menus.xml create mode 100644 rma/views/report_rma.xml create mode 100644 rma/views/res_partner_views.xml create mode 100644 rma/views/rma_portal_templates.xml create mode 100644 rma/views/rma_team_views.xml create mode 100644 rma/views/rma_views.xml create mode 100644 rma/views/stock_picking_views.xml create mode 100644 rma/views/stock_warehouse_views.xml create mode 100644 rma/wizard/__init__.py create mode 100644 rma/wizard/rma_delivery.py create mode 100644 rma/wizard/rma_delivery_views.xml create mode 100644 rma/wizard/rma_split.py create mode 100644 rma/wizard/rma_split_views.xml create mode 100644 rma/wizard/stock_picking_return.py create mode 100644 rma/wizard/stock_picking_return_views.xml diff --git a/rma/README.rst b/rma/README.rst new file mode 100644 index 00000000..788f6684 --- /dev/null +++ b/rma/README.rst @@ -0,0 +1,152 @@ +=========================================== +Return Merchandise Authorization Management +=========================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frma-lightgray.png?logo=github + :target: https://github.com/OCA/rma/tree/12.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-12-0/rma-12-0-rma + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/145/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows you to manage `Return Merchandise Authorization (RMA) +`_. +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'. + +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. + +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. + +**Note: An RMA can also be created from an incoming email (See configuration +section).** + +Known issues / Roadmap +====================== + +* As soon as the picking is selected, the user should select the move, + but perhaps stock.move _rec_name could be improved to better show what + the product of that move is. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `_: + + * Ernesto Tejeda + * Pedro M. Baeza + * David Vidal + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-ernestotejeda| image:: https://github.com/ernestotejeda.png?size=40px + :target: https://github.com/ernestotejeda + :alt: ernestotejeda + +Current `maintainer `__: + +|maintainer-ernestotejeda| + +This module is part of the `OCA/rma `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/rma/__init__.py b/rma/__init__.py new file mode 100644 index 00000000..336227cc --- /dev/null +++ b/rma/__init__.py @@ -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 diff --git a/rma/__manifest__.py b/rma/__manifest__.py new file mode 100644 index 00000000..c06a9a0a --- /dev/null +++ b/rma/__manifest__.py @@ -0,0 +1,37 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Return Merchandise Authorization Management", + "summary": "Return Merchandise Authorization (RMA)", + "version": "12.0.1.0.0", + "development_status": "Beta", + "category": "RMA", + "website": "https://github.com/OCA/rma", + "author": "Tecnativa, Odoo Community Association (OCA)", + "maintainers": ["ernestotejeda"], + "license": "AGPL-3", + "depends": [ + "account", + "stock", + ], + "data": [ + "views/report_rma.xml", + "report/report.xml", + "data/mail_data.xml", + "data/rma_operation_data.xml", + "security/rma_security.xml", + "security/ir.model.access.csv", + "wizard/stock_picking_return_views.xml", + "wizard/rma_delivery_views.xml", + "wizard/rma_split_views.xml", + "views/menus.xml", + "views/res_partner_views.xml", + "views/rma_portal_templates.xml", + "views/rma_team_views.xml", + "views/rma_views.xml", + "views/stock_picking_views.xml", + "views/stock_warehouse_views.xml", + ], + 'post_init_hook': 'post_init_hook', + "application": True, +} diff --git a/rma/controllers/__init__.py b/rma/controllers/__init__.py new file mode 100644 index 00000000..f43232f0 --- /dev/null +++ b/rma/controllers/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import main diff --git a/rma/controllers/main.py b/rma/controllers/main.py new file mode 100644 index 00000000..f53942f1 --- /dev/null +++ b/rma/controllers/main.py @@ -0,0 +1,132 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, exceptions, http +from odoo.exceptions import AccessError, MissingError +from odoo.http import request +from odoo.addons.portal.controllers.portal import CustomerPortal,\ + pager as portal_pager +from odoo.tools import consteq + + +class PortalRma(CustomerPortal): + + def _prepare_portal_layout_values(self): + values = super()._prepare_portal_layout_values() + values['rma_count'] = request.env['rma'].search_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/'], + type='http', auth="user", website=True) + def portal_my_rmas(self, page=1, date_begin=None, date_end=None, + sortby=None, **kw): + values = self._prepare_portal_layout_values() + rma_obj = request.env['rma'] + domain = self._get_filter_domain(kw) + searchbar_sortings = { + 'date': {'label': _('Date'), 'order': 'date desc'}, + 'name': {'label': _('Name'), 'order': 'name desc'}, + 'state': {'label': _('Status'), 'order': 'state'}, + } + # default sort by order + if not sortby: + sortby = 'date' + order = searchbar_sortings[sortby]['order'] + archive_groups = self._get_archive_groups('rma', domain) + 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, + 'archive_groups': archive_groups, + 'default_url': '/my/rmas', + 'searchbar_sortings': searchbar_sortings, + 'sortby': sortby, + }) + return request.render("rma.portal_my_rmas", values) + + @http.route(['/my/rmas/'], + 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/'], + type='http', auth="public", website=True) + def portal_my_rma_picking_report(self, picking_id, access_token=None, + **kw): + try: + picking_sudo = self._stock_picking_check_access( + picking_id, access_token=access_token) + except exceptions.AccessError: + return request.redirect('/my') + report_sudo = request.env.ref('stock.action_report_delivery').sudo() + pdf = report_sudo.render_qweb_pdf([picking_sudo.id])[0] + pdfhttpheaders = [ + ('Content-Type', 'application/pdf'), + ('Content-Length', len(pdf)), + ] + return request.make_response(pdf, headers=pdfhttpheaders) + + def _stock_picking_check_access(self, picking_id, access_token=None): + 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( + picking_sudo.sale_id.access_token, access_token): + raise + return picking_sudo diff --git a/rma/data/mail_data.xml b/rma/data/mail_data.xml new file mode 100644 index 00000000..207747bc --- /dev/null +++ b/rma/data/mail_data.xml @@ -0,0 +1,46 @@ + + + + + Draft RMA + rma + + New RMA in draft state + + + + Draft RMA + 10 + rma.team + + + team_id + + + + RMA Notification + + ${object.user_id.email_formatted |safe} + ${object.partner_id.id} + ${object.company_id.name} RMA (Ref ${object.name or 'n/a' }) + + ${(object.name or '')} + ${object.partner_id.lang} + + + +
+

+ Dear ${object.partner_id.name} + % if object.partner_id.parent_id: + (${object.partner_id.parent_id.name}) + % endif +

+ Here is the RMA ${object.name} from ${object.company_id.name}. +

+ Do not hesitate to contact us if you have any question. +

+
+
+
+
diff --git a/rma/data/rma_operation_data.xml b/rma/data/rma_operation_data.xml new file mode 100644 index 00000000..60ea6e04 --- /dev/null +++ b/rma/data/rma_operation_data.xml @@ -0,0 +1,12 @@ + + + + Replace + + + Repair + + + Refund + + diff --git a/rma/hooks.py b/rma/hooks.py new file mode 100644 index 00000000..f5e24d84 --- /dev/null +++ b/rma/hooks.py @@ -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, SUPERUSER_ID + + +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'] + location_vals = warehouse._get_locations_values({}) + for field_name, values in location_vals.items(): + if field_name == 'rma_loc_id' and not warehouse.rma_loc_id: + warehouse.rma_loc_id = stock_location.with_context( + active_test=False).create(values).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 + + 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) + }) + 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) + }) + whs.write(data) + + # Create rma locations and picking types + warehouses = env['stock.warehouse'].search([]) + for warehouse in warehouses: + create_rma_locations(warehouse) + create_rma_picking_types(warehouse) + # Create rma sequence per company + for company in env['res.company'].search([]): + company.create_rma_index() diff --git a/rma/i18n/es.po b/rma/i18n/es.po new file mode 100644 index 00000000..242403e8 --- /dev/null +++ b/rma/i18n/es.po @@ -0,0 +1,1635 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * rma +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-06-21 05:13+0000\n" +"PO-Revision-Date: 2020-06-21 01:16-0400\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: \n" +"X-Generator: Poedit 2.0.6\n" + +#. module: rma +#: model:mail.template,report_name:rma.mail_template_rma_notification +msgid "${(object.name or '')}" +msgstr "" + +#. module: rma +#: model:mail.template,subject:rma.mail_template_rma_notification +msgid "${object.company_id.name} RMA (Ref ${object.name or 'n/a' })" +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma_team.py:43 +#, python-format +msgid "%s (copy)" +msgstr "%s (copia)" + +#. module: rma +#: model:mail.template,body_html:rma.mail_template_rma_notification +msgid "" +"
\n" +"

\n" +" Dear ${object.partner_id.name}\n" +" % if object.partner_id.parent_id:\n" +" (${object.partner_id.parent_id.name})\n" +" % endif\n" +"

\n" +" Here is the RMA ${object.name} from ${object.company_id." +"name}.\n" +"

\n" +" Do not hesitate to contact us if you have any question.\n" +"

\n" +"
\n" +" " +msgstr "" +"
\n" +"

\n" +" Estimado ${object.partner_id.name}\n" +" % if object.partner_id.parent_id:\n" +" (${object.partner_id.parent_id.name})\n" +" % endif\n" +"

\n" +" Aquí tiene el RMA ${object.name} Desde ${object." +"company_id.name}.\n" +"

\n" +" No dude en ponerse en contacto con nosotros si tiene alguna pregunta.\n" +"

\n" +"
\n" +" " + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "" +"" +msgstr "" +"" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid " Paid" +msgstr "Pagado" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid " Waiting Payment" +msgstr "Esperando Pago" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "" +"" +msgstr "" +"" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "" +"" +msgstr "" +"" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "" +" Cancelled" +msgstr "" +"Cancelado" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "" +" Preparation" +msgstr "" +"Preparación" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "" +" Shipped" +msgstr "" +" Enviado" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "" +" Partially Available" +msgstr "" +"Disponible parcialmente" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Delivery" +msgstr "Órdenes de entrega" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Reception" +msgstr "Recepción" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Refund" +msgstr "Factura rectificativa" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Customer:" +msgstr "Cliente:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Date:" +msgstr "Fecha:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Deadline:" +msgstr "Fecha límite:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Deadline" +msgstr "Fecha límite:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Delivered qty:" +msgstr "Cantidad entregada:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Delivered quantity" +msgstr "Cantidad entregada" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Invoicing address:" +msgstr "Dirección de facturación:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Move:" +msgstr "Movimiento:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Origin delivery:" +msgstr "Orden de Entrega:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +#, fuzzy +#| msgid "Origin delivery:" +msgid "Origin delivery" +msgstr "Orden de Entrega:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Origin:" +msgstr "Referencia:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Origin" +msgstr "Referencia:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Product:" +msgstr "Producto:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Product" +msgstr "Producto" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Quantity:" +msgstr "Cantidad:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Quantity" +msgstr "Cantidad" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "RMA Date" +msgstr "Fecha" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "RMA Note:" +msgstr "Nota:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Responsible:" +msgstr "Responsable:" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "State:" +msgstr "Estado:" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_defaults +msgid "" +"A Python dictionary that will be evaluated to provide default values when " +"creating new records for this alias." +msgstr "" +"Diccionario Python a evaluar para proporcionar valores por defecto cuando un " +"nuevo registro se cree para este seudónimo." + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_team_view_form +msgid "Accept Emails From" +msgstr "Aceptar los correos electrónicos de" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__access_warning +msgid "Access warning" +msgstr "Alerta de acceso" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_needaction +#: model:ir.model.fields,field_description:rma.field_rma_team__message_needaction +msgid "Action Needed" +msgstr "Acción Necesaria" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__active +msgid "Active" +msgstr "Activo" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__activity_ids +msgid "Activities" +msgstr "Actividades" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__activity_state +msgid "Activity State" +msgstr "Estado de la actividad" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_id +msgid "Alias" +msgstr "Seudónimo" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_contact +msgid "Alias Contact Security" +msgstr "Seudónimo del contacto de seguridad" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_name +msgid "Alias Name" +msgstr "Seudónimo" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_domain +msgid "Alias domain" +msgstr "Seudónimo del dominio" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_model_id +msgid "Aliased Model" +msgstr "Modelo con seudónimo" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Are you sure you want to cancel this RMA" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_attachment_count +#: model:ir.model.fields,field_description:rma.field_rma_team__message_attachment_count +msgid "Attachment Count" +msgstr "Conteo de archivos adjuntos" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_team_view_form +msgid "Avatar" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__can_be_locked +msgid "Can Be Locked" +msgstr "Puede ser bloquedo" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__can_be_refunded +msgid "Can Be Refunded" +msgstr "Puede ser reembolsado" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__can_be_replaced +msgid "Can Be Replaced" +msgstr "Puede ser reemplazado" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__can_be_returned +msgid "Can Be Returned" +msgstr "Puede ser devuelto" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__can_be_split +msgid "Can Be Split" +msgstr "Puede ser dividido" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_redelivery_wizard_view_form +#: model_terms:ir.ui.view,arch_db:rma.rma_split_wizard_view_form2 +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Cancel" +msgstr "Cancelar" + +#. module: rma +#: selection:rma,state:0 +msgid "Canceled" +msgstr "Cancelado" + +#. module: rma +#: model_terms:ir.actions.act_window,help:rma.rma_action +#: model_terms:ir.actions.act_window,help:rma.rma_team_action +msgid "Click to add a new RMA." +msgstr "Click para agregar un nuevo RMA." + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__commercial_partner_id +msgid "Commercial Entity" +msgstr "Entidad comercial" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Communication" +msgstr "Comunicación" + +#. module: rma +#: model:ir.model,name:rma.model_res_company +msgid "Companies" +msgstr "Compañías" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__company_id +#: model:ir.model.fields,field_description:rma.field_rma_team__company_id +msgid "Company" +msgstr "Compañía" + +#. module: rma +#: model:ir.ui.menu,name:rma.rma_configuration_menu +msgid "Configuration" +msgstr "Configuración" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Confirm" +msgstr "Confirmar" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search selection:rma,state:0 +msgid "Confirmed" +msgstr "Confirmado" + +#. module: rma +#: model:ir.model,name:rma.model_res_partner +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Contact" +msgstr "Contacto" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_return_picking__create_rma +msgid "Create RMAs" +msgstr "Crear RMAs" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__create_uid +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__create_uid +#: model:ir.model.fields,field_description:rma.field_rma_operation__create_uid +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__create_uid +#: model:ir.model.fields,field_description:rma.field_rma_team__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__create_date +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__create_date +#: model:ir.model.fields,field_description:rma.field_rma_operation__create_date +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__create_date +#: model:ir.model.fields,field_description:rma.field_rma_team__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__partner_id +msgid "Customer" +msgstr "Cliente" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__access_url +msgid "Customer Portal URL" +msgstr "URL del portal de cliente" + +#. module: rma +#: code:addons/rma/controllers/main.py:38 +#: model:ir.model.fields,field_description:rma.field_rma__date +#: model_terms:ir.ui.view,arch_db:rma.portal_my_rmas +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +#, python-format +msgid "Date" +msgstr "Fecha" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Date:" +msgstr "Fecha:" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__deadline +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +msgid "Deadline" +msgstr "Fecha límite" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_defaults +msgid "Default Values" +msgstr "Valores por defecto" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_redelivery_wizard_view_form +msgid "Deliver" +msgstr "Entregar" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__delivered_qty +msgid "Delivered qty" +msgstr "Ctd. entregada" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__delivered_qty_done +msgid "Delivered qty done" +msgstr "Ctd. entregada realizada" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Delivery" +msgstr "Entrega" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__delivery_picking_count +msgid "Delivery count" +msgstr "Cantidad de entregas" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__delivery_move_ids +msgid "Delivery reservation" +msgstr "Movimientos de entrega" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__description +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Description" +msgstr "Descripción" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__display_name +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__display_name +#: model:ir.model.fields,field_description:rma.field_rma_operation__display_name +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__display_name +#: model:ir.model.fields,field_description:rma.field_rma_team__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search selection:rma,state:0 +msgid "Draft" +msgstr "Borrador" + +#. module: rma +#: model:mail.message.subtype,name:rma.mt_rma_draft +#: model:mail.message.subtype,name:rma.mt_rma_team_rma_draft +msgid "Draft RMA" +msgstr "RMA en estado Borrador" + +#. module: rma +#: code:addons/rma/models/rma.py:1076 +#, python-format +msgid "" +"E-mail subject: %s\n" +"\n" +"E-mail body:\n" +"%s" +msgstr "" +"Asunto del correo electrónico: %s\n" +"\n" +"Cuerpo del correo electrónico:\n" +"%s" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_team_view_form +msgid "Email" +msgstr "Correo electrónico" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_team_view_form +msgid "Email Alias" +msgstr "Pseudónimo de correo" + +#. module: rma +#: code:addons/rma/wizard/rma_split.py:63 +#, python-format +msgid "Extracted RMA" +msgstr "RMA Extraído" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__origin_split_rma_id +msgid "Extracted from" +msgstr "Extraído de" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_follower_ids +#: model:ir.model.fields,field_description:rma.field_rma_team__message_follower_ids +msgid "Followers" +msgstr "Seguidores" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_channel_ids +#: model:ir.model.fields,field_description:rma.field_rma_team__message_channel_ids +msgid "Followers (Channels)" +msgstr "Seguidores (Canales)" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_partner_ids +#: model:ir.model.fields,field_description:rma.field_rma_team__message_partner_ids +msgid "Followers (Partners)" +msgstr "Seguidores (Empresas)" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +msgid "Group By" +msgstr "Agrupar por" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__id +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__id +#: model:ir.model.fields,field_description:rma.field_rma_operation__id +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__id +#: model:ir.model.fields,field_description:rma.field_rma_team__id +msgid "ID" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_parent_thread_id +msgid "" +"ID of the parent record holding the alias (example: project holding the task " +"creation alias)" +msgstr "" +"ID del registro padre que tiene el seudónimo. (ejemplo: el proyecto que " +"contiene el seudónimo para la creación de tareas)" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__message_unread +#: model:ir.model.fields,help:rma.field_rma_team__message_unread +msgid "If checked new messages require your attention." +msgstr "Si está marcado, hay nuevos mensajes que requieren su atención" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__message_needaction +#: model:ir.model.fields,help:rma.field_rma_team__message_needaction +msgid "If checked, new messages require your attention." +msgstr "Si está marcado, hay nuevos mensajes que requieren su atención." + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__message_has_error +#: model:ir.model.fields,help:rma.field_rma_team__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "Si se encuentra marcado, algunos mensajes tienen error de envío." + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__active +msgid "" +"If the active field is set to false, it will allow you to hide the RMA Team " +"without removing it." +msgstr "" +"Si el campo activo se establece a Falso, permitirá ocultar El equipo de RMA " +"sin eliminarlo." + +#. module: rma +#: code:addons/rma/models/rma.py:1080 +#, python-format +msgid "Incoming e-mail" +msgstr "Correo electrónico entrante" + +#. module: rma +#: model:ir.model,name:rma.model_account_invoice +msgid "Invoice" +msgstr "Factura" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__partner_invoice_id +msgid "Invoice Address" +msgstr "Dirección de factura" + +#. module: rma +#: model:ir.model,name:rma.model_account_invoice_line +msgid "Invoice Line" +msgstr "Linea de factura" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_is_follower +#: model:ir.model.fields,field_description:rma.field_rma_team__message_is_follower +msgid "Is Follower" +msgstr "Es un seguidor" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma____last_update +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard____last_update +#: model:ir.model.fields,field_description:rma.field_rma_operation____last_update +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard____last_update +#: model:ir.model.fields,field_description:rma.field_rma_team____last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__write_uid +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__write_uid +#: model:ir.model.fields,field_description:rma.field_rma_operation__write_uid +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__write_uid +#: model:ir.model.fields,field_description:rma.field_rma_team__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__write_date +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__write_date +#: model:ir.model.fields,field_description:rma.field_rma_operation__write_date +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__write_date +#: model:ir.model.fields,field_description:rma.field_rma_team__write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__location_id +msgid "Location" +msgstr "Ubicación" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Lock" +msgstr "Bloquear" + +#. module: rma +#: selection:rma,state:0 +msgid "Locked" +msgstr "Bloqueado" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_main_attachment_id +#: model:ir.model.fields,field_description:rma.field_rma_team__message_main_attachment_id +msgid "Main Attachment" +msgstr "Adjuntos principales" + +#. module: rma +#: model:ir.module.category,description:rma.rma_module_category +msgid "Manage Return Merchandise Authorizations (RMAs)." +msgstr "Autorización de Devolución de Mercancía (RMA)." + +#. module: rma +#: model:res.groups,name:rma.rma_group_manager +msgid "Manager" +msgstr "Responsable" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_has_error +#: model:ir.model.fields,field_description:rma.field_rma_team__message_has_error +msgid "Message Delivery error" +msgstr "Error de Envío de Mensaje" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_ids +#: model:ir.model.fields,field_description:rma.field_rma_team__message_ids +msgid "Messages" +msgstr "Mensajes" + +#. module: rma +#: code:addons/rma/controllers/main.py:39 +#: model:ir.model.fields,field_description:rma.field_rma__name +#: model:ir.model.fields,field_description:rma.field_rma_operation__name +#: model:ir.model.fields,field_description:rma.field_rma_team__name +#, python-format +msgid "Name" +msgstr "Nombre" + +#. module: rma +#: code:addons/rma/models/rma.py:31 code:addons/rma/models/rma.py:496 +#: code:addons/rma/models/rma.py:1079 +#, python-format +msgid "New" +msgstr "Nuevo" + +#. module: rma +#: model:mail.message.subtype,description:rma.mt_rma_draft +msgid "New RMA in draft state" +msgstr "Nuevo RMA en estado Borrador" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Siguiente plazo de actividad" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__activity_summary +msgid "Next Activity Summary" +msgstr "Resumen de la siguiente actividad" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__activity_type_id +msgid "Next Activity Type" +msgstr "Siguiente tipo de actividad" + +#. module: rma +#: code:addons/rma/models/rma.py:757 +#, python-format +msgid "None of the selected RMAs can perform a replacement." +msgstr "Ninguno de los RMAs seleccionados puede realizar un reemplazo." + +#. module: rma +#: code:addons/rma/models/rma.py:740 +#, python-format +msgid "None of the selected RMAs can perform a return." +msgstr "Ninguno de los RMAs seleccionados puede realizar una devolución." + +#. module: rma +#: selection:rma,priority:0 +msgid "Normal" +msgstr "" + +#. module: rma +#: selection:rma,priority:0 +msgid "Not urgent" +msgstr "No Urgente" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_needaction_counter +#: model:ir.model.fields,field_description:rma.field_rma_team__message_needaction_counter +msgid "Number of Actions" +msgstr "Número de acciones" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_has_error_counter +#: model:ir.model.fields,field_description:rma.field_rma_team__message_has_error_counter +msgid "Number of error" +msgstr "Número de error" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__message_needaction_counter +#: model:ir.model.fields,help:rma.field_rma_team__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "Número de mensajes que requieren una acción" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__message_has_error_counter +#: model:ir.model.fields,help:rma.field_rma_team__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Número de mensajes con error de envío" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__message_unread_counter +#: model:ir.model.fields,help:rma.field_rma_team__message_unread_counter +msgid "Number of unread messages" +msgstr "Número de mensajes no leidos" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_force_thread_id +msgid "" +"Optional ID of a thread (record) to which all incoming messages will be " +"attached, even if they did not reply to it. If set, this will disable the " +"creation of new records completely." +msgstr "" +"Id. opcional de un hilo (registro) al que todos los mensajes entrantes serán " +"adjuntados, incluso si no fueron respuestas del mismo. Si se establece, se " +"deshabilitará completamente la creación de nuevos registros." + +#. module: rma +#: model:ir.ui.menu,name:rma.rma_orders_menu +msgid "Orders" +msgstr "Órdenes" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__picking_id +msgid "Origin Delivery" +msgstr "Orden de Entrega" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__move_id +msgid "Origin move" +msgstr "Movimiento" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Other Information" +msgstr "Otra información" + +#. module: rma +#: selection:rma,activity_state:0 +msgid "Overdue" +msgstr "Vencidas" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_user_id +msgid "Owner" +msgstr "Propietario" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_parent_model_id +msgid "Parent Model" +msgstr "Modelo padre" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_parent_thread_id +msgid "Parent Record Thread ID" +msgstr "ID del hilo del registro padre" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_parent_model_id +msgid "" +"Parent model holding the alias. The model holding the alias reference is not " +"necessarily the model given by alias_model_id (example: project " +"(parent_model) and task (model))" +msgstr "" +"Modelo padre que contiene el alias. El modelo que contiene la referencia " +"alias no es necesariamente el modelo dado por alias_model_id" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +msgid "Partner" +msgstr "Empresa" + +#. module: rma +#: selection:rma,activity_state:0 +msgid "Planned" +msgstr "Planeado" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_contact +msgid "" +"Policy to post a message on the document using the mailgateway.\n" +"- everyone: everyone can post\n" +"- partners: only authenticated partners\n" +"- followers: only followers of the related document or members of following " +"channels\n" +msgstr "" +"Política para publicar un mensaje en el documento utilizando el servidor de " +"correo.\n" +"- todo el mundo: todos pueden publicar\n" +"- socios: sólo socios autenticados\n" +"- seguidores: sólo seguidores del documento relacionado o miembros de los " +"siguientes canales\n" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__access_url +msgid "Portal Access URL" +msgstr "URL de acceso al portal" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Preview" +msgstr "Previsualizar" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__priority +msgid "Priority" +msgstr "Prioridad" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__procurement_group_id +msgid "Procurement group" +msgstr "Grupo de abastecimiento" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__product_id +#: model_terms:ir.ui.view,arch_db:rma.portal_my_rmas +msgid "Product" +msgstr "Producto" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__product_uom_qty +msgid "Product qty" +msgstr "Cantidad" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__product_uom_qty +#: model_terms:ir.ui.view,arch_db:rma.portal_my_rmas +msgid "Quantity" +msgstr "Cantidad" + +#. module: rma +#: code:addons/rma/wizard/rma_delivery.py:49 sql_constraint:rma.split.wizard:0 +#, python-format +msgid "Quantity must be greater than 0." +msgstr "La cantidad debe ser mayor que cero." + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__product_uom_qty +msgid "Quantity to extract" +msgstr "Cantidad a extraer" + +#. module: rma +#: code:addons/rma/models/rma.py:790 +#, python-format +msgid "" +"Quantity to extract cannot be greater than remaining delivery quantity (%s " +"%s)" +msgstr "" +"La cantidad a extraer no puede ser mayor que la cantidad de entrega " +"restante(%s %s)" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_split_wizard__product_uom_qty +msgid "Quantity to extract to a new RMA." +msgstr "Cantidad a extraer en nuevo RMA" + +#. module: rma +#: model:ir.actions.act_window,name:rma.rma_action +#: model:ir.model,name:rma.model_rma +#: model:ir.model.fields,field_description:rma.field_account_invoice_line__rma_id +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__rma_id +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma +#: model:ir.module.category,name:rma.rma_module_category +#: model:ir.ui.menu,name:rma.rma_menu +#: model_terms:ir.ui.view,arch_db:rma.view_partner_form +#: model_terms:ir.ui.view,arch_db:rma.view_picking_form +msgid "RMA" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_my_rmas +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "RMA #" +msgstr "" + +#. module: rma +#: code:addons/rma/models/res_company.py:18 +#, python-format +msgid "RMA Code" +msgstr "Código de RMA" + +#. module: rma +#: code:addons/rma/models/stock_warehouse.py:82 +#, python-format +msgid "RMA Delivery Orders" +msgstr "Órdenes de entrega de RMA" + +#. module: rma +#: model:ir.model,name:rma.model_rma_delivery_wizard +msgid "RMA Delivery Wizard" +msgstr "Asistente de entrega de RMA" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_in_type_id +msgid "RMA In Type" +msgstr "Tipo de operación para recepción de RMA" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_loc_id +msgid "RMA Location" +msgstr "Ubicación de RMA" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "RMA Order -" +msgstr "Orden de RMA -" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_my_home_menu_rma +#: model_terms:ir.ui.view,arch_db:rma.portal_my_home_rma +#: model_terms:ir.ui.view,arch_db:rma.portal_my_rmas +msgid "RMA Orders" +msgstr "Órdenes de RMA" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_out_type_id +msgid "RMA Out Type" +msgstr "Tipo de operación para entrega de RMA" + +#. module: rma +#: code:addons/rma/models/stock_warehouse.py:73 +#, python-format +msgid "RMA Receipts" +msgstr "Recepciones de RMA" + +#. module: rma +#: model:ir.actions.report,name:rma.report_rma_action +msgid "RMA Report" +msgstr "Reporte de RMA" + +#. module: rma +#: model:ir.model,name:rma.model_rma_split_wizard +msgid "RMA Split Wizard" +msgstr "Asistente para dividir RMA" + +#. module: rma +#: model:ir.model,name:rma.model_rma_team +#: model:ir.model.fields,field_description:rma.field_res_users__rma_team_id +#: model:ir.ui.menu,name:rma.rma_configuration_rma_team_menu +#: model_terms:ir.ui.view,arch_db:rma.rma_team_view_form +msgid "RMA Team" +msgstr "Equipo de RMA" + +#. module: rma +#: model:ir.model.fields,help:rma.field_res_users__rma_team_id +msgid "RMA Team the user is member of." +msgstr "Equipo de RMA del cual el usuario es miembro." + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_res_partner__rma_count +#: model:ir.model.fields,field_description:rma.field_res_users__rma_count +#: model:ir.model.fields,field_description:rma.field_stock_picking__rma_count +msgid "RMA count" +msgstr "Cantidad de RMAs" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_move__rma_receiver_ids +msgid "RMA receivers" +msgstr "RMAs que originaron esta orden" + +#. module: rma +#: model:ir.model.fields,help:rma.field_stock_warehouse__rma +msgid "RMA related products can be stored in this warehouse." +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_rma_operation +msgid "RMA requested operation" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_move__rma_id +msgid "RMA return" +msgstr "RMA que realizó esta devolución" + +#. module: rma +#: model:ir.actions.act_window,name:rma.rma_team_action +#: model:ir.model.fields,field_description:rma.field_rma__team_id +msgid "RMA team" +msgstr "Equipo de RMA" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_res_partner__rma_ids +#: model:ir.model.fields,field_description:rma.field_res_users__rma_ids +#: model:ir.model.fields,field_description:rma.field_stock_move__rma_ids +msgid "RMAs" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Receipt" +msgstr "Recepción" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search selection:rma,state:0 +msgid "Received" +msgstr "Recibido" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__reception_move_id +msgid "Reception move" +msgstr "Movimiento de recepción" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_force_thread_id +msgid "Record Thread ID" +msgstr "Id. del hilo de registro" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__origin +msgid "Reference of the document that generated this RMA." +msgstr "Referencia al documento que generó este RMA." + +#. module: rma +#: code:addons/rma/models/rma.py:679 +#: model:ir.model.fields,field_description:rma.field_rma__refund_id +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +#: model:rma.operation,name:rma.rma_operation_refund +#, python-format +msgid "Refund" +msgstr "Factura rectificativa" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__partner_invoice_id +msgid "Refund address for current RMA." +msgstr "Dirección de facturación de este RMA." + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__refund_line_id +msgid "Refund line" +msgstr "Línea de factura rectificativa" + +#. module: rma +#: selection:rma,state:0 +msgid "Refunded" +msgstr "reembolsado" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__remaining_qty +msgid "Remaining delivered qty" +msgstr "Ctd. entregada restante" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__remaining_qty_to_done +msgid "Remaining delivered qty to done" +msgstr "Ctd. entregada restante por realizar" + +#. module: rma +#: model:rma.operation,name:rma.rma_operation_return +msgid "Repair" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +#: selection:rma.delivery.wizard,type:0 +#: model:rma.operation,name:rma.rma_operation_replace +msgid "Replace" +msgstr "Reemplazar" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__product_id +msgid "Replace Product" +msgstr "Reemplazar producto" + +#. module: rma +#: selection:rma,state:0 +msgid "Replaced" +msgstr "Reemplazado" + +#. module: rma +#: code:addons/rma/models/rma.py:995 +#, python-format +msgid "" +"Replacement: Move %s (Picking %s) has been created." +msgstr "" +"Reemplazo: El movimiento %s (Orden de entrega %s) ha sido creado." + +#. module: rma +#: code:addons/rma/models/rma.py:1006 +#, python-format +msgid "" +"Replacement:
Product %s
Quantity %f %s
This replacement did not " +"create a new move, but one of the previously created moves was updated with " +"this data." +msgstr "" +"Reemplazo:
Producto %s
Cantidad %f %s
El reemplazo realizado no creó un " +"movimiento nuevo, pero uno de los movimientos creados anteriormente fué " +"actualizado con estos datos." + +#. module: rma +#: model:ir.ui.menu,name:rma.rma_reporting_menu +msgid "Reporting" +msgstr "Informes" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__operation_id +#, fuzzy +#| msgid "Type of Operation" +msgid "Requested operation" +msgstr "Tipo de operación" + +#. module: rma +#: code:addons/rma/models/rma.py:721 +#, python-format +msgid "Required field(s):%s" +msgstr "Campo(s) requerido(s):%s" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__user_id +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +msgid "Responsible" +msgstr "Responsable" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__activity_user_id +msgid "Responsible User" +msgstr "Usuario responsable" + +#. module: rma +#: model:ir.model,name:rma.model_stock_return_picking +msgid "Return Picking" +msgstr "Albarán de devolución" + +#. module: rma +#: model:ir.actions.act_window,name:rma.rma_delivery_wizard_action +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +#: selection:rma.delivery.wizard,type:0 +msgid "Return to customer" +msgstr "Devolver al cliente" + +#. module: rma +#: code:addons/rma/models/rma.py:956 +#, python-format +msgid "" +"Return: %s has been created." +msgstr "" +"Devolución: La orden de entrega %s ha sido creada." + +#. module: rma +#: selection:rma,state:0 +msgid "Returned" +msgstr "Devuelto" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__rma_count +msgid "Rma Count" +msgstr "Cantidad de RMAs" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__scheduled_date +msgid "Scheduled Date" +msgstr "Fecha prevista" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__access_token +msgid "Security Token" +msgstr "Token de seguridad" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Send by Email" +msgstr "Enviar por correo electrónico" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Send by Mail" +msgstr "Enviar por correo" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__sent +msgid "Sent" +msgstr "Enviado" + +#. module: rma +#: code:addons/rma/models/stock_warehouse.py:48 +#, python-format +msgid "Sequence RMA in" +msgstr "Secuencia de recepción de RMA" + +#. module: rma +#: code:addons/rma/models/stock_warehouse.py:53 +#, python-format +msgid "Sequence RMA out" +msgstr "Secuencia de entrega de RMA" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Set to draft" +msgstr "Establecer a borrador" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Share" +msgstr "Compartir" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__origin +msgid "Source Document" +msgstr "Documento origen" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_split_wizard_view_form2 +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Split" +msgstr "Dividir" + +#. module: rma +#: model:ir.actions.act_window,name:rma.rma_split_wizard_action +msgid "Split RMA" +msgstr "Dividir RMA" + +#. module: rma +#: code:addons/rma/models/rma.py:874 +#, python-format +msgid "" +"Split: %s has been " +"created." +msgstr "" +"División: El RMA %s ha sido creado." + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__state +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +msgid "State" +msgstr "Provincia" + +#. module: rma +#: code:addons/rma/controllers/main.py:40 +#: model_terms:ir.ui.view,arch_db:rma.portal_my_rmas +#, python-format +msgid "Status" +msgstr "Estado" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"Estado basado en actividades\n" +"Vencida: la fecha tope ya ha pasado\n" +"Hoy: La fecha tope es hoy\n" +"Planificada: futuras actividades." + +#. module: rma +#: model:ir.model,name:rma.model_stock_move +msgid "Stock Move" +msgstr "Movimiento de existencias" + +#. module: rma +#: model:ir.model,name:rma.model_stock_rule +msgid "Stock Rule" +msgstr "Regla de Inventario" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__user_id +msgid "Team Leader" +msgstr "Líder del equipo" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__member_ids +#: model_terms:ir.ui.view,arch_db:rma.rma_team_view_form +msgid "Team Members" +msgstr "Miembros del equipo" + +#. module: rma +#: sql_constraint:rma.operation:0 +msgid "That operation name already exists !" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_model_id +msgid "" +"The model (Odoo Document Kind) to which this alias corresponds. Any incoming " +"email that does not reply to an existing record will cause the creation of a " +"new record of this model (e.g. a Project Task)" +msgstr "" +"El modelo (Tipo de documento de Odoo) al que corresponde este seudónimo. " +"Cualquier correo entrante que no sea respuesta a un registro existente, " +"causará la creación de un nuevo registro de este modelo" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_name +msgid "" +"The name of the email alias, e.g. 'jobs' if you want to catch emails for " +"" +msgstr "" +"El nombre de este seudónimo de correo electrónico. Por ejemplo, \"trabajos" +"\", si lo que quiere es obtener los correos para ." + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_user_id +msgid "" +"The owner of records created upon receiving emails on this alias. If this " +"field is not set the system will attempt to find the right owner based on " +"the sender (From) address, or will use the Administrator account if no " +"system user is found for that address." +msgstr "" +"El propietario de los registros creados al recibir correos electrónicos en " +"este seudónimo. Si el campo no está establecido, el sistema tratará de " +"encontrar el propietario adecuado basado en la dirección del emisor (De), o " +"usará la cuenta de administrador si no se encuentra un usuario para esa " +"dirección." + +#. module: rma +#: code:addons/rma/models/stock_move.py:60 +#, python-format +msgid "" +"The quantity done for the product '%s' must be equal to its initial demand " +"because the stock move is linked to an RMA (%s)." +msgstr "" +"La cantidad realizada para el producto '%s' debe ser igual a la demanda " +"inicial porque el movimiento está enlazado a un RMA (%s)." + +#. module: rma +#: code:addons/rma/models/rma.py:778 +#, python-format +msgid "The quantity to return is greater than remaining quantity." +msgstr "La cantidad a devolver es mayor que la cantidad restante del RMA." + +#. module: rma +#: code:addons/rma/models/account_invoice.py:22 +#, python-format +msgid "" +"There is at least one invoice lines whose quantity is less than the quantity " +"specified in its linked RMA." +msgstr "" +"Hay al menos una linea de factura que tiene una cantidad menor que la " +"cantidad especificada en el RMA asociado." + +#. module: rma +#: code:addons/rma/models/rma.py:767 +#, python-format +msgid "This RMA cannot be split." +msgstr "Este RMA no puede ser dividido." + +#. module: rma +#: code:addons/rma/models/rma.py:754 +#, python-format +msgid "This RMA cannot perform a replacement." +msgstr "Este RMA no puede realizar un reemplazo." + +#. module: rma +#: code:addons/rma/models/rma.py:737 +#, python-format +msgid "This RMA cannot perform a return." +msgstr "Este RMA no puede realizar una devolución." + +#. module: rma +#: model:ir.actions.server,name:rma.rma_refund_action_server +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "To Refund" +msgstr "Reembolsar" + +#. module: rma +#: selection:rma,activity_state:0 +msgid "Today" +msgstr "Hoy" + +#. module: rma +#: model:ir.model,name:rma.model_stock_picking +msgid "Transfer" +msgstr "Transferir" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__type +msgid "Type" +msgstr "Tipo" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_return_picking__picking_type_code +msgid "Type of Operation" +msgstr "Tipo de operación" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +msgid "Unassigned RMAs" +msgstr "RMAs no asignados" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__product_uom +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__product_uom +msgid "Unit of measure" +msgstr "Unidad de Medida" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Unlock" +msgstr "Desbloquear" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_unread +#: model:ir.model.fields,field_description:rma.field_rma_team__message_unread +msgid "Unread Messages" +msgstr "Mensajes por leer" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_unread_counter +#: model:ir.model.fields,field_description:rma.field_rma_team__message_unread_counter +msgid "Unread Messages Counter" +msgstr "Contador de mensajes sin leer" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__product_uom +msgid "UoM" +msgstr "UdM" + +#. module: rma +#: selection:rma,priority:0 +msgid "Urgent" +msgstr "Urgente" + +#. module: rma +#: model:res.groups,name:rma.rma_group_user_all +msgid "User: All Documents" +msgstr "Usuario: Mostrar todos los documentos" + +#. module: rma +#: model:res.groups,name:rma.rma_group_user_own +msgid "User: Own Documents Only" +msgstr "Usuario: Solo mostrar documentos propios" + +#. module: rma +#: model:ir.model,name:rma.model_res_users +msgid "Users" +msgstr "Usuarios" + +#. module: rma +#: selection:rma,priority:0 +msgid "Very Urgent" +msgstr "Muy Urgente" + +#. module: rma +#: selection:rma,state:0 +msgid "Waiting for refund" +msgstr "Esperando por reembolso" + +#. module: rma +#: selection:rma,state:0 +msgid "Waiting for replacement" +msgstr "Esperando por reemplazo" + +#. module: rma +#: selection:rma,state:0 +msgid "Waiting for return" +msgstr "Esperando por devolución" + +#. module: rma +#: model:ir.model,name:rma.model_stock_warehouse +#: model:ir.model.fields,field_description:rma.field_rma__warehouse_id +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__warehouse_id +msgid "Warehouse" +msgstr "Almacén" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__website_message_ids +#: model:ir.model.fields,field_description:rma.field_rma_team__website_message_ids +msgid "Website Messages" +msgstr "Mensajes del sitio web" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__website_message_ids +#: model:ir.model.fields,help:rma.field_rma_team__website_message_ids +msgid "Website communication history" +msgstr "Historial de comunicaciones del sitio web" + +#. module: rma +#: code:addons/rma/models/rma.py:514 +#, python-format +msgid "You cannot delete RMAs that are not in draft state" +msgstr "No puede " + +#. module: rma +#: code:addons/rma/wizard/stock_picking_return.py:56 +#, python-format +msgid "" +"You must specify the 'Customer' in the 'Stock Picking' from which RMAs will " +"be created" +msgstr "" +"Debe seleccionar el 'Cliente' en la 'Orden de Entrega' desde la cual los " +"RMAs serán creados." + +#. module: rma +#: model:res.groups,comment:rma.rma_group_user_all +msgid "" +"the user will have access to all records of everyone in the RMA application." +msgstr "" +"El usuario tendrá acceso a todos los registros de RMA de todos lo usuarios." + +#. module: rma +#: model:res.groups,comment:rma.rma_group_user_own +msgid "the user will have access to his own data in the RMA application." +msgstr "El usuario tendrá acceso solo a sus propios RMAs" + +#. module: rma +#: model:res.groups,comment:rma.rma_group_manager +msgid "" +"the user will have an access to the RMA configuration as well as statistic " +"reports." +msgstr "" +"El usuario tendrá acceso a la configuración de RMA y a los informes " +"estadísticos." diff --git a/rma/i18n/rma.pot b/rma/i18n/rma.pot new file mode 100644 index 00000000..7b327b2b --- /dev/null +++ b/rma/i18n/rma.pot @@ -0,0 +1,1485 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * rma +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: rma +#: model:mail.template,report_name:rma.mail_template_rma_notification +msgid "${(object.name or '')}" +msgstr "" + +#. module: rma +#: model:mail.template,subject:rma.mail_template_rma_notification +msgid "${object.company_id.name} RMA (Ref ${object.name or 'n/a' })" +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma_team.py:43 +#, python-format +msgid "%s (copy)" +msgstr "" + +#. module: rma +#: model:mail.template,body_html:rma.mail_template_rma_notification +msgid "
\n" +"

\n" +" Dear ${object.partner_id.name}\n" +" % if object.partner_id.parent_id:\n" +" (${object.partner_id.parent_id.name})\n" +" % endif\n" +"

\n" +" Here is the RMA ${object.name} from ${object.company_id.name}.\n" +"

\n" +" Do not hesitate to contact us if you have any question.\n" +"

\n" +"
\n" +" " +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid " Paid" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid " Waiting Payment" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid " Cancelled" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid " Preparation" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid " Shipped" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid " Partially Available" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Delivery" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Reception" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Refund" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Customer:" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Date:" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Deadline:" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Deadline" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Delivered qty:" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Delivered quantity" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Invoicing address:" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Move:" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Origin delivery:" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Origin delivery" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Origin:" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Origin" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Product:" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Product" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Quantity:" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Quantity" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "RMA Date" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "RMA Note:" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "Responsible:" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "State:" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_defaults +msgid "A Python dictionary that will be evaluated to provide default values when creating new records for this alias." +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_team_view_form +msgid "Accept Emails From" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__access_warning +msgid "Access warning" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_needaction +#: model:ir.model.fields,field_description:rma.field_rma_team__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__active +msgid "Active" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__activity_ids +msgid "Activities" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__activity_state +msgid "Activity State" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_id +msgid "Alias" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_contact +msgid "Alias Contact Security" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_name +msgid "Alias Name" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_domain +msgid "Alias domain" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_model_id +msgid "Aliased Model" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Are you sure you want to cancel this RMA" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_attachment_count +#: model:ir.model.fields,field_description:rma.field_rma_team__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_team_view_form +msgid "Avatar" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__can_be_locked +msgid "Can Be Locked" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__can_be_refunded +msgid "Can Be Refunded" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__can_be_replaced +msgid "Can Be Replaced" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__can_be_returned +msgid "Can Be Returned" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__can_be_split +msgid "Can Be Split" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_redelivery_wizard_view_form +#: model_terms:ir.ui.view,arch_db:rma.rma_split_wizard_view_form2 +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Cancel" +msgstr "" + +#. module: rma +#: selection:rma,state:0 +msgid "Canceled" +msgstr "" + +#. module: rma +#: model_terms:ir.actions.act_window,help:rma.rma_action +#: model_terms:ir.actions.act_window,help:rma.rma_team_action +msgid "Click to add a new RMA." +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__commercial_partner_id +msgid "Commercial Entity" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Communication" +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_res_company +msgid "Companies" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__company_id +#: model:ir.model.fields,field_description:rma.field_rma_team__company_id +msgid "Company" +msgstr "" + +#. module: rma +#: model:ir.ui.menu,name:rma.rma_configuration_menu +msgid "Configuration" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Confirm" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +#: selection:rma,state:0 +msgid "Confirmed" +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_res_partner +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Contact" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_return_picking__create_rma +msgid "Create RMAs" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__create_uid +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__create_uid +#: model:ir.model.fields,field_description:rma.field_rma_operation__create_uid +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__create_uid +#: model:ir.model.fields,field_description:rma.field_rma_team__create_uid +msgid "Created by" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__create_date +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__create_date +#: model:ir.model.fields,field_description:rma.field_rma_operation__create_date +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__create_date +#: model:ir.model.fields,field_description:rma.field_rma_team__create_date +msgid "Created on" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__partner_id +msgid "Customer" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__access_url +msgid "Customer Portal URL" +msgstr "" + +#. module: rma +#: code:addons/rma/controllers/main.py:38 +#: model:ir.model.fields,field_description:rma.field_rma__date +#: model_terms:ir.ui.view,arch_db:rma.portal_my_rmas +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +#, python-format +msgid "Date" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Date:" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__deadline +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +msgid "Deadline" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_defaults +msgid "Default Values" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_redelivery_wizard_view_form +msgid "Deliver" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__delivered_qty +msgid "Delivered qty" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__delivered_qty_done +msgid "Delivered qty done" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Delivery" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__delivery_picking_count +msgid "Delivery count" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__delivery_move_ids +msgid "Delivery reservation" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__description +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "Description" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__display_name +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__display_name +#: model:ir.model.fields,field_description:rma.field_rma_operation__display_name +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__display_name +#: model:ir.model.fields,field_description:rma.field_rma_team__display_name +msgid "Display Name" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +#: selection:rma,state:0 +msgid "Draft" +msgstr "" + +#. module: rma +#: model:mail.message.subtype,name:rma.mt_rma_draft +#: model:mail.message.subtype,name:rma.mt_rma_team_rma_draft +msgid "Draft RMA" +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:1076 +#, python-format +msgid "E-mail subject: %s\n" +"\n" +"E-mail body:\n" +"%s" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_team_view_form +msgid "Email" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_team_view_form +msgid "Email Alias" +msgstr "" + +#. module: rma +#: code:addons/rma/wizard/rma_split.py:63 +#, python-format +msgid "Extracted RMA" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__origin_split_rma_id +msgid "Extracted from" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_follower_ids +#: model:ir.model.fields,field_description:rma.field_rma_team__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_channel_ids +#: model:ir.model.fields,field_description:rma.field_rma_team__message_channel_ids +msgid "Followers (Channels)" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_partner_ids +#: model:ir.model.fields,field_description:rma.field_rma_team__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +msgid "Group By" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__id +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__id +#: model:ir.model.fields,field_description:rma.field_rma_operation__id +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__id +#: model:ir.model.fields,field_description:rma.field_rma_team__id +msgid "ID" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_parent_thread_id +msgid "ID of the parent record holding the alias (example: project holding the task creation alias)" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__message_unread +#: model:ir.model.fields,help:rma.field_rma_team__message_unread +msgid "If checked new messages require your attention." +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__message_needaction +#: model:ir.model.fields,help:rma.field_rma_team__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__message_has_error +#: model:ir.model.fields,help:rma.field_rma_team__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__active +msgid "If the active field is set to false, it will allow you to hide the RMA Team without removing it." +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:1080 +#, python-format +msgid "Incoming e-mail" +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_account_invoice +msgid "Invoice" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__partner_invoice_id +msgid "Invoice Address" +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_account_invoice_line +msgid "Invoice Line" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_is_follower +#: model:ir.model.fields,field_description:rma.field_rma_team__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma____last_update +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard____last_update +#: model:ir.model.fields,field_description:rma.field_rma_operation____last_update +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard____last_update +#: model:ir.model.fields,field_description:rma.field_rma_team____last_update +msgid "Last Modified on" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__write_uid +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__write_uid +#: model:ir.model.fields,field_description:rma.field_rma_operation__write_uid +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__write_uid +#: model:ir.model.fields,field_description:rma.field_rma_team__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__write_date +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__write_date +#: model:ir.model.fields,field_description:rma.field_rma_operation__write_date +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__write_date +#: model:ir.model.fields,field_description:rma.field_rma_team__write_date +msgid "Last Updated on" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__location_id +msgid "Location" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Lock" +msgstr "" + +#. module: rma +#: selection:rma,state:0 +msgid "Locked" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_main_attachment_id +#: model:ir.model.fields,field_description:rma.field_rma_team__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: rma +#: model:ir.module.category,description:rma.rma_module_category +msgid "Manage Return Merchandise Authorizations (RMAs)." +msgstr "" + +#. module: rma +#: model:res.groups,name:rma.rma_group_manager +msgid "Manager" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_has_error +#: model:ir.model.fields,field_description:rma.field_rma_team__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_ids +#: model:ir.model.fields,field_description:rma.field_rma_team__message_ids +msgid "Messages" +msgstr "" + +#. module: rma +#: code:addons/rma/controllers/main.py:39 +#: model:ir.model.fields,field_description:rma.field_rma__name +#: model:ir.model.fields,field_description:rma.field_rma_operation__name +#: model:ir.model.fields,field_description:rma.field_rma_team__name +#, python-format +msgid "Name" +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:31 +#: code:addons/rma/models/rma.py:496 +#: code:addons/rma/models/rma.py:1079 +#, python-format +msgid "New" +msgstr "" + +#. module: rma +#: model:mail.message.subtype,description:rma.mt_rma_draft +msgid "New RMA in draft state" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:757 +#, python-format +msgid "None of the selected RMAs can perform a replacement." +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:740 +#, python-format +msgid "None of the selected RMAs can perform a return." +msgstr "" + +#. module: rma +#: selection:rma,priority:0 +msgid "Normal" +msgstr "" + +#. module: rma +#: selection:rma,priority:0 +msgid "Not urgent" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_needaction_counter +#: model:ir.model.fields,field_description:rma.field_rma_team__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_has_error_counter +#: model:ir.model.fields,field_description:rma.field_rma_team__message_has_error_counter +msgid "Number of error" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__message_needaction_counter +#: model:ir.model.fields,help:rma.field_rma_team__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__message_has_error_counter +#: model:ir.model.fields,help:rma.field_rma_team__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__message_unread_counter +#: model:ir.model.fields,help:rma.field_rma_team__message_unread_counter +msgid "Number of unread messages" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_force_thread_id +msgid "Optional ID of a thread (record) to which all incoming messages will be attached, even if they did not reply to it. If set, this will disable the creation of new records completely." +msgstr "" + +#. module: rma +#: model:ir.ui.menu,name:rma.rma_orders_menu +msgid "Orders" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__picking_id +msgid "Origin Delivery" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__move_id +msgid "Origin move" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Other Information" +msgstr "" + +#. module: rma +#: selection:rma,activity_state:0 +msgid "Overdue" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_user_id +msgid "Owner" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_parent_model_id +msgid "Parent Model" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_parent_thread_id +msgid "Parent Record Thread ID" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_parent_model_id +msgid "Parent model holding the alias. The model holding the alias reference is not necessarily the model given by alias_model_id (example: project (parent_model) and task (model))" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +msgid "Partner" +msgstr "" + +#. module: rma +#: selection:rma,activity_state:0 +msgid "Planned" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_contact +msgid "Policy to post a message on the document using the mailgateway.\n" +"- everyone: everyone can post\n" +"- partners: only authenticated partners\n" +"- followers: only followers of the related document or members of following channels\n" +"" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__access_url +msgid "Portal Access URL" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Preview" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__priority +msgid "Priority" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__procurement_group_id +msgid "Procurement group" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__product_id +#: model_terms:ir.ui.view,arch_db:rma.portal_my_rmas +msgid "Product" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__product_uom_qty +msgid "Product qty" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__product_uom_qty +#: model_terms:ir.ui.view,arch_db:rma.portal_my_rmas +msgid "Quantity" +msgstr "" + +#. module: rma +#: code:addons/rma/wizard/rma_delivery.py:49 +#: sql_constraint:rma.split.wizard:0 +#, python-format +msgid "Quantity must be greater than 0." +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__product_uom_qty +msgid "Quantity to extract" +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:790 +#, python-format +msgid "Quantity to extract cannot be greater than remaining delivery quantity (%s %s)" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_split_wizard__product_uom_qty +msgid "Quantity to extract to a new RMA." +msgstr "" + +#. module: rma +#: model:ir.actions.act_window,name:rma.rma_action +#: model:ir.model,name:rma.model_rma +#: model:ir.model.fields,field_description:rma.field_account_invoice_line__rma_id +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__rma_id +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma +#: model:ir.module.category,name:rma.rma_module_category +#: model:ir.ui.menu,name:rma.rma_menu +#: model_terms:ir.ui.view,arch_db:rma.view_partner_form +#: model_terms:ir.ui.view,arch_db:rma.view_picking_form +msgid "RMA" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_my_rmas +#: model_terms:ir.ui.view,arch_db:rma.report_rma_document +msgid "RMA #" +msgstr "" + +#. module: rma +#: code:addons/rma/models/res_company.py:18 +#, python-format +msgid "RMA Code" +msgstr "" + +#. module: rma +#: code:addons/rma/models/stock_warehouse.py:82 +#, python-format +msgid "RMA Delivery Orders" +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_rma_delivery_wizard +msgid "RMA Delivery Wizard" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_in_type_id +msgid "RMA In Type" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_loc_id +msgid "RMA Location" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_rma_page +msgid "RMA Order -" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.portal_my_home_menu_rma +#: model_terms:ir.ui.view,arch_db:rma.portal_my_home_rma +#: model_terms:ir.ui.view,arch_db:rma.portal_my_rmas +msgid "RMA Orders" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_warehouse__rma_out_type_id +msgid "RMA Out Type" +msgstr "" + +#. module: rma +#: code:addons/rma/models/stock_warehouse.py:73 +#, python-format +msgid "RMA Receipts" +msgstr "" + +#. module: rma +#: model:ir.actions.report,name:rma.report_rma_action +msgid "RMA Report" +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_rma_split_wizard +msgid "RMA Split Wizard" +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_rma_team +#: model:ir.model.fields,field_description:rma.field_res_users__rma_team_id +#: model:ir.ui.menu,name:rma.rma_configuration_rma_team_menu +#: model_terms:ir.ui.view,arch_db:rma.rma_team_view_form +msgid "RMA Team" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_res_users__rma_team_id +msgid "RMA Team the user is member of." +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_res_partner__rma_count +#: model:ir.model.fields,field_description:rma.field_res_users__rma_count +#: model:ir.model.fields,field_description:rma.field_stock_picking__rma_count +msgid "RMA count" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_move__rma_receiver_ids +msgid "RMA receivers" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_stock_warehouse__rma +msgid "RMA related products can be stored in this warehouse." +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_rma_operation +msgid "RMA requested operation" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_move__rma_id +msgid "RMA return" +msgstr "" + +#. module: rma +#: model:ir.actions.act_window,name:rma.rma_team_action +#: model:ir.model.fields,field_description:rma.field_rma__team_id +msgid "RMA team" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_res_partner__rma_ids +#: model:ir.model.fields,field_description:rma.field_res_users__rma_ids +#: model:ir.model.fields,field_description:rma.field_stock_move__rma_ids +msgid "RMAs" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Receipt" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +#: selection:rma,state:0 +msgid "Received" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__reception_move_id +msgid "Reception move" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__alias_force_thread_id +msgid "Record Thread ID" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__origin +msgid "Reference of the document that generated this RMA." +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:679 +#: model:ir.model.fields,field_description:rma.field_rma__refund_id +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +#: model:rma.operation,name:rma.rma_operation_refund +#, python-format +msgid "Refund" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__partner_invoice_id +msgid "Refund address for current RMA." +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__refund_line_id +msgid "Refund line" +msgstr "" + +#. module: rma +#: selection:rma,state:0 +msgid "Refunded" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__remaining_qty +msgid "Remaining delivered qty" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__remaining_qty_to_done +msgid "Remaining delivered qty to done" +msgstr "" + +#. module: rma +#: model:rma.operation,name:rma.rma_operation_return +msgid "Repair" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +#: selection:rma.delivery.wizard,type:0 +#: model:rma.operation,name:rma.rma_operation_replace +msgid "Replace" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__product_id +msgid "Replace Product" +msgstr "" + +#. module: rma +#: selection:rma,state:0 +msgid "Replaced" +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:995 +#, python-format +msgid "Replacement: Move
%s (Picking %s) has been created." +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:1006 +#, python-format +msgid "Replacement:
Product %s
Quantity %f %s
This replacement did not create a new move, but one of the previously created moves was updated with this data." +msgstr "" + +#. module: rma +#: model:ir.ui.menu,name:rma.rma_reporting_menu +msgid "Reporting" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__operation_id +msgid "Requested operation" +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:721 +#, python-format +msgid "Required field(s):%s" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__user_id +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +msgid "Responsible" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_stock_return_picking +msgid "Return Picking" +msgstr "" + +#. module: rma +#: model:ir.actions.act_window,name:rma.rma_delivery_wizard_action +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +#: selection:rma.delivery.wizard,type:0 +msgid "Return to customer" +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:956 +#, python-format +msgid "Return: %s has been created." +msgstr "" + +#. module: rma +#: selection:rma,state:0 +msgid "Returned" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__rma_count +msgid "Rma Count" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__scheduled_date +msgid "Scheduled Date" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__access_token +msgid "Security Token" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Send by Email" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Send by Mail" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__sent +msgid "Sent" +msgstr "" + +#. module: rma +#: code:addons/rma/models/stock_warehouse.py:48 +#, python-format +msgid "Sequence RMA in" +msgstr "" + +#. module: rma +#: code:addons/rma/models/stock_warehouse.py:53 +#, python-format +msgid "Sequence RMA out" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Set to draft" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Share" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__origin +msgid "Source Document" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_split_wizard_view_form2 +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Split" +msgstr "" + +#. module: rma +#: model:ir.actions.act_window,name:rma.rma_split_wizard_action +msgid "Split RMA" +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:874 +#, python-format +msgid "Split: %s has been created." +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__state +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +msgid "State" +msgstr "" + +#. module: rma +#: code:addons/rma/controllers/main.py:40 +#: model_terms:ir.ui.view,arch_db:rma.portal_my_rmas +#, python-format +msgid "Status" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__activity_state +msgid "Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_stock_rule +msgid "Stock Rule" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__user_id +msgid "Team Leader" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_team__member_ids +#: model_terms:ir.ui.view,arch_db:rma.rma_team_view_form +msgid "Team Members" +msgstr "" + +#. module: rma +#: sql_constraint:rma.operation:0 +msgid "That operation name already exists !" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_model_id +msgid "The model (Odoo Document Kind) to which this alias corresponds. Any incoming email that does not reply to an existing record will cause the creation of a new record of this model (e.g. a Project Task)" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_name +msgid "The name of the email alias, e.g. 'jobs' if you want to catch emails for " +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma_team__alias_user_id +msgid "The owner of records created upon receiving emails on this alias. If this field is not set the system will attempt to find the right owner based on the sender (From) address, or will use the Administrator account if no system user is found for that address." +msgstr "" + +#. module: rma +#: code:addons/rma/models/stock_move.py:60 +#, python-format +msgid "The quantity done for the product '%s' must be equal to its initial demand because the stock move is linked to an RMA (%s)." +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:778 +#, python-format +msgid "The quantity to return is greater than remaining quantity." +msgstr "" + +#. module: rma +#: code:addons/rma/models/account_invoice.py:22 +#, python-format +msgid "There is at least one invoice lines whose quantity is less than the quantity specified in its linked RMA." +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:767 +#, python-format +msgid "This RMA cannot be split." +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:754 +#, python-format +msgid "This RMA cannot perform a replacement." +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:737 +#, python-format +msgid "This RMA cannot perform a return." +msgstr "" + +#. module: rma +#: model:ir.actions.server,name:rma.rma_refund_action_server +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "To Refund" +msgstr "" + +#. module: rma +#: selection:rma,activity_state:0 +msgid "Today" +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__type +msgid "Type" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_stock_return_picking__picking_type_code +msgid "Type of Operation" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_search +msgid "Unassigned RMAs" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__product_uom +#: model:ir.model.fields,field_description:rma.field_rma_split_wizard__product_uom +msgid "Unit of measure" +msgstr "" + +#. module: rma +#: model_terms:ir.ui.view,arch_db:rma.rma_view_form +msgid "Unlock" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_unread +#: model:ir.model.fields,field_description:rma.field_rma_team__message_unread +msgid "Unread Messages" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__message_unread_counter +#: model:ir.model.fields,field_description:rma.field_rma_team__message_unread_counter +msgid "Unread Messages Counter" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__product_uom +msgid "UoM" +msgstr "" + +#. module: rma +#: selection:rma,priority:0 +msgid "Urgent" +msgstr "" + +#. module: rma +#: model:res.groups,name:rma.rma_group_user_all +msgid "User: All Documents" +msgstr "" + +#. module: rma +#: model:res.groups,name:rma.rma_group_user_own +msgid "User: Own Documents Only" +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_res_users +msgid "Users" +msgstr "" + +#. module: rma +#: selection:rma,priority:0 +msgid "Very Urgent" +msgstr "" + +#. module: rma +#: selection:rma,state:0 +msgid "Waiting for refund" +msgstr "" + +#. module: rma +#: selection:rma,state:0 +msgid "Waiting for replacement" +msgstr "" + +#. module: rma +#: selection:rma,state:0 +msgid "Waiting for return" +msgstr "" + +#. module: rma +#: model:ir.model,name:rma.model_stock_warehouse +#: model:ir.model.fields,field_description:rma.field_rma__warehouse_id +#: model:ir.model.fields,field_description:rma.field_rma_delivery_wizard__warehouse_id +msgid "Warehouse" +msgstr "" + +#. module: rma +#: model:ir.model.fields,field_description:rma.field_rma__website_message_ids +#: model:ir.model.fields,field_description:rma.field_rma_team__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: rma +#: model:ir.model.fields,help:rma.field_rma__website_message_ids +#: model:ir.model.fields,help:rma.field_rma_team__website_message_ids +msgid "Website communication history" +msgstr "" + +#. module: rma +#: code:addons/rma/models/rma.py:514 +#, python-format +msgid "You cannot delete RMAs that are not in draft state" +msgstr "" + +#. module: rma +#: code:addons/rma/wizard/stock_picking_return.py:56 +#, python-format +msgid "You must specify the 'Customer' in the 'Stock Picking' from which RMAs will be created" +msgstr "" + +#. module: rma +#: model:res.groups,comment:rma.rma_group_user_all +msgid "the user will have access to all records of everyone in the RMA application." +msgstr "" + +#. module: rma +#: model:res.groups,comment:rma.rma_group_user_own +msgid "the user will have access to his own data in the RMA application." +msgstr "" + +#. module: rma +#: model:res.groups,comment:rma.rma_group_manager +msgid "the user will have an access to the RMA configuration as well as statistic reports." +msgstr "" + diff --git a/rma/models/__init__.py b/rma/models/__init__.py new file mode 100644 index 00000000..134da114 --- /dev/null +++ b/rma/models/__init__.py @@ -0,0 +1,12 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import account_invoice +from . import rma +from . import rma_operation +from . import rma_team +from . import res_company +from . import res_partner +from . import res_users +from . import stock_move +from . import stock_picking +from . import stock_warehouse diff --git a/rma/models/account_invoice.py b/rma/models/account_invoice.py new file mode 100644 index 00000000..731891cf --- /dev/null +++ b/rma/models/account_invoice.py @@ -0,0 +1,35 @@ +# 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 AccountInvoice(models.Model): + _inherit = 'account.invoice' + + def action_invoice_open(self): + """ Avoids to validate a refund with less quantity of product than + quantity in the linked RMA. + """ + precision = self.env['decimal.precision'].precision_get( + 'Product Unit of Measure') + if self.mapped('invoice_line_ids').filtered( + lambda r: (r.rma_id and float_compare( + r.quantity, r.rma_id.product_uom_qty, precision) < 0)): + raise ValidationError( + _("There is at least one invoice lines whose quantity is " + "less than the quantity specified in its linked RMA.")) + res = super().action_invoice_open() + self.mapped('invoice_line_ids.rma_id').write({'state': 'refunded'}) + return res + + +class AccountInvoiceLine(models.Model): + _inherit = 'account.invoice.line' + + rma_id = fields.Many2one( + comodel_name='rma', + string='RMA', + ) diff --git a/rma/models/res_company.py b/rma/models/res_company.py new file mode 100644 index 00000000..555f09bd --- /dev/null +++ b/rma/models/res_company.py @@ -0,0 +1,23 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models, _ + + +class Company(models.Model): + _inherit = "res.company" + + @api.model + def create(self, vals): + company = super(Company, self).create(vals) + company.create_rma_index() + return company + + def create_rma_index(self): + self.env['ir.sequence'].sudo().create({ + 'name': _('RMA Code'), + 'prefix': 'RMA', + 'code': 'rma', + 'padding': 4, + 'company_id': self.id, + }) diff --git a/rma/models/res_partner.py b/rma/models/res_partner.py new file mode 100644 index 00000000..494c267a --- /dev/null +++ b/rma/models/res_partner.py @@ -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 = dict( + [(r['partner_id'][0], r['partner_id_count']) for r in rma_data]) + for record in self: + record.rma_count = mapped_data.get(record.id, 0) + + def action_view_rma(self): + self.ensure_one() + action = self.env.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 diff --git a/rma/models/res_users.py b/rma/models/res_users.py new file mode 100644 index 00000000..4cce3c9a --- /dev/null +++ b/rma/models/res_users.py @@ -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.', + ) diff --git a/rma/models/rma.py b/rma/models/rma.py new file mode 100644 index 00000000..8367c5b3 --- /dev/null +++ b/rma/models/rma.py @@ -0,0 +1,1144 @@ +# 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 +from odoo.tests import Form +from odoo.tools import html2plaintext +import odoo.addons.decimal_precision as dp +from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES +from collections import Counter + + +class Rma(models.Model): + _name = "rma" + _description = "RMA" + _order = "date desc, priority" + _inherit = ["mail.thread", "portal.mixin", "mail.activity.mixin"] + + def _domain_location_id(self): + rma_loc = self.env['stock.warehouse'].search([]).mapped('rma_loc_id') + return [('id', 'child_of', rma_loc.ids)] + + # General fields + sent = fields.Boolean() + name = fields.Char( + string='Name', + index=True, + readonly=True, + states={'draft': [('readonly', False)]}, + copy=False, + default=lambda self: _('New'), + ) + origin = fields.Char( + string='Source Document', + states={ + 'locked': [('readonly', True)], + 'cancelled': [('readonly', True)], + }, + help="Reference of the document that generated this RMA.", + ) + date = fields.Datetime( + default=lambda self: fields.Datetime.now(), + index=True, + required=True, + readonly=True, + states={'draft': [('readonly', False)]}, + ) + deadline = fields.Date( + states={ + 'locked': [('readonly', True)], + 'cancelled': [('readonly', True)], + }, + ) + user_id = fields.Many2one( + comodel_name="res.users", + string="Responsible", + track_visibility="always", + default=lambda self: self.env.user, + states={ + 'locked': [('readonly', True)], + 'cancelled': [('readonly', True)], + }, + ) + team_id = fields.Many2one( + comodel_name="rma.team", + string="RMA team", + index=True, + states={ + 'locked': [('readonly', True)], + 'cancelled': [('readonly', True)], + }, + ) + company_id = fields.Many2one( + comodel_name="res.company", + default=lambda self: self.env.user.company_id, + states={ + 'locked': [('readonly', True)], + 'cancelled': [('readonly', True)], + }, + ) + partner_id = fields.Many2one( + string="Customer", + comodel_name="res.partner", + readonly=True, + states={'draft': [('readonly', False)]}, + index=True, + track_visibility='always' + ) + partner_invoice_id = fields.Many2one( + string="Invoice Address", + comodel_name="res.partner", + readonly=True, + states={'draft': [('readonly', False)]}, + domain=[('customer', '=', True)], + help="Refund address for current RMA." + ) + commercial_partner_id = fields.Many2one( + comodel_name="res.partner", + related="partner_id.commercial_partner_id", + ) + picking_id = fields.Many2one( + comodel_name="stock.picking", + string="Origin Delivery", + domain="[" + " ('state', '=', 'done')," + " ('picking_type_id.code', '=', 'outgoing')," + " ('partner_id', 'child_of', commercial_partner_id)," + "]", + readonly=True, + states={'draft': [('readonly', False)]}, + ) + move_id = fields.Many2one( + comodel_name='stock.move', + string='Origin move', + domain="[" + " ('picking_id', '=', picking_id)," + " ('picking_id', '!=', False)" + "]", + readonly=True, + states={'draft': [('readonly', False)]}, + ) + product_id = fields.Many2one( + comodel_name="product.product", + domain=[('type', 'in', ['consu', 'product'])], + ) + product_uom_qty = fields.Float( + string="Quantity", + required=True, + default=1.0, + digits=dp.get_precision('Product Unit of Measure'), + readonly=True, + states={'draft': [('readonly', False)]}, + ) + product_uom = fields.Many2one( + comodel_name="uom.uom", + string="UoM", + required=True, + readonly=True, + states={'draft': [('readonly', False)]}, + default=lambda self: self.env.ref('uom.product_uom_unit').id, + ) + procurement_group_id = fields.Many2one( + comodel_name='procurement.group', + string='Procurement group', + readonly=True, + states={ + 'draft': [('readonly', False)], + 'confirmed': [('readonly', False)], + 'received': [('readonly', False)], + }, + ) + priority = fields.Selection( + string="Priority", + selection=PROCUREMENT_PRIORITIES, + default="1", + readonly=True, + states={'draft': [('readonly', False)]}, + ) + operation_id = fields.Many2one( + comodel_name='rma.operation', + string='Requested operation', + ) + state = fields.Selection( + [ + ("draft", "Draft"), + ("confirmed", "Confirmed"), + ("received", "Received"), + ("waiting_refund", "Waiting for refund"), + ("waiting_return", "Waiting for return"), + ("waiting_replacement", "Waiting for replacement"), + ("refunded", "Refunded"), + ("returned", "Returned"), + ("replaced", "Replaced"), + ("locked", "Locked"), + ("cancelled", "Canceled"), + ], + default="draft", + copy=False, + track_visibility="onchange", + ) + description = fields.Text( + states={ + 'locked': [('readonly', True)], + 'cancelled': [('readonly', True)], + }, + ) + # Reception fields + location_id = fields.Many2one( + comodel_name='stock.location', + domain=_domain_location_id, + readonly=True, + states={'draft': [('readonly', False)]}, + ) + warehouse_id = fields.Many2one( + comodel_name='stock.warehouse', + compute="_compute_warehouse_id", + store=True, + ) + reception_move_id = fields.Many2one( + comodel_name='stock.move', + string='Reception move', + copy=False, + ) + # Refund fields + refund_id = fields.Many2one( + comodel_name='account.invoice', + string='Refund', + readonly=True, + copy=False, + ) + refund_line_id = fields.Many2one( + comodel_name='account.invoice.line', + string='Refund line', + readonly=True, + copy=False, + ) + can_be_refunded = fields.Boolean( + compute="_compute_can_be_refunded" + ) + # Delivery fields + delivery_move_ids = fields.One2many( + comodel_name='stock.move', + inverse_name='rma_id', + string='Delivery reservation', + readonly=True, + copy=False, + ) + delivery_picking_count = fields.Integer( + string='Delivery count', + compute='_compute_delivery_picking_count', + ) + delivered_qty = fields.Float( + string="Delivered qty", + digits=dp.get_precision('Product Unit of Measure'), + compute='_compute_delivered_qty', + store=True, + ) + delivered_qty_done = fields.Float( + string="Delivered qty done", + digits=dp.get_precision('Product Unit of Measure'), + compute='_compute_delivered_qty', + ) + can_be_returned = fields.Boolean( + compute="_compute_can_be_returned", + ) + can_be_replaced = fields.Boolean( + compute="_compute_can_be_replaced", + ) + can_be_locked = fields.Boolean( + compute="_compute_can_be_locked", + ) + remaining_qty = fields.Float( + string="Remaining delivered qty", + digits=dp.get_precision('Product Unit of Measure'), + compute='_compute_remaining_qty', + ) + remaining_qty_to_done = fields.Float( + string="Remaining delivered qty to done", + digits=dp.get_precision('Product Unit of Measure'), + compute='_compute_remaining_qty', + ) + # Split fields + can_be_split = fields.Boolean( + compute="_compute_can_be_split", + ) + origin_split_rma_id = fields.Many2one( + comodel_name='rma', + string='Extracted from', + readonly=True, + copy=False, + ) + + def _compute_delivery_picking_count(self): + # It is enough to count the moves to know how many pickings + # there are because there will be a unique move linked to the + # same picking and the same rma. + rma_data = self.env['stock.move'].read_group( + [('rma_id', 'in', self.ids)], + ['rma_id', 'picking_id'], + ['rma_id', 'picking_id'], + lazy=False, + ) + mapped_data = Counter(map(lambda r: r['rma_id'][0], rma_data)) + for record in self: + record.delivery_picking_count = mapped_data.get(record.id, 0) + + @api.depends( + 'delivery_move_ids', + 'delivery_move_ids.state', + 'delivery_move_ids.scrapped', + 'delivery_move_ids.product_uom_qty', + 'delivery_move_ids.reserved_availability', + 'delivery_move_ids.quantity_done', + 'delivery_move_ids.product_uom', + 'product_uom', + ) + def _compute_delivered_qty(self): + """ Compute 'delivered_qty' and 'delivered_qty_done' fields. + + delivered_qty: represents the quantity delivery or to be + delivery. For each move in delivery_move_ids the quantity done + is taken, if it is empty the reserved quantity is taken, + otherwise the initial demand is taken. + + delivered_qty_done: represents the quantity delivered and done. + For each 'done' move in delivery_move_ids the quantity done is + taken. This field is used to control when the RMA cam be set + to 'delivered' state. + """ + for record in self: + delivered_qty = 0.0 + delivered_qty_done = 0.0 + for move in record.delivery_move_ids.filtered( + lambda r: r.state != 'cancel' and not r.scrapped): + if move.quantity_done: + quantity_done = move.product_uom._compute_quantity( + move.quantity_done, record.product_uom) + if move.state == 'done': + delivered_qty_done += quantity_done + delivered_qty += quantity_done + elif move.reserved_availability: + delivered_qty += move.product_uom._compute_quantity( + move.reserved_availability, record.product_uom) + elif move.product_uom_qty: + delivered_qty += move.product_uom._compute_quantity( + move.product_uom_qty, record.product_uom) + record.delivered_qty = delivered_qty + record.delivered_qty_done = delivered_qty_done + + @api.depends('product_uom_qty', 'delivered_qty', 'delivered_qty_done') + def _compute_remaining_qty(self): + """ Compute 'remaining_qty' and 'remaining_qty_to_done' fields. + + remaining_qty: is used to set a default quantity of replacing + or returning of product to the customer. + + remaining_qty_to_done: the aim of this field to control when the + RMA cam be set to 'delivered' state. An RMA with + remaining_qty_to_done <= 0 can be set to 'delivery'. It is used + in stock.move._action_done method of stock.move and + rma.extract_quantity. + """ + for r in self: + r.remaining_qty = r.product_uom_qty - r.delivered_qty + r.remaining_qty_to_done = r.product_uom_qty - r.delivered_qty_done + + @api.depends( + 'state', + ) + def _compute_can_be_refunded(self): + """ Compute 'can_be_refunded'. This field controls the visibility + of 'Refund' button in the rma form view and determinates if + an rma can be refunded. It is used in rma.action_refund method. + """ + for record in self: + record.can_be_refunded = record.state == 'received' + + @api.depends('remaining_qty', 'state') + def _compute_can_be_returned(self): + """ Compute 'can_be_returned'. This field controls the visibility + of the 'Return to customer' button in the rma form + view and determinates if an rma can be returned to the customer. + This field is used in: + rma._compute_can_be_split + rma._ensure_can_be_returned. + """ + for r in self: + r.can_be_returned = (r.state in ['received', 'waiting_return'] + and r.remaining_qty > 0) + + @api.depends('state') + def _compute_can_be_replaced(self): + """ Compute 'can_be_replaced'. This field controls the visibility + of 'Replace' button in the rma form + view and determinates if an rma can be replaced. + This field is used in: + rma._compute_can_be_split + rma._ensure_can_be_replaced. + """ + for r in self: + r.can_be_replaced = r.state in ['received', 'waiting_replacement', + 'replaced'] + + @api.depends('product_uom_qty', 'state', 'remaining_qty', + 'remaining_qty_to_done') + def _compute_can_be_split(self): + """ Compute 'can_be_split'. This field controls the + visibility of 'Split' button in the rma form view and + determinates if an rma can be split. + This field is used in: + rma._ensure_can_be_split + """ + for r in self: + if (r.product_uom_qty > 1 + and ((r.state == 'waiting_return' and r.remaining_qty > 0) + or (r.state == 'waiting_replacement' + and r.remaining_qty_to_done > 0))): + r.can_be_split = True + else: + r.can_be_split = False + + @api.depends('remaining_qty_to_done', 'state') + def _compute_can_be_locked(self): + for r in self: + r.can_be_locked = (r.remaining_qty_to_done > 0 + and r.state in ['received', 'waiting_refund', + 'waiting_return', + 'waiting_replacement']) + + @api.depends('location_id') + def _compute_warehouse_id(self): + for record in self.filtered('location_id'): + record.warehouse_id = self.env['stock.warehouse'].search( + [('rma_loc_id', 'parent_of', record.location_id.id)], limit=1) + + def _compute_access_url(self): + for record in self: + record.access_url = '/my/rmas/{}'.format(record.id) + + # Constrains methods (@api.constrains) + @api.constrains('state', 'partner_id', 'partner_invoice_id', 'product_id') + def _check_required_after_draft(self): + """ Check that RMAs are being created or edited with the + necessary fields filled out. Only applies to 'Draft' and + 'Cancelled' states. + """ + rma = self.filtered(lambda r: r.state not in ['draft', 'cancelled']) + rma._ensure_required_fields() + + # onchange methods (@api.onchange) + @api.onchange("user_id") + def _onchange_user_id(self): + if self.user_id: + self.team_id = self.env['rma.team'].sudo().search([ + '|', + ('user_id', '=', self.user_id.id), + ('member_ids', '=', self.user_id.id), + '|', + ('company_id', '=', False), + ('company_id', 'child_of', self.company_id.ids) + ], limit=1) + else: + self.team_id = False + + @api.onchange("partner_id") + def _onchange_partner_id(self): + self.picking_id = False + partner_invoice_id = False + if self.partner_id: + address = self.partner_id.address_get(['invoice']) + partner_invoice_id = address.get('invoice', False) + self.partner_invoice_id = partner_invoice_id + + @api.onchange("picking_id") + def _onchange_picking_id(self): + location = False + if self.picking_id: + warehouse = self.picking_id.picking_type_id.warehouse_id + location = warehouse.rma_loc_id.id + self.location_id = location + self.move_id = False + self.product_id = False + + @api.onchange("move_id") + def _onchange_move_id(self): + if self.move_id: + self.product_id = self.move_id.product_id + self.product_uom_qty = self.move_id.product_uom_qty + self.product_uom = self.move_id.product_uom + + @api.onchange("product_id") + def _onchange_product_id(self): + domain_product_uom = [] + if self.product_id: + # Set UoM and UoM domain (product_uom) + domain_product_uom = [ + ('category_id', '=', self.product_id.uom_id.category_id.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 + # Set stock location (location_id) + user = self.env.user + if (not user.has_group('stock.group_stock_multi_locations') + and not self.location_id): + # If this condition is True, it is because a picking is not set + company = self.company_id or self.env.user.company_id + warehouse = self.env['stock.warehouse'].search( + [('company_id', '=', company.id)], limit=1) + self.location_id = warehouse.rma_loc_id.id + return {'domain': {'product_uom': domain_product_uom}} + + # CRUD methods (ORM overrides) + @api.model + def create(self, vals): + if vals.get('name', _('New')) == _('New'): + ir_sequence = self.env['ir.sequence'] + if 'company_id' in vals: + ir_sequence = ir_sequence.with_context( + force_company=vals['company_id']) + vals['name'] = ir_sequence.next_by_code('rma') + return super().create(vals) + + def copy(self, default=None): + 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 unlink(self): + if self.filtered(lambda r: r.state != 'draft'): + raise ValidationError( + _("You cannot delete RMAs that are not in draft state")) + return super().unlink() + + # Action methods + def action_rma_send(self): + self.ensure_one() + template = self.env.ref('rma.mail_template_rma_notification', False) + form = self.env.ref('mail.email_compose_message_wizard_form', False) + ctx = { + 'default_model': 'rma', + 'default_res_id': self.ids[0], + 'default_use_template': bool(template), + 'default_template_id': template and template.id or False, + 'default_composition_mode': 'comment', + 'mark_rma_as_sent': True, + 'model_description': 'RMA', + 'force_email': True + } + return { + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'mail.compose.message', + 'views': [(form.id, 'form')], + 'view_id': form.id, + 'target': 'new', + 'context': ctx, + } + + def action_confirm(self): + """Invoked when 'Confirm' button in rma form view is clicked.""" + self.ensure_one() + self._ensure_required_fields() + if self.state == 'draft': + if self.picking_id: + reception_move = self._create_receptions_from_picking() + else: + reception_move = self._create_receptions_from_product() + self.write({ + 'reception_move_id': reception_move.id, + 'state': 'confirmed', + }) + if self.partner_id not in self.message_partner_ids: + self.message_subscribe([self.partner_id.id]) + + def action_refund(self): + """Invoked when 'Refund' button in rma form view is clicked + and 'rma_refund_action_server' server action is run. + """ + group_dict = {} + for record in self.filtered("can_be_refunded"): + key = (record.partner_invoice_id.id, record.company_id.id) + group_dict.setdefault(key, self.env['rma']) + group_dict[key] |= record + for rmas in group_dict.values(): + origin = ', '.join(rmas.mapped('name')) + invoice_form = Form(self.env['account.invoice'].with_context( + default_type='out_refund', + company_id=rmas[0].company_id.id, + )) + rmas[0]._prepare_refund(invoice_form, origin) + refund = invoice_form.save() + for rma in rmas: + with invoice_form.invoice_line_ids.new() as line_form: + rma._prepare_refund_line(line_form) + # rma_id is not present in the form view, so we need to get + # the 'values to save' to add the rma id and use the + # create method instead of save the form. We also need + # the new refund line id to be linked to the rma. + refund_vals = invoice_form._values_to_save(all_fields=True) + line_vals = refund_vals['invoice_line_ids'][-1][2] + line_vals.update(invoice_id=refund.id, rma_id=rma.id) + line = self.env['account.invoice.line'].create(line_vals) + rma.write({ + 'refund_line_id': line.id, + 'refund_id': refund.id, + 'state': 'waiting_refund', + }) + refund.message_post_with_view( + 'mail.message_origin_link', + values={'self': refund, 'origin': rmas}, + subtype_id=self.env.ref('mail.mt_note').id, + ) + + def action_replace(self): + """Invoked when 'Replace' button in rma form view is clicked.""" + self.ensure_one() + self._ensure_can_be_replaced() + action = self.env.ref("rma.rma_delivery_wizard_action").read()[0] + action['name'] = 'Replace product(s)' + action['context'] = dict(self.env.context) + action['context'].update( + active_id=self.id, + active_ids=self.ids, + rma_delivery_type='replace', + ) + return action + + def action_return(self): + """Invoked when 'Return to customer' button in rma form + view is clicked. + """ + self.ensure_one() + self._ensure_can_be_returned() + action = self.env.ref("rma.rma_delivery_wizard_action").read()[0] + action['context'] = dict(self.env.context) + action['context'].update( + active_id=self.id, + active_ids=self.ids, + rma_delivery_type='return', + ) + return action + + def action_split(self): + """Invoked when 'Split' button in rma form view is clicked.""" + self.ensure_one() + self._ensure_can_be_split() + action = self.env.ref("rma.rma_split_wizard_action").read()[0] + action['context'] = dict(self.env.context) + action['context'].update(active_ids=self.ids) + return action + + def action_cancel(self): + """Invoked when 'Cancel' button in rma form view is clicked.""" + self.mapped('reception_move_id')._action_cancel() + self.write({'state': 'cancelled'}) + + def action_draft(self): + cancelled_rma = self.filtered(lambda r: r.state == 'cancelled') + cancelled_rma.write({'state': 'draft'}) + + def action_lock(self): + """Invoked when 'Lock' button in rma form view is clicked.""" + self.filtered("can_be_locked").write({'state': 'locked'}) + + def action_unlock(self): + """Invoked when 'Unlock' button in rma form view is clicked.""" + locked_rma = self.filtered(lambda r: r.state == 'locked') + locked_rma.write({'state': 'received'}) + + def action_preview(self): + """Invoked when 'Preview' button in rma form view is clicked.""" + self.ensure_one() + return { + 'type': 'ir.actions.act_url', + 'target': 'self', + 'url': self.get_portal_url(), + } + + def action_view_receipt(self): + """Invoked when 'Receipt' smart button in rma form view is clicked.""" + self.ensure_one() + action = self.env.ref('stock.action_picking_tree_all').read()[0] + action.update( + res_id=self.reception_move_id.picking_id.id, + view_mode="form", + view_id=False, + views=False, + ) + return action + + def action_view_refund(self): + """Invoked when 'Refund' smart button in rma form view is clicked.""" + self.ensure_one() + return { + 'name': _('Refund'), + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'account.invoice', + 'views': [(self.env.ref('account.invoice_form').id, 'form')], + 'res_id': self.refund_id.id, + } + + def action_view_delivery(self): + """Invoked when 'Delivery' smart button in rma form view is clicked.""" + action = self.env.ref('stock.action_picking_tree_all').read()[0] + picking = self.delivery_move_ids.mapped('picking_id') + if len(picking) > 1: + action['domain'] = [('id', 'in', picking.ids)] + elif picking: + action.update( + res_id=picking.id, + view_mode="form", + view_id=False, + views=False, + ) + return action + + # Validation business methods + def _ensure_required_fields(self): + """ This method is used to ensure the following fields are not empty: + ['partner_id', 'partner_invoice_id', 'product_id', 'location_id'] + + This method is intended to be called on confirm RMA action and is + invoked by: + rma._check_required_after_draft + rma.action_confirm + """ + ir_translation = self.env['ir.translation'] + required = ['partner_id', 'partner_invoice_id', 'product_id', + 'location_id'] + for record in self: + desc = "" + for field in filter(lambda item: not record[item], required): + desc += "\n%s" % ir_translation.get_field_string("rma")[field] + if desc: + raise ValidationError(_('Required field(s):%s') % desc) + + def _ensure_can_be_returned(self): + """ This method is intended to be invoked after user click on + 'Replace' or 'Return to customer' button (before the delivery wizard + is launched) and after confirm the wizard. + + This method is invoked by: + rma.action_replace + rma.action_return + rma.create_replace + rma.create_return + """ + if len(self) == 1: + if not self.can_be_returned: + raise ValidationError( + _("This RMA cannot perform a return.")) + elif not self.filtered("can_be_returned"): + raise ValidationError( + _("None of the selected RMAs can perform a return.")) + + def _ensure_can_be_replaced(self): + """ This method is intended to be invoked after user click on + 'Replace' button (before the delivery wizard + is launched) and after confirm the wizard. + + This method is invoked by: + rma.action_replace + rma.create_replace + """ + if len(self) == 1: + if not self.can_be_replaced: + raise ValidationError( + _("This RMA cannot perform a replacement.")) + elif not self.filtered("can_be_replaced"): + raise ValidationError( + _("None of the selected RMAs can perform a replacement.")) + + def _ensure_can_be_split(self): + """intended to be called before launch and after save the split wizard. + invoked by: + rma.action_split + rma.extract_quantity + """ + self.ensure_one() + if not self.can_be_split: + raise ValidationError(_("This RMA cannot be split.")) + + def _ensure_qty_to_return(self, qty=None, uom=None): + """ This method is intended to be invoked after confirm the wizard. + invoked by: rma.create_return + """ + if qty and uom: + if uom != self.product_uom: + qty = uom._compute_quantity(qty, self.product_uom) + if qty > self.remaining_qty: + raise ValidationError( + _("The quantity to return is greater than " + "remaining quantity.")) + + def _ensure_qty_to_extract(self, qty, uom): + """ This method is intended to be invoked after confirm the wizard. + invoked by: rma.extract_quantity + """ + to_split_uom_qty = qty + if uom != self.product_uom: + to_split_uom_qty = uom._compute_quantity(qty, self.product_uom) + if to_split_uom_qty > self.remaining_qty: + raise ValidationError( + _("Quantity to extract cannot be greater than remaining " + "delivery quantity (%s %s)") + % (self.remaining_qty, self.product_uom.name) + ) + + # Reception business methods + def _create_receptions_from_picking(self): + self.ensure_one() + create_vals = {} + if self.location_id: + create_vals['location_id'] = self.location_id.id + return_wizard = self.env['stock.return.picking'].with_context( + active_id=self.picking_id.id, + active_ids=self.picking_id.ids, + ).create(create_vals) + return_wizard.product_return_moves.filtered( + lambda r: r.move_id != self.move_id + ).unlink() + return_line = return_wizard.product_return_moves + return_line.quantity = self.product_uom_qty + # set_rma_picking_type is to override the copy() method of stock + # picking and change the default picking type to rma picking type. + picking_action = return_wizard.with_context( + set_rma_picking_type=True).create_returns() + picking_id = picking_action['res_id'] + picking = self.env['stock.picking'].browse(picking_id) + picking.origin = "%s (%s)" % (self.name, picking.origin) + move = picking.move_lines + move.priority = self.priority + return move + + def _create_receptions_from_product(self): + self.ensure_one() + picking_form = Form( + recordp=self.env['stock.picking'].with_context( + default_picking_type_id=self.warehouse_id.rma_in_type_id.id), + view="stock.view_picking_form", + ) + self._prepare_picking(picking_form) + picking = picking_form.save() + picking.action_confirm() + picking.action_assign() + picking.message_post_with_view( + 'mail.message_origin_link', + values={'self': picking, 'origin': self}, + subtype_id=self.env.ref('mail.mt_note').id, + ) + return picking.move_lines + + def _prepare_picking(self, picking_form): + picking_form.company_id = self.company_id + picking_form.origin = self.name + picking_form.partner_id = self.partner_id + picking_form.location_dest_id = self.location_id + with picking_form.move_ids_without_package.new() as move_form: + move_form.product_id = self.product_id + move_form.product_uom_qty = self.product_uom_qty + move_form.product_uom = self.product_uom + + # Extract business methods + def extract_quantity(self, qty, uom): + self.ensure_one() + self._ensure_can_be_split() + self._ensure_qty_to_extract(qty, uom) + self.product_uom_qty -= uom._compute_quantity(qty, self.product_uom) + if self.remaining_qty_to_done <= 0: + if self.state == 'waiting_return': + self.state = 'returned' + elif self.state == 'waiting_replacement': + self.state = 'replaced' + extracted_rma = self.copy({ + 'origin': self.name, + 'product_uom_qty': qty, + 'product_uom': uom.id, + 'state': 'received', + 'reception_move_id': self.reception_move_id.id, + 'origin_split_rma_id': self.id, + }) + extracted_rma.message_post_with_view( + 'mail.message_origin_link', + values={'self': extracted_rma, 'origin': self}, + subtype_id=self.env.ref('mail.mt_note').id, + ) + self.message_post( + body=_( + 'Split: %s has been created.' + ) % ( + extracted_rma.id, + extracted_rma.name, + ) + ) + return extracted_rma + + # Refund business methods + def _prepare_refund(self, invoice_form, origin): + """ Hook method for preparing the refund Form. + + This method could be override in order to add new custom field + values in the refund creation. + + invoked by: + rma.action_refund + """ + self.ensure_one() + invoice_form.company_id = self.company_id + invoice_form.partner_id = self.partner_invoice_id + invoice_form.origin = origin + + def _prepare_refund_line(self, line_form): + """ Hook method for preparing a refund line Form. + + This method could be override in order to add new custom field + values in the refund line creation. + + invoked by: + rma.action_refund + """ + self.ensure_one() + line_form.product_id = self.product_id + line_form.quantity = self.product_uom_qty + line_form.uom_id = self.product_uom + line_form.price_unit = self._get_refund_line_price_unit() + + def _get_refund_line_price_unit(self): + """To be overriden in a third module with the proper origin values + in case a sale order is linked to the original move""" + return self.product_id.lst_price + + # Returning business methods + def create_return(self, scheduled_date, qty=None, uom=None): + """Intended to be invoked by the delivery wizard""" + self._ensure_can_be_returned() + self._ensure_qty_to_return(qty, uom) + group_dict = {} + for record in self.filtered('can_be_returned'): + key = (record.partner_id.id, record.company_id.id, + record.warehouse_id) + group_dict.setdefault(key, self.env['rma']) + group_dict[key] |= record + for rmas in group_dict.values(): + origin = ', '.join(rmas.mapped('name')) + rma_out_type = rmas[0].warehouse_id.rma_out_type_id + picking_form = Form( + recordp=self.env['stock.picking'].with_context( + default_picking_type_id=rma_out_type.id), + view="stock.view_picking_form", + ) + rmas[0]._prepare_returning_picking(picking_form, origin) + picking = picking_form.save() + for rma in rmas: + with picking_form.move_ids_without_package.new() as move_form: + rma._prepare_returning_move( + move_form, scheduled_date, qty, uom) + # rma_id is not present in the form view, so we need to get + # the 'values to save' to add the rma id and use the + # create method intead of save the form. + picking_vals = picking_form._values_to_save(all_fields=True) + move_vals = picking_vals['move_ids_without_package'][-1][2] + move_vals.update( + picking_id=picking.id, + rma_id=rma.id, + move_orig_ids=[(4, rma.reception_move_id.id)], + ) + self.env['stock.move'].create(move_vals) + rma.message_post( + body=_( + 'Return: %s has been created.' + ) % (picking.id, picking.name) + ) + picking.action_confirm() + picking.action_assign() + picking.message_post_with_view( + 'mail.message_origin_link', + values={'self': picking, 'origin': rmas}, + subtype_id=self.env.ref('mail.mt_note').id, + ) + self.write({'state': 'waiting_return'}) + + def _prepare_returning_picking(self, picking_form, origin=None): + picking_form.picking_type_id = self.warehouse_id.rma_out_type_id + picking_form.company_id = self.company_id + picking_form.origin = origin or self.name + picking_form.partner_id = self.partner_id + + def _prepare_returning_move(self, move_form, scheduled_date, + quantity=None, uom=None): + move_form.product_id = self.product_id + move_form.product_uom_qty = quantity or self.product_uom_qty + move_form.product_uom = uom or self.product_uom + move_form.date_expected = scheduled_date + + # Replacing business methods + def create_replace(self, scheduled_date, warehouse, product, qty, uom): + """Intended to be invoked by the delivery wizard""" + self.ensure_one() + self._ensure_can_be_replaced() + moves_before = self.delivery_move_ids + self._action_launch_stock_rule(scheduled_date, warehouse, product, + qty, uom) + new_move = self.delivery_move_ids - moves_before + if new_move: + self.reception_move_id.move_dest_ids = [(4, new_move.id)] + self.message_post( + body=_( + 'Replacement: ' + 'Move %s (Picking %s) ' + 'has been created.' + ) % (new_move.id, new_move.name_get()[0][1], + new_move.picking_id.id, new_move.picking_id.name) + ) + else: + self.message_post( + body=_( + 'Replacement:
' + 'Product %s
' + 'Quantity %f %s
' + 'This replacement did not create a new move, but one of ' + 'the previously created moves was updated with this data.' + ) % (product.id, product.display_name, qty, uom.name) + ) + if self.state != 'waiting_replacement': + self.state = 'waiting_replacement' + + def _action_launch_stock_rule( + self, + scheduled_date, + warehouse, + product, + qty, + uom, + ): + """ Creates a delivery picking and launch stock rule. It is invoked by: + rma.create_replace + """ + self.ensure_one() + if self.product_id.type not in ('consu', 'product'): + return + if not self.procurement_group_id: + self.procurement_group_id = self.env['procurement.group'].create({ + 'name': self.name, + 'move_type': 'direct', + 'partner_id': self.partner_id.id, + }).id + values = self._prepare_procurement_values( + self.procurement_group_id, scheduled_date, warehouse) + self.env['procurement.group'].run( + product, + qty, + uom, + self.partner_id.property_stock_customer, + self.product_id.display_name, + self.procurement_group_id.name, + values, + ) + + def _prepare_procurement_values( + self, + group_id, + scheduled_date, + warehouse, + ): + self.ensure_one() + return { + 'company_id': self.company_id.id, + 'group_id': group_id, + 'date_planned': scheduled_date, + 'warehouse_id': warehouse, + 'partner_id': self.partner_id.id, + 'rma_id': self.id, + 'priority': self.priority, + } + + # Mail business methods + def message_new(self, msg_dict, custom_values=None): + """Extract the needed values from an incoming rma emails data-set + to be used to create an RMA. + """ + if custom_values is None: + custom_values = {} + subject = msg_dict.get('subject', '') + body = html2plaintext(msg_dict.get('body', '')) + desc = _("E-mail subject: %s\n\nE-mail body:\n%s") % (subject, body) + defaults = { + 'description': desc, + 'name': _('New'), + 'origin': _('Incoming e-mail'), + } + if msg_dict.get('author_id'): + partner = self.env['res.partner'].browse(msg_dict.get('author_id')) + defaults.update( + partner_id=partner.id, + partner_invoice_id=partner.address_get( + ['invoice']).get('invoice', False), + ) + if msg_dict.get("priority"): + defaults["priority"] = msg_dict.get("priority") + defaults.update(custom_values) + rma = super().message_new(msg_dict, custom_values=defaults) + if (rma.user_id + and rma.user_id.partner_id not in rma.message_partner_ids): + rma.message_subscribe([rma.user_id.partner_id.id]) + return rma + + @api.returns('mail.message', lambda value: value.id) + def message_post(self, **kwargs): + """ Set 'sent' field to True when an email is sent from rma form + view. This field (sent) is used to set the appropriate style to the + 'Send by Email' button in the rma form view. + """ + if self.env.context.get('mark_rma_as_sent'): + self.write({'sent': True}) + # mail_post_autofollow=True to include email recipient contacts as + # RMA followers + self_with_context = self.with_context(mail_post_autofollow=True) + return super(Rma, self_with_context).message_post(**kwargs) + + # Reporting business methods + def _get_report_base_filename(self): + self.ensure_one() + return 'RMA Report - %s' % self.name + + # Other business methods + def update_received_state(self): + """ Invoked by: + [stock.move].unlink + [stock.move]._action_cancel + """ + rma = self.filtered(lambda r: r.delivered_qty == 0) + if rma: + rma.write({'state': 'received'}) + + def update_replaced_state(self): + """ Invoked by: + [stock.move]._action_done + [stock.move].unlink + [stock.move]._action_cancel + """ + rma = self.filtered( + lambda r: (r.state == 'waiting_replacement' + and 0 >= r.remaining_qty_to_done == r.remaining_qty)) + if rma: + rma.write({'state': 'replaced'}) + + def update_returned_state(self): + """ Invoked by [stock.move]._action_done""" + rma = self.filtered( + lambda r: (r.state == 'waiting_return' + and r.remaining_qty_to_done <= 0)) + if rma: + rma.write({'state': 'returned'}) diff --git a/rma/models/rma_operation.py b/rma/models/rma_operation.py new file mode 100644 index 00000000..85271aeb --- /dev/null +++ b/rma/models/rma_operation.py @@ -0,0 +1,15 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class RmaOperation(models.Model): + _name = "rma.operation" + _description = "RMA requested operation" + + name = fields.Char(required=True, translate=True) + + _sql_constraints = [ + ('name_uniq', 'unique (name)', "That operation name already exists !"), + ] diff --git a/rma/models/rma_team.py b/rma/models/rma_team.py new file mode 100644 index 00000000..31b60097 --- /dev/null +++ b/rma/models/rma_team.py @@ -0,0 +1,56 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models + + +class RmaTeam(models.Model): + _name = "rma.team" + _inherit = ['mail.alias.mixin', 'mail.thread'] + _description = "RMA Team" + _order = "name" + + 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.user.company_id, + ) + 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): + 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 get_alias_model_name(self, vals): + return vals.get('alias_model', 'rma') + + def get_alias_values(self): + values = super().get_alias_values() + values['alias_defaults'] = {'team_id': self.id} + return values diff --git a/rma/models/stock_move.py b/rma/models/stock_move.py new file mode 100644 index 00000000..8ab511ce --- /dev/null +++ b/rma/models/stock_move.py @@ -0,0 +1,96 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class StockMove(models.Model): + _inherit = "stock.move" + + # RMAs that were created from the delivery move + rma_ids = fields.One2many( + comodel_name='rma', + inverse_name='move_id', + string='RMAs', + copy=False, + ) + # RMAs linked to the incoming movement from client + rma_receiver_ids = fields.One2many( + comodel_name='rma', + inverse_name='reception_move_id', + string='RMA receivers', + copy=False, + ) + # RMA that create the delivery movement to the customer + rma_id = fields.Many2one( + comodel_name='rma', + string='RMA return', + copy=False, + ) + + def unlink(self): + rma_receiver = self.mapped('rma_receiver_ids') + rma = self.mapped('rma_id') + res = super().unlink() + rma_receiver.write({'state': 'draft'}) + rma.update_received_state() + rma.update_replaced_state() + return res + + def _action_cancel(self): + res = super()._action_cancel() + cancelled_moves = self.filtered(lambda r: r.state == 'cancel') + 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): + """ 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.rma_receiver_ids + if (rma_receiver + and move.quantity_done != rma_receiver.product_uom_qty): + raise ValidationError( + _("The quantity done for the product '%s' must " + "be equal to its initial demand because the " + "stock move is linked to an RMA (%s).") + % (move.product_id.name, move.rma_receiver_ids.name) + ) + res = super()._action_done() + move_done = self.filtered(lambda r: r.state == 'done') + # set RMAs as received + to_be_received = move_done.mapped('rma_receiver_ids').filtered( + lambda r: r.state == 'confirmed') + to_be_received.write({'state': 'received'}) + # set RMAs as delivered + move_done.mapped('rma_id').update_replaced_state() + move_done.mapped('rma_id').update_returned_state() + return res + + @api.model + def _prepare_merge_moves_distinct_fields(self): + """ The main use is that launched delivery RMAs doesn't merge + two moves if they are linked to a different RMAs. + """ + return super()._prepare_merge_moves_distinct_fields() + ['rma_id'] + + def _prepare_move_split_vals(self, qty): + """ Intended to the backport of picking linked to RMAs propagates the + RMA link id. + """ + res = super()._prepare_move_split_vals(qty) + res['rma_id'] = self.rma_id.id + return res + + +class StockRule(models.Model): + _inherit = 'stock.rule' + + def _get_custom_move_fields(self): + return super()._get_custom_move_fields() + ['rma_id'] diff --git a/rma/models/stock_picking.py b/rma/models/stock_picking.py new file mode 100644 index 00000000..e341be0b --- /dev/null +++ b/rma/models/stock_picking.py @@ -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 StockPicking(models.Model): + _inherit = 'stock.picking' + + rma_count = fields.Integer( + string='RMA count', + compute='_compute_rma_count', + ) + + def _compute_rma_count(self): + for rec in self: + rec.rma_count = len(rec.move_lines.mapped('rma_ids')) + + def copy(self, default=None): + if self.env.context.get('set_rma_picking_type'): + location_dest_id = default['location_dest_id'] + warehouse = self.env['stock.warehouse'].search( + [('rma_loc_id', 'parent_of', location_dest_id)], limit=1) + if warehouse: + default['picking_type_id'] = warehouse.rma_in_type_id.id + return super().copy(default) + + def action_view_rma(self): + self.ensure_one() + action = self.env.ref('rma.rma_action').read()[0] + rma = self.move_lines.mapped('rma_ids') + if len(rma) == 1: + action.update( + res_id=rma.id, + view_mode="form", + view_id=False, + views=False, + ) + else: + action['domain'] = [('id', 'in', rma.ids)] + return action diff --git a/rma/models/stock_warehouse.py b/rma/models/stock_warehouse.py new file mode 100644 index 00000000..7c0716d2 --- /dev/null +++ b/rma/models/stock_warehouse.py @@ -0,0 +1,118 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# 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', + ) + + def _get_locations_values(self, vals): + values = super()._get_locations_values(vals) + values.update({ + 'rma_loc_id': { + 'name': 'RMA', + 'active': True, + 'return_location': True, + 'usage': 'internal', + 'company_id': vals.get('company_id', self.company_id.id), + 'location_id': self.view_location_id.id, + }, + }) + return values + + def _get_sequence_values(self): + values = super()._get_sequence_values() + values.update({ + 'rma_in_type_id': { + 'name': self.name + ' ' + _('Sequence RMA in'), + 'prefix': self.code + '/RMA/IN/', 'padding': 5, + 'company_id': self.company_id.id, + }, + 'rma_out_type_id': { + 'name': self.name + ' ' + _('Sequence RMA out'), + 'prefix': self.code + '/RMA/OUT/', 'padding': 5, + 'company_id': self.company_id.id, + }, + }) + return values + + def _update_name_and_code(self, new_name=False, new_code=False): + for warehouse in self: + sequence_data = warehouse._get_sequence_values() + warehouse.rma_in_type_id.sequence_id.write( + sequence_data['rma_in_type_id']) + warehouse.rma_in_type_id.sequence_id.write( + sequence_data['rma_out_type_id']) + + def _get_picking_type_create_values(self, max_sequence): + data, next_sequence = super()._get_picking_type_create_values( + max_sequence) + data.update({ + 'rma_in_type_id': { + 'name': _('RMA Receipts'), + 'code': 'incoming', + 'use_create_lots': True, + 'use_existing_lots': False, + 'default_location_src_id': False, + 'default_location_dest_id': self.rma_loc_id.id, + 'sequence': max_sequence + 1, + }, + '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, + }, + }) + return data, max_sequence + 3 + + def _get_picking_type_update_values(self): + data = super()._get_picking_type_update_values() + data.update({ + 'rma_in_type_id': { + 'default_location_dest_id': self.rma_loc_id.id, + }, + 'rma_out_type_id': { + 'default_location_src_id': self.rma_loc_id.id, + }, + }) + return data + + def _create_or_update_sequences_and_picking_types(self): + data = super()._create_or_update_sequences_and_picking_types() + stock_picking_type = self.env['stock.picking.type'] + if 'out_type_id' in data: + rma_out_type = stock_picking_type.browse(data['rma_out_type_id']) + rma_out_type.write({ + 'return_picking_type_id': data.get('rma_in_type_id', False) + }) + if 'rma_in_type_id' in data: + rma_in_type = stock_picking_type.browse(data['rma_in_type_id']) + rma_in_type.write({ + 'return_picking_type_id': data.get('rma_out_type_id', False) + }) + return data diff --git a/rma/readme/CONFIGURE.rst b/rma/readme/CONFIGURE.rst new file mode 100644 index 00000000..c5513046 --- /dev/null +++ b/rma/readme/CONFIGURE.rst @@ -0,0 +1,8 @@ +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'. diff --git a/rma/readme/CONTRIBUTORS.rst b/rma/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..d4052271 --- /dev/null +++ b/rma/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* `Tecnativa `_: + + * Ernesto Tejeda + * Pedro M. Baeza + * David Vidal diff --git a/rma/readme/DESCRIPTION.rst b/rma/readme/DESCRIPTION.rst new file mode 100644 index 00000000..0c0c6fd8 --- /dev/null +++ b/rma/readme/DESCRIPTION.rst @@ -0,0 +1,8 @@ +This module allows you to manage `Return Merchandise Authorization (RMA) +`_. +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. diff --git a/rma/readme/ROADMAP.rst b/rma/readme/ROADMAP.rst new file mode 100644 index 00000000..c6cb6029 --- /dev/null +++ b/rma/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +* As soon as the picking is selected, the user should select the move, + but perhaps stock.move _rec_name could be improved to better show what + the product of that move is. diff --git a/rma/readme/USAGE.rst b/rma/readme/USAGE.rst new file mode 100644 index 00000000..6e6194a9 --- /dev/null +++ b/rma/readme/USAGE.rst @@ -0,0 +1,37 @@ +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. + +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. + +**Note: An RMA can also be created from an incoming email (See configuration +section).** diff --git a/rma/report/report.xml b/rma/report/report.xml new file mode 100644 index 00000000..5816ab29 --- /dev/null +++ b/rma/report/report.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/rma/security/ir.model.access.csv b/rma/security/ir.model.access.csv new file mode 100644 index 00000000..0afc4d55 --- /dev/null +++ b/rma/security/ir.model.access.csv @@ -0,0 +1,8 @@ +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 diff --git a/rma/security/rma_security.xml b/rma/security/rma_security.xml new file mode 100644 index 00000000..5a8037c9 --- /dev/null +++ b/rma/security/rma_security.xml @@ -0,0 +1,60 @@ + + + + + + RMA + Manage Return Merchandise Authorizations (RMAs). + + + + User: Own Documents Only + + + the user will have access to his own data in the RMA application. + + + User: All Documents + + + the user will have access to all records of everyone in the RMA application. + + + Manager + the user will have an access to the RMA configuration as well as statistic reports. + + + + + + + Personal RMAs + + ['|',('user_id','=',user.id),('user_id','=',False)] + + + + All RMAs + + [(1,'=',1)] + + + + + RMA multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + RMA team multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + + + diff --git a/rma/static/description/icon.png b/rma/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2f8ea42a5706573c60ddab7451b99b07babf29e7 GIT binary patch literal 7450 zcmV+#9p&PQP)hA>a-^0)`p8xN5^J)w<{C`P`R^3;{hyG6p78uBr++fFElDDMQt*hzuZw*r&g;}^9q2myvg(J z1@HxyS(0cs7|`9mtxG5@{IY6uYU)dBL0C3dAJnL}*F1qGnCIo|xmm1MM4Qd14w0m< zC|!yof6vj?^7Oa2FHw)t7@yG~1Zr3+3i7$ja)x@g!=ivPb+U+JqNsE+iUN_Sz@jJ+ zsTEKZ%?e-?Ae1gikphg;x+t1vU=&ys8)m{39v{!*)vJk0Or*E9l^Z8dV(#ftYH2bV2Y03pokdYZ)M`;{wS?+)=#r9>8FWUus{LEmn=3LpBoJ1Y}gRIyMHb{JW*uv>eUo&-%jMBMG7$9u+-eV z2|#RSrs9zBP|E%Hv#h+F@c4KI7;jhr`Z_ukO1xT{JR2q4YuFvC z)6sqH8kWAk`2~#8Wa9iN^C%WcqRn6+!fq!zHFe$%E&WLo5rM;Eq03;HlOAE8!1#k@ z1T+o@(J3hkG_F2gttKQajP5VKU~mp55)gxd#cllB?R>g#ALsY%p{J!qp~b1F#6%V? zU5Ypd_i7l{!rbZW<5GYlAgd(N+SG)Op$rV6I-SzWs8q<3#O=$M=UiZx=m|?xXJ=3+ z>9gB8xMvR+l$OS==#&(q5)&nG@72TgeRgq%MlJe*LS&b=?A0?Zf;;lum) zBM1UT+qVbZJx$i5KHt^`ZEeulNYs%dSR{!@AAg+1t5=g!S~_lUL{S`SxkM4>h}T!O zIwRxt$-ID7mTA3ub;uA7&?D@y*|>S~Bv;1RC5`JrFcfxr@yZn%jvPVg>LMa0hVc0K zaRs*bwzSZB`LcU8rUJ}ZM^8;nL@|^xbF9w2sRYIjxT@3!T)TRe&e~ccBO-`SPDZ8C z1UCq{b&FdK4RZ#VDO(d_bU65A7CHCqz~#0!KKQ49;{1mnGSJl(oE;MuHnwHH3NXH* z`OB_dvj!R&xMQ*K{yXn*;iHc*b$18egwHX@-IT97Gs!Z4*|lrNPgn{b|B1le_I7%2 z-3oYFi9E+rmP`x&CRyg=UAvGS4hqZ5$K~9c=!c;d1nj`7<;zJeE+(b8n1rQEgLd>K z7Z>y3i!ahzUr%doErS#6D4iF;xS%<(XAiO@QMhee(0-cnHm@2$EeJS(N1uM0RK1?$ z!a~1UBLbO%5JZuL{CpDf^C|u6SFsKZ(0<_pZFP0DoIQ&?%d$jFYX@tRWe$Gy5wa}% zcJ57ZbFKnHJOFw4(ML%wE+$p4C(6&B2C^*E{>2v<&Yk0CVD_Oj9C4Bqa^xwHdTU{M(_4V97e_jD*EYN(icP}u~Biw!cdeEa2*Uf1J0l_)6 zU-#gHq?VMBs@Jn1E6Z;yakry`t2H%TJ$H_)=g!g9)rHAw#bmW|%V;F4xR}_53-4Km zx}!&FI&+4M!a}n3dg79jhJ7EoU;%j>Hhef>J^_4TyX*VA+Tx&n+}Xk?is zk32$jN(wzKEnM8cpVKup0d+AUK!_kb=Gc#HC)KfTYHf~?KL|a`Q?F|i>?%Yv;83xV1eH=V=h_!k>7Ne1x z#zqGGbW4HgXBws(gddib&8l&q%TQH0Tak^MH>dauSfB0c`yz~;`eiAnJ z_xAGEkAB33vuAyt6F{v}5w6h?uGOLo3nQzjh-|%{Y`vbOoE*O`n6aZ{sQYSY;P!Cz!6c)b4Q5H#kV&;2^hp zd%4ls$*HO;!nIoB;^N40!joN6GQ$3eN=P7T^JelkZ$_3Rx~^TLy}q9Ih6Z|W+?apB zggPs6(^tRB;?h#m3k%VNhWb6G<;oRKfBZ2`)z!2&H)FEd+<^FWqB$#DB?yFRH0U%M zRAU%2_jGn*?CvHmF>zF2+6@K{crsjg3RyS9LX3qj7En z6D|l8u3X8EH{V1Rz(y9Sd3k6fBZ*E=r~dS5E}S{T;K0C$u0Q~^8V%7J4VoY}$}kTM z(9_w;;!$iY$>E@-sR^su?DKnFNC-NoGf> zu2@0*7=p{y8VxCWJt=xUrO!Ug;h+7CyAv?#nvuY$fk9d3iPv5W2o{$XCMUCT>sB^y z-3man!9c_5(_E~srp3da7?)%jqo28@p3Y9%3QxNq&T+M_j`Jr@1byDVt}Y}=@@Yr4U%N(E04ZDb4?akiUQbqWF-h6k6P}4QRHtLZ zYp+rH=9_a1m@rY~ORu~_+9d7PCW<1dd3mJfC!2g)n1UDP06#*a_;^2=LRqlf zDz%!-!a_0&3)%GCb6Cx0u3xx-;oLb4b#)jT8vG8`ed*Zl-Bf+>0d}j^{aKFF)0?H& zlTlD`Pvd>El)GUAcdlNg`P8Y|4NM5IFeitn{_gLlwH%tzQ1b4-pFC%tm%;viuGQCb zwYC;RT^%<)0@5#@K8>Nio{k$gIQz*b+`4`po5cda;=DYvi;IW4nW7@X#<;=8RA}Wl zzscR}*MnNkFe4myB102`uT@kKGluz@>6G#AT@1Cgd{I}2p|+Mb=g1==HI>}bQg=6_ zo8jrpOx@k=d-c`X2~312^8BxUMUGz2Y}4Iaw~%Fdgi~!c=vHGRpS|@~z%Tc-JLfii z_q($a7J%5q#90XoKw^GA1y0w)tN{i=V9AOVv+6+`ySuq^_UtgHoMmv3%V*BeH;!Mj}<3<)vv#s#nuYSee|M(C0T#t9x z>ZvE5eU|56etBBUd%LZT)0LH+K5~RB_4UKbsuM-RRVp3`aOb?~1ID9uY3=L7>hXrU z0+qZc472m|-SDJjWK3y!5AE8;|NZGt0Z@w~@!{cg4h*N^qd@4Z`{b5O`FKb%bV2l)_wjtKYQ^-B!>f05J2!8hNyQJsjOjwy@S>{T8 zy&ICwwzlD2qbo$hLu!?ZxX@76jk6rHB1F?6NwoL(yQ9_oDQ?lCA$T@!gnp@$Im@SXL~zB0I;~!+}DQy#3v^5^MC($V#l#fj>B%}VoePvoi<62Ga%e&_w9rx zLaTL046X^#xSiF&jA-(Ed$Eln8-gx2mX!}Z#Oh6(C|b3OkN{RMTFhqt_qV@IlXI<5 zx(6&@df^3@ty{;NU;EmyWB%9s?{jM3K0f`={~QL5%l`U5zyCej045!6Rx1ssPI2<^ zVa|T`8U4=aJ%6@dTv!;PUNhel1(;Sm z(cNfPh<hPXmW3%Vfgn3^njUj=LFGtJM^(UOfa)Ss5n| z9_07`@DB)rfXZV;l$M*z_kZ&nLL(yFrx^_xfZI23^6M8~!0aqndw;)sxc&UgFSB;b z7OJbNIC=Om=g*u$vfJI=i@TfgYKgodDOk0NqV?-3c<@0YeArhN-}=^s*Tt*_W|#-o z;lNN|&-tn<&L2Ncw^y7;uh~qG+5FdBd9IO{SA_^3BMeU?H+In?Ui|HEiS=54?GG4$ ziziR=&X0fm*O8OaI4tPOb#)gY-mnNjvR53<(&fu3TDOj6>(&t!7w7l6Oa?G>BHN9q zQjuFyLT*V3Uw-jLZZtO1P*p|4v14?$v=AK{O0?5v>9<Eix;!$*=Ol)Z+F9EsINyQio;-8 z^TZQuc;=Z2xvnNOl&`=3I=}he_b@tF61ZSV$;zUrtc;?vG7_hFeoO%-0L3LGQMO|T zWjl7zd*==pjvuGKs*1~J&vM_&m2CUr4=4P3U;oKZ_~(~iA~8LkqV?-3Dk~$!Yssqu z%q&p!f(1PA)Kfh0)Ki!T2GE3t`d#E7ta3_A`T73+!-nRwO$snxn(WG)&9J~si0M_N z0Ha6&Mv)=~7)1&&iWFcJDZnUFfKj9Xqe#*01m@tM|BP)8Z5BS+a@g(s_lF-&=s9I) z{LrkJ8u*tt-r$eF{Vf}we3H%O<)mh1DOC7S_wCyp-o2ZnAAA74y_6LcOb95Rnl*Dt zecj#cdG9^;y!RgWm6oz)+cws2-i&qzXGxeUoj-Gi0~HmVIeHXPl89BQ#R$JtU|PIi9&ynX5q@m=ANG6JCC>`|D6v`qt&u@%NDk5+eUtA z>731T2h*AZHBe<%0oc)^$l~&g|g5 zW;5L;(_e4WxkSWjv2bMXUXJYDOG-uto6F02_{k?{RAQpBs-v})Lx27=hxhDphjx12 zE3%9V$k6GqGPF_qqKF8xgMekMnw4v-z17m&JCt9>Gi!mz-)gm*l@CA6=JIm%Yt~Fy zW~j+%VI)c7-0|ZasHmX!`0-KSdS7Wr5+WnX3Sv3TSX-A_YxH_6)fxHUZ?)3d-~ZPg zL7cMN?VLDth!cknu`n@_P0o!g7Ea=u7^r%?yQ%!}Lk|AwPjq_iQQ*0DNvGAi*H{gM zB`S)nMT;gCmO0bH_)=S6A7(%6tVKbfWbIlumzT4mtPFJ!ql$@WVJ_FzaiF4t6Ne6A z_i#?S)-1XH_LpiTF)E780AUGs1NJ!^)0^yUt{sv@b8qj6tX7gNQ+@0hHOGz-9T&%j zr=DWd)~!>L3e7w?$Y=ZabD*Mv8%<3<69CB!rAAA>$A~Xcrz11K)sXTyw!oS5LEtJ; zBqKI<$cQu=8Sr|GUERU%ci(0AAO1joX(^k_%Xwh)=74g!jLFve;tLK`RB&|PK1}`n ze_hM%r9}}DBXYd>Qj$(b#uP7|onH&tJl@uuJv}%iY54kPS*EeNn#P(Me*cSKu>SGK z*<4;uR)Co%J>{_5Id$X+2P!IPIDL9V0sz-~YSkEW=M8H?qoY|IL`Exlu5GY2RZ4db z&$~<}`h0A&H()gK@y?xm{9pe?c78saw{2tnqmPcrB{Q13Iy(4d*Den2+QnU0dNF5y z8uwy+Ste4eB|gCEjg;tU(#MhhOaW#biVqJbK0F+N=HA|+JbIpQp&(#rY{YQ!BLDWw zU-ICVEo?3?_X$q3!NA=+cldbcPEH>^isY4(&-HeTUJL#Mf+Z$~^dNmo<858$^eAau zMybulEu(R0lrl9WDd@CuT;Pl~v8x~mBt%Az$%ysqYl?}Pc32c(#%M3VP;$d|XToMYd?fxxXK6up zJKictPDU4G7{^4yq5uRRPGQr#S`FDwURYeqKc-5e0E0?Z%|2MB8WcA$R-4VFRM+f-Wx6Ex^!GO_RW>_d znXXpFrT@Ng;iPD`TCd*qT*9GfifjQPU|6PGMyvI*h#(y6?&+b&r1VTvC6yqsYK+T$ zr@K{gXBN!T3@?2?2=t+;9?N?MhQ<(Zi?9uk163m0f;Zl<}rdnig! z5We;QPM)k50Cp4psM5&5-L~&%Zq*(imG9fiTMT z6w?HVM3yyvZ_r0&`@FEP<9!v@3q&E12jt^H4Ex*@*3ll&Xyx&> z5A%e6}5DrJh#_s-?qem56MNx42 Y|6GCN!Hxm}hX4Qo07*qoM6N<$f{|u$(*OVf literal 0 HcmV?d00001 diff --git a/rma/static/description/index.html b/rma/static/description/index.html new file mode 100644 index 00000000..a45186e6 --- /dev/null +++ b/rma/static/description/index.html @@ -0,0 +1,496 @@ + + + + + + +Return Merchandise Authorization Management + + + +
+

Return Merchandise Authorization Management

+ + +

Beta License: AGPL-3 OCA/rma Translate me on Weblate Try me on Runbot

+

This module allows you to manage Return Merchandise Authorization (RMA). +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

+ +
+

Configuration

+

If you want RMAs to be created from incoming emails, you need to:

+
    +
  1. Go to Settings > General Settings.
  2. +
  3. Check ‘External Email Servers’ checkbox under Discuss section.
  4. +
  5. Set an ‘alias domain’ and an incoming server.
  6. +
  7. Go to RMA > Configuration > RMA Team and select a team or create a new +one.
  8. +
  9. Go to ‘Email’ tab and set an ‘Email Alias’.
  10. +
+
+
+

Usage

+

To use this module, you need to:

+
    +
  1. Go to RMA > Orders and create a new RMA.
  2. +
  3. 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.
  4. +
  5. You will see an smart button labeled ‘Receipt’. Click on that button to see +the reception operation form.
  6. +
  7. If everything is right, validate the operation and go back to the RMA to +see it in a ‘received’ state.
  8. +
  9. 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.
    • +
    +
  10. +
+

An RMA can also be created from a return of a delivery order:

+
    +
  1. Select a delivery order and click on ‘Return’ button to create a return.
  2. +
  3. Check “Create RMAs” checkbox in the returning wizard, select the RMA +stock location and click on ‘Return’ button.
  4. +
  5. 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.
  6. +
+

Note: An RMA can also be created from an incoming email (See configuration +section).

+
+
+

Known issues / Roadmap

+
    +
  • As soon as the picking is selected, the user should select the move, +but perhaps stock.move _rec_name could be improved to better show what +the product of that move is.
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+
    +
  • Tecnativa:
      +
    • Ernesto Tejeda
    • +
    • Pedro M. Baeza
    • +
    • David Vidal
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

Current maintainer:

+

ernestotejeda

+

This module is part of the OCA/rma project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/rma/tests/__init__.py b/rma/tests/__init__.py new file mode 100644 index 00000000..5f9ab818 --- /dev/null +++ b/rma/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_rma diff --git a/rma/tests/test_rma.py b/rma/tests/test_rma.py new file mode 100644 index 00000000..197276ad --- /dev/null +++ b/rma/tests/test_rma.py @@ -0,0 +1,638 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests import Form, SavepointCase +from odoo.exceptions import UserError, ValidationError + + +class TestRma(SavepointCase): + + @classmethod + def setUpClass(cls): + super(TestRma, cls).setUpClass() + 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 + account_pay = cls.env['account.account'].create({ + 'code': 'X1111', + 'name': 'Creditors - (test)', + 'user_type_id': cls.env.ref( + 'account.data_account_type_payable').id, + 'reconcile': True, + }) + cls.journal = cls.env['account.journal'].create({ + 'name': 'sale_0', + 'code': 'SALE0', + 'type': 'sale', + 'default_debit_account_id': account_pay.id, + }) + cls.product = cls.product_product.create({ + 'name': 'Product test 1', + 'type': 'product', + }) + account_type = cls.env['account.account.type'].create({ + 'name': 'RCV type', + 'type': 'receivable', + }) + cls.account_receiv = cls.env['account.account'].create({ + 'name': 'Receivable', + 'code': 'RCV00', + 'user_type_id': account_type.id, + 'reconcile': True, + }) + cls.partner = cls.res_partner.create({ + 'name': 'Partner test', + 'property_account_receivable_id': cls.account_receiv.id, + }) + cls.partner_invoice = cls.res_partner.create({ + 'name': 'Partner invoice test', + 'parent_id': cls.partner.id, + 'type': 'invoice', + }) + + def _create_rma(self, partner=None, product=None, qty=None, location=None): + rma_form = Form(self.env['rma']) + if partner: + rma_form.partner_id = partner + if product: + rma_form.product_id = product + if qty: + rma_form.product_uom_qty = qty + if location: + rma_form.location_id = location + return rma_form.save() + + def _create_confirm_receive(self, partner=None, product=None, qty=None, + location=None): + rma = self._create_rma(partner, product, qty, location) + rma.action_confirm() + rma.reception_move_id.quantity_done = rma.product_uom_qty + rma.reception_move_id.picking_id.action_done() + return rma + + def _test_readonly_fields(self, rma): + with Form(rma) as rma_form: + with self.assertRaises(AssertionError): + rma_form.partner_id = self.env['res.partner'] + with self.assertRaises(AssertionError): + rma_form.partner_invoice_id = self.env['res.partner'] + with self.assertRaises(AssertionError): + rma_form.picking_id = self.env['stock.picking'] + with self.assertRaises(AssertionError): + rma_form.move_id = self.env['stock.move'] + with self.assertRaises(AssertionError): + rma_form.product_id = self.env['product.product'] + with self.assertRaises(AssertionError): + rma_form.product_uom_qty = 0 + with self.assertRaises(AssertionError): + rma_form.product_uom = self.env['uom.uom'] + with self.assertRaises(AssertionError): + rma_form.location_id = self.env['stock.location'] + + def _create_delivery(self): + picking_type = self.env['stock.picking.type'].search( + [ + ('code', '=', 'outgoing'), + '|', + ('warehouse_id.company_id', '=', self.company.id), + ('warehouse_id', '=', False) + ], + limit=1, + ) + picking_form = Form( + recordp=self.env['stock.picking'].with_context( + default_picking_type_id=picking_type.id), + view="stock.view_picking_form", + ) + picking_form.company_id = self.company + picking_form.partner_id = self.partner + with picking_form.move_ids_without_package.new() as move: + move.product_id = self.product + move.product_uom_qty = 10 + with picking_form.move_ids_without_package.new() as move: + move.product_id = self.product_product.create( + {'name': 'Product 2 test', 'type': 'product'}) + move.product_uom_qty = 20 + picking = picking_form.save() + picking.action_confirm() + for move in picking.move_lines: + move.quantity_done = move.product_uom_qty + picking.button_validate() + return picking + + def test_onchange(self): + rma_form = Form(self.env['rma']) + # If partner changes, the invoice address is set + rma_form.partner_id = self.partner + self.assertEqual(rma_form.partner_invoice_id, self.partner_invoice) + # If origin move changes, the product is set + uom_ten = self.env['uom.uom'].create({ + 'name': "Ten", + 'category_id': self.env.ref('uom.product_uom_unit').id, + 'factor_inv': 10, + 'uom_type': 'bigger', + }) + product_2 = self.product_product.create({ + 'name': 'Product test 2', + 'type': 'product', + 'uom_id': uom_ten.id, + }) + outgoing_picking_type = self.env['stock.picking.type'].search( + [ + ('code', '=', 'outgoing'), + '|', + ('warehouse_id.company_id', '=', self.company.id), + ('warehouse_id', '=', False) + ], + limit=1, + ) + picking_form = Form( + recordp=self.env['stock.picking'].with_context( + default_picking_type_id=outgoing_picking_type.id), + view="stock.view_picking_form", + ) + picking_form.company_id = self.company + picking_form.partner_id = self.partner + picking_form.partner_id = self.partner + with picking_form.move_ids_without_package.new() as move: + move.product_id = product_2 + move.product_uom_qty = 15 + move.product_uom = uom_ten + picking = picking_form.save() + picking.action_done() + rma_form.picking_id = picking + rma_form.move_id = picking.move_lines + self.assertEqual(rma_form.product_id, product_2) + self.assertEqual(rma_form.product_uom_qty, 15) + self.assertEqual(rma_form.product_uom, uom_ten) + # If product changes, unit of measure changes + rma_form.picking_id = self.env['stock.picking'] + rma_form.product_id = self.product + self.assertEqual(rma_form.product_id, self.product) + self.assertEqual(rma_form.product_uom_qty, 15) + self.assertNotEqual(rma_form.product_uom, uom_ten) + self.assertEqual(rma_form.product_uom, self.product.uom_id) + rma = rma_form.save() + # If product changes, unit of measure domain should also change + domain = rma._onchange_product_id()['domain']['product_uom'] + self.assertListEqual( + domain, [('category_id', '=', self.product.uom_id.category_id.id)]) + + def test_ensure_required_fields_on_confirm(self): + rma = self._create_rma() + with self.assertRaises(ValidationError) as e: + rma.action_confirm() + self.assertEqual( + e.exception.name, + "Required field(s):\nCustomer\nInvoice Address\nProduct\nLocation" + ) + with Form(rma) as rma_form: + rma_form.partner_id = self.partner + with self.assertRaises(ValidationError) as e: + rma.action_confirm() + self.assertEqual( + e.exception.name, "Required field(s):\nProduct\nLocation") + with Form(rma) as rma_form: + rma_form.product_id = self.product + rma_form.location_id = self.rma_loc + rma.action_confirm() + self.assertEqual(rma.state, 'confirmed') + + def test_confirm_and_receive(self): + rma = self._create_rma(self.partner, self.product, 10, self.rma_loc) + rma.action_confirm() + self.assertEqual(rma.reception_move_id.picking_id.state, 'assigned') + self.assertEqual(rma.reception_move_id.product_id, rma.product_id) + self.assertEqual(rma.reception_move_id.product_uom_qty, 10) + self.assertEqual(rma.reception_move_id.product_uom, rma.product_uom) + self.assertEqual(rma.state, 'confirmed') + self._test_readonly_fields(rma) + rma.reception_move_id.quantity_done = 9 + with self.assertRaises(ValidationError): + rma.reception_move_id.picking_id.action_done() + rma.reception_move_id.quantity_done = 10 + rma.reception_move_id.picking_id.action_done() + self.assertEqual(rma.reception_move_id.picking_id.state, 'done') + self.assertEqual(rma.reception_move_id.quantity_done, 10) + self.assertEqual(rma.state, 'received') + self._test_readonly_fields(rma) + + def test_cancel(self): + # cancel a draft RMA + rma = self._create_rma(self.partner, self.product) + rma.action_cancel() + self.assertEqual(rma.state, 'cancelled') + self._test_readonly_fields(rma) + # cancel a confirmed RMA + rma = self._create_rma(self.partner, self.product, 10, self.rma_loc) + rma.action_confirm() + rma.action_cancel() + self.assertEqual(rma.state, 'cancelled') + # A RMA is only cancelled from draft and confirmed states + rma = self._create_confirm_receive(self.partner, self.product, 10, + self.rma_loc) + with self.assertRaises(UserError): + rma.action_cancel() + + def test_lock_unlock(self): + # A RMA is only locked from 'received' state + rma_1 = self._create_rma(self.partner, self.product, 10, self.rma_loc) + rma_2 = self._create_confirm_receive(self.partner, self.product, 10, + self.rma_loc) + self.assertEqual(rma_1.state, 'draft') + self.assertEqual(rma_2.state, 'received') + (rma_1 | rma_2).action_lock() + self.assertEqual(rma_1.state, 'draft') + self.assertEqual(rma_2.state, 'locked') + # A RMA is only unlocked from 'lock' state and it will be set + # to 'received' state + (rma_1 | rma_2).action_unlock() + self.assertEqual(rma_1.state, 'draft') + self.assertEqual(rma_2.state, 'received') + + 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.type, 'out_refund') + self.assertEqual(rma.refund_id.state, 'draft') + 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.uom_id, rma.product_uom) + self.assertEqual(rma.state, 'waiting_refund') + self.assertFalse(rma.can_be_refunded) + self.assertFalse(rma.can_be_returned) + self.assertFalse(rma.can_be_replaced) + rma.refund_line_id.quantity = 9 + with self.assertRaises(ValidationError): + rma.refund_id.action_invoice_open() + rma.refund_line_id.quantity = 10 + rma.refund_id.action_invoice_open() + self.assertEqual(rma.state, 'refunded') + self.assertFalse(rma.can_be_refunded) + self.assertFalse(rma.can_be_returned) + self.assertFalse(rma.can_be_replaced) + self._test_readonly_fields(rma) + + def test_mass_refund(self): + # Create, confirm and receive rma_1 + rma_1 = self._create_confirm_receive(self.partner, self.product, 10, + self.rma_loc) + # create, confirm and receive 3 more RMAs + # rma_2: Same partner and same product as rma_1 + rma_2 = self._create_confirm_receive(self.partner, self.product, 15, + self.rma_loc) + # rma_3: Same partner and different product than rma_1 + product = self.product_product.create( + {'name': 'Product 2 test', 'type': 'product'}) + rma_3 = self._create_confirm_receive(self.partner, product, 20, + self.rma_loc) + # rma_4: Different partner and same product as rma_1 + partner = self.res_partner.create({ + 'name': 'Partner 2 test', + 'property_account_receivable_id': self.account_receiv.id, + 'company_id': self.company.id, + }) + rma_4 = self._create_confirm_receive(partner, product, 25, + self.rma_loc) + # all rmas are ready to refund + all_rmas = (rma_1 | rma_2 | rma_3 | rma_4) + self.assertEqual(all_rmas.mapped('state'), ['received']*4) + self.assertEqual(all_rmas.mapped('can_be_refunded'), [True]*4) + # Mass refund of those four RMAs + action = self.env.ref('rma.rma_refund_action_server') + ctx = dict(self.env.context) + ctx.update(active_ids=all_rmas.ids, active_model='rma') + action.with_context(ctx).run() + # Two refunds were created + refund_1 = (rma_1 | rma_2 | rma_3).mapped('refund_id') + refund_2 = rma_4.refund_id + self.assertEqual(len(refund_1), 1) + self.assertEqual(len(refund_2), 1) + self.assertEqual((refund_1 | refund_2).mapped('state'), ['draft']*2) + # One refund per partner + self.assertNotEqual(refund_1.partner_id, refund_2.partner_id) + self.assertEqual( + refund_1.partner_id, + (rma_1 | rma_2 | rma_3).mapped('partner_invoice_id'), + ) + self.assertEqual(refund_2.partner_id, rma_4.partner_invoice_id) + # Each RMA (rma_1, rma_2 and rma_3) is linked with a different + # line of refund_1 + self.assertEqual(len(refund_1.invoice_line_ids), 3) + self.assertEqual( + refund_1.invoice_line_ids.mapped('rma_id'), + (rma_1 | rma_2 | rma_3), + ) + self.assertEqual( + (rma_1 | rma_2 | rma_3).mapped('refund_line_id'), + refund_1.invoice_line_ids, + ) + # rma_4 is linked with the unique line of refund_2 + self.assertEqual(len(refund_2.invoice_line_ids), 1) + self.assertEqual(refund_2.invoice_line_ids.rma_id, rma_4) + self.assertEqual(rma_4.refund_line_id, refund_2.invoice_line_ids) + # Assert product and quantities are propagated correctly + for rma in all_rmas: + self.assertEqual(rma.product_id, rma.refund_line_id.product_id) + self.assertEqual(rma.product_uom_qty, rma.refund_line_id.quantity) + self.assertEqual(rma.product_uom, rma.refund_line_id.uom_id) + # Less quantity -> error on confirm + rma_2.refund_line_id.quantity = 14 + with self.assertRaises(ValidationError): + refund_1.action_invoice_open() + rma_2.refund_line_id.quantity = 15 + refund_1.action_invoice_open() + refund_2.action_invoice_open() + self.assertEqual(all_rmas.mapped('state'), ['refunded']*4) + + def test_replace(self): + # Create, confirm and receive an RMA + rma = self._create_confirm_receive(self.partner, self.product, 10, + self.rma_loc) + # Replace with another product with quantity 2. + product_2 = self.product_product.create( + {'name': 'Product 2 test', 'type': 'product'}) + delivery_form = Form( + self.env['rma.delivery.wizard'].with_context( + active_ids=rma.ids, + rma_delivery_type='replace', + ) + ) + delivery_form.product_id = product_2 + delivery_form.product_uom_qty = 2 + delivery_wizard = delivery_form.save() + delivery_wizard.action_deliver() + self.assertEqual(len(rma.delivery_move_ids.picking_id.move_lines), 1) + self.assertEqual(rma.delivery_move_ids.product_id, product_2) + self.assertEqual(rma.delivery_move_ids.product_uom_qty, 2) + self.assertTrue(rma.delivery_move_ids.picking_id.state, 'waiting') + self.assertEqual(rma.state, 'waiting_replacement') + self.assertFalse(rma.can_be_refunded) + self.assertFalse(rma.can_be_returned) + self.assertTrue(rma.can_be_replaced) + self.assertEqual(rma.delivered_qty, 2) + self.assertEqual(rma.remaining_qty, 8) + self.assertEqual(rma.delivered_qty_done, 0) + self.assertEqual(rma.remaining_qty_to_done, 10) + first_move = rma.delivery_move_ids + picking = first_move.picking_id + # Replace again with another product with the remaining quantity + product_3 = self.product_product.create( + {'name': 'Product 3 test', 'type': 'product'}) + delivery_form = Form( + self.env['rma.delivery.wizard'].with_context( + active_ids=rma.ids, + rma_delivery_type='replace', + ) + ) + delivery_form.product_id = product_3 + delivery_wizard = delivery_form.save() + delivery_wizard.action_deliver() + second_move = rma.delivery_move_ids - first_move + self.assertEqual(len(rma.delivery_move_ids), 2) + self.assertEqual(rma.delivery_move_ids.mapped('picking_id'), picking) + self.assertEqual(first_move.product_id, product_2) + self.assertEqual(first_move.product_uom_qty, 2) + self.assertEqual(second_move.product_id, product_3) + self.assertEqual(second_move.product_uom_qty, 8) + self.assertTrue(picking.state, 'waiting') + self.assertEqual(rma.delivered_qty, 10) + self.assertEqual(rma.remaining_qty, 0) + self.assertEqual(rma.delivered_qty_done, 0) + self.assertEqual(rma.remaining_qty_to_done, 10) + # remaining_qty is 0 but rma is not set to 'replaced' until + # remaining_qty_to_done is less than or equal to 0 + first_move.quantity_done = 2 + second_move.quantity_done = 8 + picking.button_validate() + self.assertEqual(picking.state, 'done') + self.assertEqual(rma.delivered_qty, 10) + self.assertEqual(rma.remaining_qty, 0) + self.assertEqual(rma.delivered_qty_done, 10) + self.assertEqual(rma.remaining_qty_to_done, 0) + # The RMA is now in 'replaced' state + self.assertEqual(rma.state, 'replaced') + self.assertFalse(rma.can_be_refunded) + self.assertFalse(rma.can_be_returned) + # Despite being in 'replaced' state, + # RMAs can still perform replacements. + self.assertTrue(rma.can_be_replaced) + self._test_readonly_fields(rma) + + def test_return_to_customer(self): + # Create, confirm and receive an RMA + rma = self._create_confirm_receive(self.partner, self.product, 10, + self.rma_loc) + # Return the same product with quantity 2 to the customer. + delivery_form = Form( + self.env['rma.delivery.wizard'].with_context( + active_ids=rma.ids, + rma_delivery_type='return', + ) + ) + delivery_form.product_uom_qty = 2 + delivery_wizard = delivery_form.save() + delivery_wizard.action_deliver() + picking = rma.delivery_move_ids.picking_id + self.assertEqual(len(picking.move_lines), 1) + self.assertEqual(rma.delivery_move_ids.product_id, self.product) + self.assertEqual(rma.delivery_move_ids.product_uom_qty, 2) + self.assertTrue(picking.state, 'waiting') + self.assertEqual(rma.state, 'waiting_return') + self.assertFalse(rma.can_be_refunded) + self.assertFalse(rma.can_be_replaced) + self.assertTrue(rma.can_be_returned) + self.assertEqual(rma.delivered_qty, 2) + self.assertEqual(rma.remaining_qty, 8) + self.assertEqual(rma.delivered_qty_done, 0) + self.assertEqual(rma.remaining_qty_to_done, 10) + first_move = rma.delivery_move_ids + picking = first_move.picking_id + # Validate the picking + first_move.quantity_done = 2 + picking.button_validate() + self.assertEqual(picking.state, 'done') + self.assertEqual(rma.delivered_qty, 2) + self.assertEqual(rma.remaining_qty, 8) + self.assertEqual(rma.delivered_qty_done, 2) + self.assertEqual(rma.remaining_qty_to_done, 8) + # Return the remaining quantity to the customer + delivery_form = Form( + self.env['rma.delivery.wizard'].with_context( + active_ids=rma.ids, + rma_delivery_type='return', + ) + ) + delivery_wizard = delivery_form.save() + delivery_wizard.action_deliver() + second_move = rma.delivery_move_ids - first_move + second_move.quantity_done = 8 + self.assertEqual(rma.delivered_qty, 10) + self.assertEqual(rma.remaining_qty, 0) + self.assertEqual(rma.delivered_qty_done, 2) + self.assertEqual(rma.remaining_qty_to_done, 8) + self.assertEqual(rma.state, 'waiting_return') + # remaining_qty is 0 but rma is not set to 'returned' until + # remaining_qty_to_done is less than or equal to 0 + picking_2 = second_move.picking_id + picking_2.button_validate() + self.assertEqual(picking_2.state, 'done') + self.assertEqual(rma.delivered_qty, 10) + self.assertEqual(rma.remaining_qty, 0) + self.assertEqual(rma.delivered_qty_done, 10) + self.assertEqual(rma.remaining_qty_to_done, 0) + # The RMA is now in 'returned' state + self.assertEqual(rma.state, 'returned') + self.assertFalse(rma.can_be_refunded) + self.assertFalse(rma.can_be_returned) + self.assertFalse(rma.can_be_replaced) + self._test_readonly_fields(rma) + + def test_mass_return_to_customer(self): + # Create, confirm and receive rma_1 + rma_1 = self._create_confirm_receive(self.partner, self.product, 10, + self.rma_loc) + # create, confirm and receive 3 more RMAs + # rma_2: Same partner and same product as rma_1 + rma_2 = self._create_confirm_receive(self.partner, self.product, 15, + self.rma_loc) + # rma_3: Same partner and different product than rma_1 + product = self.product_product.create( + {'name': 'Product 2 test', 'type': 'product'}) + rma_3 = self._create_confirm_receive(self.partner, product, 20, + self.rma_loc) + # rma_4: Different partner and same product as rma_1 + partner = self.res_partner.create({'name': 'Partner 2 test'}) + rma_4 = self._create_confirm_receive(partner, product, 25, + self.rma_loc) + # all rmas are ready to be returned to the customer + all_rmas = (rma_1 | rma_2 | rma_3 | rma_4) + self.assertEqual(all_rmas.mapped('state'), ['received'] * 4) + self.assertEqual(all_rmas.mapped('can_be_returned'), [True] * 4) + # Mass return of those four RMAs + delivery_wizard = self.env['rma.delivery.wizard'].with_context( + active_ids=all_rmas.ids, rma_delivery_type='return').create({}) + delivery_wizard.action_deliver() + # Two pickings were created + pick_1 = (rma_1 | rma_2 | rma_3).mapped('delivery_move_ids.picking_id') + pick_2 = rma_4.delivery_move_ids.picking_id + self.assertEqual(len(pick_1), 1) + self.assertEqual(len(pick_2), 1) + self.assertNotEqual(pick_1, pick_2) + self.assertEqual((pick_1 | pick_2).mapped('state'), ['assigned'] * 2) + # One picking per partner + self.assertNotEqual(pick_1.partner_id, pick_2.partner_id) + self.assertEqual( + pick_1.partner_id, + (rma_1 | rma_2 | rma_3).mapped('partner_id'), + ) + self.assertEqual(pick_2.partner_id, rma_4.partner_id) + # Each RMA of (rma_1, rma_2 and rma_3) is linked to a different + # line of picking_1 + self.assertEqual(len(pick_1.move_lines), 3) + self.assertEqual( + pick_1.move_lines.mapped('rma_id'), + (rma_1 | rma_2 | rma_3), + ) + self.assertEqual( + (rma_1 | rma_2 | rma_3).mapped('delivery_move_ids'), + pick_1.move_lines, + ) + # rma_4 is linked with the unique move of pick_2 + self.assertEqual(len(pick_2.move_lines), 1) + self.assertEqual(pick_2.move_lines.rma_id, rma_4) + self.assertEqual(rma_4.delivery_move_ids, pick_2.move_lines) + # Assert product and quantities are propagated correctly + for rma in all_rmas: + self.assertEqual(rma.product_id, rma.delivery_move_ids.product_id) + self.assertEqual(rma.product_uom_qty, + rma.delivery_move_ids.product_uom_qty) + self.assertEqual(rma.product_uom, + rma.delivery_move_ids.product_uom) + rma.delivery_move_ids.quantity_done = rma.product_uom_qty + pick_1.button_validate() + pick_2.button_validate() + self.assertEqual(all_rmas.mapped('state'), ['returned'] * 4) + + def test_rma_from_picking_return(self): + # Create a return from a delivery picking + origin_delivery = self._create_delivery() + return_wizard = self.env['stock.return.picking'].with_context( + active_id=origin_delivery.id, + active_ids=origin_delivery.ids, + ).create({'create_rma': True}) + picking_action = return_wizard.create_returns() + # Each origin move is linked to a different RMA + origin_moves = origin_delivery.move_lines + self.assertTrue(origin_moves[0].rma_ids) + self.assertTrue(origin_moves[1].rma_ids) + rmas = origin_moves.mapped('rma_ids') + self.assertEqual(rmas.mapped('state'), ['confirmed']*2) + # Each reception move is linked one of the generated RMAs + reception = self.env['stock.picking'].browse(picking_action['res_id']) + reception_moves = reception.move_lines + self.assertTrue(reception_moves[0].rma_receiver_ids) + self.assertTrue(reception_moves[1].rma_receiver_ids) + self.assertEqual(reception_moves.mapped('rma_receiver_ids'), rmas) + # Validate the reception picking to set rmas to 'received' state + reception_moves[0].quantity_done = reception_moves[0].product_uom_qty + reception_moves[1].quantity_done = reception_moves[1].product_uom_qty + reception.button_validate() + self.assertEqual(rmas.mapped('state'), ['received'] * 2) + + def test_split(self): + origin_delivery = self._create_delivery() + rma_form = Form(self.env['rma']) + rma_form.partner_id = self.partner + rma_form.picking_id = origin_delivery + rma_form.move_id = origin_delivery.move_lines.filtered( + lambda r: r.product_id == self.product) + rma = rma_form.save() + rma.action_confirm() + rma.reception_move_id.quantity_done = 10 + rma.reception_move_id.picking_id.action_done() + # Return quantity 4 of the same product to the customer + delivery_form = Form( + self.env['rma.delivery.wizard'].with_context( + active_ids=rma.ids, + rma_delivery_type='return', + ) + ) + delivery_form.product_uom_qty = 4 + delivery_wizard = delivery_form.save() + delivery_wizard.action_deliver() + rma.delivery_move_ids.quantity_done = 4 + rma.delivery_move_ids.picking_id.button_validate() + self.assertEqual(rma.state, 'waiting_return') + # Extract the remaining quantity to another RMA + self.assertTrue(rma.can_be_split) + split_wizard = self.env['rma.split.wizard'].with_context( + active_id=rma.id, + active_ids=rma.ids, + ).create({}) + action = split_wizard.action_split() + # Check rma is set to 'returned' after split. Check new_rma values + self.assertEqual(rma.state, 'returned') + new_rma = self.env['rma'].browse(action['res_id']) + self.assertEqual(new_rma.origin_split_rma_id, rma) + self.assertEqual(new_rma.delivered_qty, 0) + self.assertEqual(new_rma.remaining_qty, 6) + self.assertEqual(new_rma.delivered_qty_done, 0) + self.assertEqual(new_rma.remaining_qty_to_done, 6) + self.assertEqual(new_rma.state, 'received') + self.assertTrue(new_rma.can_be_refunded) + self.assertTrue(new_rma.can_be_returned) + self.assertTrue(new_rma.can_be_replaced) + self.assertEqual(new_rma.move_id, rma.move_id) + self.assertEqual(new_rma.reception_move_id, rma.reception_move_id) + self.assertEqual(new_rma.product_uom_qty + rma.product_uom_qty, 10) + self.assertEqual(new_rma.move_id.quantity_done, 10) + self.assertEqual(new_rma.reception_move_id.quantity_done, 10) diff --git a/rma/views/menus.xml b/rma/views/menus.xml new file mode 100644 index 00000000..568c906d --- /dev/null +++ b/rma/views/menus.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/rma/views/report_rma.xml b/rma/views/report_rma.xml new file mode 100644 index 00000000..c6320d0d --- /dev/null +++ b/rma/views/report_rma.xml @@ -0,0 +1,96 @@ + + + + + diff --git a/rma/views/res_partner_views.xml b/rma/views/res_partner_views.xml new file mode 100644 index 00000000..d783f1f6 --- /dev/null +++ b/rma/views/res_partner_views.xml @@ -0,0 +1,23 @@ + + + + + res.partner.form + res.partner + + +
+ +
+
+
+
diff --git a/rma/views/rma_portal_templates.xml b/rma/views/rma_portal_templates.xml new file mode 100644 index 00000000..16cff15b --- /dev/null +++ b/rma/views/rma_portal_templates.xml @@ -0,0 +1,269 @@ + + + + + + + + + + + diff --git a/rma/views/rma_team_views.xml b/rma/views/rma_team_views.xml new file mode 100644 index 00000000..328e1ee6 --- /dev/null +++ b/rma/views/rma_team_views.xml @@ -0,0 +1,90 @@ + + + + + rma.team.view.form + rma.team + +
+ +
+ +
+
+
+ + + + + + + + + + + + + + +
+
+ Avatar +
+ +
+
+
+
+
+
+
+
+ + + + +
+
+
+ + +
+
+
+
+ + + RMA team + rma.team + form + tree,form + +

+ Click to add a new RMA. +

+
+
+ + +
diff --git a/rma/views/rma_views.xml b/rma/views/rma_views.xml new file mode 100644 index 00000000..c86d14c2 --- /dev/null +++ b/rma/views/rma_views.xml @@ -0,0 +1,285 @@ + + + + + rma.view.search + rma + + + + + + + + + + + + + + + + + + + + + + rma.view.tree + rma + + + + + + + + + + + + + + + + + + rma.view.form + rma + +
+
+
+ +
+ + + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + rma.pivot + rma + + + + + + + + + + + rma.calendar + rma + + + + + + + + + + + + To Refund + + + code + records.action_refund() + + + + RMA + rma + form + tree,form,pivot,calendar + {"search_default_user_id": uid} + +

+ Click to add a new RMA. +

+
+
+ + + + +
diff --git a/rma/views/stock_picking_views.xml b/rma/views/stock_picking_views.xml new file mode 100644 index 00000000..d1f75400 --- /dev/null +++ b/rma/views/stock_picking_views.xml @@ -0,0 +1,23 @@ + + + + + stock.picking.form + stock.picking + + +
+ +
+
+
+
diff --git a/rma/views/stock_warehouse_views.xml b/rma/views/stock_warehouse_views.xml new file mode 100644 index 00000000..2371182e --- /dev/null +++ b/rma/views/stock_warehouse_views.xml @@ -0,0 +1,17 @@ + + + + Stock Warehouse Inherit MRP + stock.warehouse + + + + + + + + + + + + diff --git a/rma/wizard/__init__.py b/rma/wizard/__init__.py new file mode 100644 index 00000000..b81a7402 --- /dev/null +++ b/rma/wizard/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import rma_delivery +from . import rma_split +from . import stock_picking_return diff --git a/rma/wizard/rma_delivery.py b/rma/wizard/rma_delivery.py new file mode 100644 index 00000000..ef72053b --- /dev/null +++ b/rma/wizard/rma_delivery.py @@ -0,0 +1,102 @@ +# 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 +import odoo.addons.decimal_precision as dp + + +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'), + ], + string="Type", + required=True, + ) + product_id = fields.Many2one( + comodel_name="product.product", + string="Replace Product", + ) + product_uom_qty = fields.Float( + string='Product qty', + digits=dp.get_precision('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, + ) + + @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): + domain_product_uom = [] + if self.product_id: + domain_product_uom = [ + ('category_id', '=', self.product_id.uom_id.category_id.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 + return {'domain': {'product_uom': domain_product_uom}} + + 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.create_return(self.scheduled_date, qty, uom) diff --git a/rma/wizard/rma_delivery_views.xml b/rma/wizard/rma_delivery_views.xml new file mode 100644 index 00000000..2efcbc05 --- /dev/null +++ b/rma/wizard/rma_delivery_views.xml @@ -0,0 +1,50 @@ + + + + + rma.delivery.wizard.form + rma.delivery.wizard + +
+ + + + + + + + + + + +
+
+ +
+
+ +
diff --git a/rma/wizard/rma_split.py b/rma/wizard/rma_split.py new file mode 100644 index 00000000..fc8ea850 --- /dev/null +++ b/rma/wizard/rma_split.py @@ -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 +import odoo.addons.decimal_precision as dp + + +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=dp.get_precision('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, + } diff --git a/rma/wizard/rma_split_views.xml b/rma/wizard/rma_split_views.xml new file mode 100644 index 00000000..d33ba877 --- /dev/null +++ b/rma/wizard/rma_split_views.xml @@ -0,0 +1,33 @@ + + + + + rma.split.wizard.form + rma.split.wizard + +
+ + + + +
+
+
+
+
+ + Split RMA + rma.split.wizard + form + form + new + +
diff --git a/rma/wizard/stock_picking_return.py b/rma/wizard/stock_picking_return.py new file mode 100644 index 00000000..0608c341 --- /dev/null +++ b/rma/wizard/stock_picking_return.py @@ -0,0 +1,83 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ReturnPicking(models.TransientModel): + _inherit = 'stock.return.picking' + + create_rma = fields.Boolean( + string="Create RMAs" + ) + picking_type_code = fields.Selection( + selection=[ + ('incoming', 'Vendors'), + ('outgoing', 'Customers'), + ('internal', 'Internal'), + ], + related='picking_id.picking_type_id.code', + store=True, + readonly=True, + ) + + @api.onchange("create_rma") + def _onchange_create_rma(self): + if self.create_rma: + warehouse = self.picking_id.picking_type_id.warehouse_id + self.location_id = warehouse.rma_loc_id.id + rma_loc = warehouse.search([]).mapped('rma_loc_id') + rma_loc_domain = [('id', 'child_of', rma_loc.ids)] + else: + self.location_id = self.default_get(['location_id'])['location_id'] + rma_loc_domain = [ + '|', + ('id', '=', self.picking_id.location_id.id), + ('return_location', '=', True), + ] + return {'domain': {'location_id': rma_loc_domain}} + + def create_returns(self): + """ Override create_returns method for creating one or more + 'confirmed' RMAs after return a delivery picking in case + 'Create RMAs' checkbox is checked in this wizard. + New RMAs will be linked to the delivery picking as the origin + delivery and also RMAs will be linked to the returned picking + as the 'Receipt'. + """ + if self.create_rma: + # set_rma_picking_type is to override the copy() method of stock + # picking and change the default picking type to rma picking type + self_with_context = self.with_context(set_rma_picking_type=True) + res = super(ReturnPicking, self_with_context).create_returns() + partner = self.picking_id.partner_id + if not partner: + raise ValidationError(_( + "You must specify the 'Customer' in the " + "'Stock Picking' from which RMAs will be created")) + picking = self.picking_id + returned_picking = self.env['stock.picking'].browse(res['res_id']) + if hasattr(picking, 'sale_id') and picking.sale_id: + partner_invoice_id = picking.sale_id.partner_invoice_id.id + else: + partner_invoice_id = partner.address_get( + ['invoice']).get('invoice', False), + for move in returned_picking.move_lines: + self.env['rma'].create({ + 'partner_id': partner.id, + 'partner_invoice_id': partner_invoice_id, + 'origin': picking.name, + 'picking_id': picking.id, + 'move_id': move.origin_returned_move_id.id, + 'product_id': move.origin_returned_move_id.product_id.id, + 'product_uom_qty': move.product_uom_qty, + 'product_uom': move.product_uom.id, + 'reception_move_id': move.id, + 'company_id': move.company_id.id, + 'location_id': move.location_dest_id.id, + 'state': 'confirmed', + }) + return res + else: + return super().create_returns() diff --git a/rma/wizard/stock_picking_return_views.xml b/rma/wizard/stock_picking_return_views.xml new file mode 100644 index 00000000..87b8f8ed --- /dev/null +++ b/rma/wizard/stock_picking_return_views.xml @@ -0,0 +1,20 @@ + + + + + Return lines inherit RMA + stock.return.picking + + + + + + + + + + + +