diff --git a/rma/models/account_invoice.py b/rma/models/account_invoice.py
index b3e86196..52a2f703 100644
--- a/rma/models/account_invoice.py
+++ b/rma/models/account_invoice.py
@@ -9,15 +9,20 @@ from odoo.tools import float_compare
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
+ def _check_rma_invoice_lines_qty(self):
+ """We can't refund a different qty than the stated in the RMA.
+ Extend to change criteria """
+ precision = self.env["decimal.precision"].precision_get(
+ "Product Unit of Measure")
+ return self.sudo().mapped("invoice_line_ids").filtered(
+ lambda r: (r.rma_id and float_compare(
+ r.quantity, r.rma_id.product_uom_qty, precision) < 0))
+
def action_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.sudo().mapped('invoice_line_ids').filtered(
- lambda r: (r.rma_id and float_compare(
- r.quantity, r.rma_id.product_uom_qty, precision) < 0)):
+ if self._check_rma_invoice_lines_qty():
raise ValidationError(
_("There is at least one invoice lines whose quantity is "
"less than the quantity specified in its linked RMA."))
diff --git a/rma/models/rma.py b/rma/models/rma.py
index 0b21d868..fbc061f2 100644
--- a/rma/models/rma.py
+++ b/rma/models/rma.py
@@ -611,12 +611,14 @@ class Rma(models.Model):
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_vals.update(rma._get_extra_refund_line_vals())
line = self.env['account.invoice.line'].create(line_vals)
rma.write({
'refund_line_id': line.id,
'refund_id': refund.id,
'state': 'refunded',
})
+ refund.compute_taxes()
refund.message_post_with_view(
'mail.message_origin_link',
values={'self': refund, 'origin': rmas},
@@ -952,16 +954,32 @@ class Rma(models.Model):
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
+ product = self._get_refund_line_product()
+ qty, uom = self._get_refund_line_quantity()
+ line_form.product_id = product
+ line_form.quantity = qty
+ line_form.uom_id = uom
line_form.price_unit = self._get_refund_line_price_unit()
+ def _get_refund_line_product(self):
+ """To be overriden in a third module with the proper origin values
+ in case a kit is linked with the rma"""
+ return self.product_id
+
+ def _get_refund_line_quantity(self):
+ """To be overriden in a third module with the proper origin values
+ in case a kit is linked with the rma """
+ return (self.product_uom_qty, self.product_uom)
+
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
+ def _get_extra_refund_line_vals(self):
+ """Override to write aditional stuff into the refund line"""
+ return {}
+
# Returning business methods
def create_return(self, scheduled_date, qty=None, uom=None):
"""Intended to be invoked by the delivery wizard"""
diff --git a/rma/views/rma_portal_templates.xml b/rma/views/rma_portal_templates.xml
index 33b6e98e..c70e1c37 100644
--- a/rma/views/rma_portal_templates.xml
+++ b/rma/views/rma_portal_templates.xml
@@ -36,7 +36,7 @@
RMA #
Date
- Product
+ Product
Quantity
Status
@@ -51,7 +51,7 @@
-
+
diff --git a/rma/views/rma_views.xml b/rma/views/rma_views.xml
index bec30fbd..5ce46b8c 100644
--- a/rma/views/rma_views.xml
+++ b/rma/views/rma_views.xml
@@ -283,7 +283,7 @@
rma
form
tree,form,pivot,calendar,activity
- {"search_default_user_id": uid}
+ {}
Click to add a new RMA.
diff --git a/rma_sale/controllers/sale_portal.py b/rma_sale/controllers/sale_portal.py
index d63fe6da..5c77c0c9 100644
--- a/rma_sale/controllers/sale_portal.py
+++ b/rma_sale/controllers/sale_portal.py
@@ -36,7 +36,7 @@ class CustomerPortal(CustomerPortal):
'location_id': location_id,
'partner_shipping_id': partner_shipping_id,
})
- rma = wizard.sudo().create_rma()
+ rma = wizard.sudo().create_rma(from_portal=True)
for rec in rma:
rec.origin += _(' (Portal)')
# Add the user as follower of the created RMAs so they can
diff --git a/rma_sale/models/rma.py b/rma_sale/models/rma.py
index c3a947fc..4fdda6eb 100644
--- a/rma_sale/models/rma.py
+++ b/rma_sale/models/rma.py
@@ -31,6 +31,9 @@ class Rma(models.Model):
move_id = fields.Many2one(
domain="[('id', 'in', allowed_move_ids)]",
)
+ sale_line_id = fields.Many2one(
+ related="move_id.sale_line_id",
+ )
allowed_product_ids = fields.Many2many(
comodel_name='product.product',
compute="_compute_allowed_product_ids",
@@ -88,3 +91,34 @@ class Rma(models.Model):
if self.order_id:
invoice_form.user_id = self.order_id.user_id
return res
+
+ def _get_refund_line_price_unit(self):
+ """Get the sale order price unit"""
+ if self.sale_line_id:
+ return self.sale_line_id.price_unit
+ return super()._get_refund_line_price_unit()
+
+ def _get_refund_line_product(self):
+ """To be overriden in a third module with the proper origin values
+ in case a kit is linked with the rma """
+ if not self.sale_line_id:
+ return super()._get_refund_line_product()
+ return self.sale_line_id.product_id
+
+ def _prepare_refund_line(self, line_form):
+ """Add line data"""
+ super()._prepare_refund_line(line_form)
+ line = self.sale_line_id
+ if line:
+ line_form.discount = line.discount
+
+ def _get_extra_refund_line_vals(self):
+ """Link sale line"""
+ self.ensure_one()
+ vals = super()._get_extra_refund_line_vals()
+ line = self.sale_line_id
+ if line:
+ vals.update({
+ "sequence": line.sequence,
+ })
+ return vals
diff --git a/rma_sale/models/sale.py b/rma_sale/models/sale.py
index 28ff0b14..795efe48 100644
--- a/rma_sale/models/sale.py
+++ b/rma_sale/models/sale.py
@@ -1,7 +1,7 @@
# Copyright 2020 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-from odoo import _, fields, models
+from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
@@ -28,19 +28,25 @@ class SaleOrder(models.Model):
for record in self:
record.rma_count = mapped_data.get(record.id, 0)
+ def _prepare_rma_wizard_line_vals(self, data):
+ """So we can extend the wizard easily"""
+ return {
+ 'product_id': data['product'].id,
+ 'quantity': data['quantity'],
+ 'sale_line_id': data['sale_line_id'].id,
+ 'uom_id': data['uom'].id,
+ 'picking_id': data['picking'] and data['picking'].id,
+ }
+
def action_create_rma(self):
self.ensure_one()
if self.state not in ['sale', 'done']:
raise ValidationError(_("You may only create RMAs from a "
"confirmed or done sale order."))
wizard_obj = self.env['sale.order.rma.wizard']
- line_vals = [(0, 0, {
- 'product_id': data['product'].id,
- 'quantity': data['quantity'],
- 'sale_line_id': data['sale_line_id'].id,
- 'uom_id': data['uom'].id,
- 'picking_id': data['picking'] and data['picking'].id,
- }) for data in self.get_delivery_rma_data()]
+ line_vals = [
+ (0, 0, self._prepare_rma_wizard_line_vals(data))
+ for data in self.get_delivery_rma_data()]
wizard = wizard_obj.with_context(active_id=self.id).create({
'line_ids': line_vals,
'location_id': self.warehouse_id.rma_loc_id.id
@@ -78,6 +84,19 @@ class SaleOrder(models.Model):
data += line.prepare_sale_rma_data()
return data
+ @api.depends("rma_ids.refund_id")
+ def _get_invoiced(self):
+ """Search for possible RMA refunds and link them to the order. We
+ don't want to link their sale lines as that would unbalance the
+ qtys to invoice wich isn't correct for this case"""
+ super()._get_invoiced()
+ for order in self:
+ refunds = order.sudo().rma_ids.mapped("refund_id")
+ if not refunds:
+ continue
+ order.invoice_ids += refunds
+ order.invoice_count = len(order.invoice_ids)
+
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
@@ -96,7 +115,7 @@ class SaleOrderLine(models.Model):
def prepare_sale_rma_data(self):
self.ensure_one()
product = self.product_id
- if self.product_id.type != 'product':
+ if self.product_id.type not in ['product', 'consu']:
return {}
moves = self.get_delivery_move()
data = []
diff --git a/rma_sale/views/sale_portal_template.xml b/rma_sale/views/sale_portal_template.xml
index a1f0027e..9e7addc9 100644
--- a/rma_sale/views/sale_portal_template.xml
+++ b/rma_sale/views/sale_portal_template.xml
@@ -96,10 +96,10 @@
-
+
+ t-att-value="data['picking'] and data['picking'].id"/>
new
-
+
sale.order.rma.wizard.form
sale.order.rma.wizard
diff --git a/rma_sale_mrp/README.rst b/rma_sale_mrp/README.rst
new file mode 100644
index 00000000..1be2d464
--- /dev/null
+++ b/rma_sale_mrp/README.rst
@@ -0,0 +1,119 @@
+================================================================
+Return Merchandise Authorization Management - Link with MRP Kits
+================================================================
+
+.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! 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_sale_mrp
+ :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_sale_mrp
+ :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 enables RMAs for kits, wich isn't compatible with the base modules.
+In the backend side, we can return separate component while in the frontend
+side, customers can return the whole kit and the proper RMAs will be generated.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Usage
+=====
+
+To use this module, you need to:
+
+#. Make a a sale order with a kit on it and deliver its components.
+#. Go to the portal view for the order and launch the RMA wizard.
+#. You'll see a line for the kit.
+#. There will be a limit of kits to return that should much the number of kits
+ delivered.
+#. Once you validate the wizard with the number of kits to deliver, you'll
+ have as many RMAs as components those kits have with the proper quantities
+ for each one.
+#. If you refund the components, the kit in the sale line will be used as the
+ reference.
+
+Known issues / Roadmap
+======================
+
+We compute the kits from the original demanded quantity in the sale order. If
+this quantity was to change, we could loose the right components per kit
+reference. So this should be very present. Also, v12 has a very poor support
+for delivered quantities, that is very improved in v13 with the introduction
+of the link to the BoM line in the stock moves. That approach could lead to
+errors as well, as the BoM line could change in the future loosing again the
+original components per kit reference. Anyway, is to be considered in that
+version to use the same rules so they fail for the same reasons.
+
+Some extra features would be nice to have:
+
+* Add actions constraints to disallow actions on single components.
+* Show kit components in the portal wizard.
+* Allow to make an RMA directly from a kit product.
+
+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 `__:
+
+ * 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-chienandalu| image:: https://github.com/chienandalu.png?size=40px
+ :target: https://github.com/chienandalu
+ :alt: chienandalu
+
+Current `maintainer `__:
+
+|maintainer-chienandalu|
+
+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_sale_mrp/__init__.py b/rma_sale_mrp/__init__.py
new file mode 100644
index 00000000..9b429614
--- /dev/null
+++ b/rma_sale_mrp/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizard
diff --git a/rma_sale_mrp/__manifest__.py b/rma_sale_mrp/__manifest__.py
new file mode 100644
index 00000000..acf2ce45
--- /dev/null
+++ b/rma_sale_mrp/__manifest__.py
@@ -0,0 +1,23 @@
+# Copyright 2020 Tecnativa - David Vidal
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+{
+ "name": "Return Merchandise Authorization Management - Link with MRP Kits",
+ "summary": "Allow doing RMAs from MRP kits",
+ "version": "12.0.1.0.0",
+ "development_status": "Beta",
+ "category": "RMA",
+ "website": "https://github.com/OCA/rma",
+ "author": "Tecnativa, Odoo Community Association (OCA)",
+ "maintainers": ["chienandalu"],
+ "license": "AGPL-3",
+ "depends": [
+ "rma_sale",
+ "mrp",
+ ],
+ "data": [
+ "views/sale_order_portal_template.xml",
+ "views/rma_views.xml",
+ "views/report_rma.xml",
+ "wizard/sale_order_rma_wizard_views.xml",
+ ],
+}
diff --git a/rma_sale_mrp/i18n/es.po b/rma_sale_mrp/i18n/es.po
new file mode 100644
index 00000000..1e280efe
--- /dev/null
+++ b/rma_sale_mrp/i18n/es.po
@@ -0,0 +1,249 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * rma_sale_mrp
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 12.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-01-22 12:23+0000\n"
+"PO-Revision-Date: 2021-01-22 14:44+0100\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: \n"
+"Language: es\n"
+"X-Generator: Poedit 2.3\n"
+
+#. module: rma_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
+msgid " Cancelled "
+msgstr " Cancelado "
+
+#. module: rma_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
+msgid " Draft "
+msgstr " Borrador "
+
+#. module: rma_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
+msgid " Waiting "
+msgstr " En espera "
+
+#. module: rma_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
+msgid "Related Kit Components RMAs "
+msgstr "RMAs relacionados de los componentes del kit "
+
+#. module: rma_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
+msgid " Kit Quantity "
+msgstr " Ctd. de kits "
+
+#. module: rma_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
+msgid " Kit "
+msgstr " Kit "
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__allowed_picking_ids
+msgid "Allowed Picking"
+msgstr "Albaranes permitidos"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__allowed_product_ids
+msgid "Allowed Product"
+msgstr "Producto Permitido"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__uom_category_id
+msgid "Category"
+msgstr "Categoría"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_rma_wizard__component_line_ids
+msgid "Component Lines"
+msgstr "Líneas de componentes"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,help:rma_sale_mrp.field_sale_order_line_rma_wizard_component__uom_category_id
+msgid "Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios."
+msgstr "La conversión entre las unidades de medidas sólo pueden ocurrir si pertenecen a la misma categoría. La conversión se basará en los ratios establecidos."
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__create_uid
+msgid "Created by"
+msgstr "Creado por"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__create_date
+msgid "Created on"
+msgstr "Creado el"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__picking_id
+msgid "Delivery order"
+msgstr "Orden de entrega"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__description
+msgid "Description"
+msgstr "Descripción"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__display_name
+msgid "Display Name"
+msgstr "Nombre mostrado"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__id
+msgid "ID"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_my_rmas
+msgid "Kit"
+msgstr "Kit"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard__kit_qty_done
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__kit_qty_done
+msgid "Kit Qty Done"
+msgstr "Ctd hecha de kit"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_rma__kit_qty
+msgid "Kit quantity"
+msgstr "Cantidad de kit"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component____last_update
+msgid "Last Modified on"
+msgstr "Última modificación en"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__write_uid
+msgid "Last Updated by"
+msgstr "Última actualización por"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__write_date
+msgid "Last Updated on"
+msgstr "Última actualización el"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__move_id
+msgid "Move"
+msgstr "Movimiento"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__order_id
+msgid "Order"
+msgstr "Pedido"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard__per_kit_quantity
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__per_kit_quantity
+msgid "Per Kit Quantity"
+msgstr "Cantidad por kit"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard__phantom_bom_product
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__phantom_bom_product
+msgid "Phantom Bom Product"
+msgstr "Producto Kit"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard__phantom_kit_line
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__phantom_kit_line
+msgid "Phantom Kit Line"
+msgstr "Línea de kit"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__product_id
+msgid "Product"
+msgstr "Producto"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__quantity
+msgid "Quantity"
+msgstr "Cantidad"
+
+#. module: rma_sale_mrp
+#: model:ir.model,name:rma_sale_mrp.model_rma
+msgid "RMA"
+msgstr "RMA"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_rma__phantom_bom_product
+msgid "Related kit product"
+msgstr "Product kit relacionado"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__operation_id
+msgid "Requested operation"
+msgstr "Operación solicitada"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_rma__rma_kit_register
+msgid "Rma Kit Register"
+msgstr "Registro de RMA del kit"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__sale_line_id
+msgid "Sale Line"
+msgstr "Línea de venta"
+
+#. module: rma_sale_mrp
+#: model:ir.model,name:rma_sale_mrp.model_sale_order
+msgid "Sale Order"
+msgstr "Pedido de venta"
+
+#. module: rma_sale_mrp
+#: model:ir.model,name:rma_sale_mrp.model_sale_order_line_rma_wizard
+msgid "Sale Order Line Rma Wizard"
+msgstr "Asistente de RMA"
+
+#. module: rma_sale_mrp
+#: model:ir.model,name:rma_sale_mrp.model_sale_order_rma_wizard
+msgid "Sale Order Rma Wizard"
+msgstr "Asistente de Orden de Venta - RMA"
+
+#. module: rma_sale_mrp
+#: model:ir.model,name:rma_sale_mrp.model_sale_order_line
+msgid "Sales Order Line"
+msgstr "Línea de pedido de venta"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,help:rma_sale_mrp.field_rma__kit_qty
+msgid "To how many kits this components corresponds to. Used mainly for refunding the right quantity"
+msgstr "A cuántos kits corresponde este componente. Utilizado principalmente para reembolsar la cantidad correcta"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__uom_id
+msgid "Unit of Measure"
+msgstr "Unidad de medida"
+
+#. module: rma_sale_mrp
+#: model:ir.model,name:rma_sale_mrp.model_sale_order_line_rma_wizard_component
+msgid "Used to hide kit components in the wizards"
+msgstr "Utilizado para ocultar los componentes en los asistentes"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,help:rma_sale_mrp.field_sale_order_line_rma_wizard__kit_qty_done
+#: model:ir.model.fields,help:rma_sale_mrp.field_sale_order_line_rma_wizard_component__kit_qty_done
+msgid "Used to inform kit qty used in the rma. Will be useful to refund"
+msgstr "Utilizado para informar la cantidad de kits utilizados en el RMA. Resultará útil al reembolsar"
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__wizard_id
+msgid "Wizard"
+msgstr "Asistente"
+
+#. module: rma_sale_mrp
+#: code:addons/rma_sale_mrp/models/rma.py:49
+#, python-format
+msgid "You can't refund a kit in wich some RMAs aren't received"
+msgstr "No puedes reembolsar un kit cuyos RMAs no están en estado `recibido`"
diff --git a/rma_sale_mrp/i18n/rma_sale_mrp.pot b/rma_sale_mrp/i18n/rma_sale_mrp.pot
new file mode 100644
index 00000000..1c357803
--- /dev/null
+++ b/rma_sale_mrp/i18n/rma_sale_mrp.pot
@@ -0,0 +1,248 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * rma_sale_mrp
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 12.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-01-22 12:20+0000\n"
+"PO-Revision-Date: 2021-01-22 12:20+0000\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_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
+msgid " Cancelled "
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
+msgid " Draft "
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
+msgid " Waiting "
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
+msgid "Related Kit Components RMAs "
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
+msgid " Kit Quantity "
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
+msgid " Kit "
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__allowed_picking_ids
+msgid "Allowed Picking"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__allowed_product_ids
+msgid "Allowed Product"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__uom_category_id
+msgid "Category"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_rma_wizard__component_line_ids
+msgid "Component Lines"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,help:rma_sale_mrp.field_sale_order_line_rma_wizard_component__uom_category_id
+msgid "Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios."
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__create_date
+msgid "Created on"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__picking_id
+msgid "Delivery order"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__description
+msgid "Description"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__id
+msgid "ID"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_my_rmas
+msgid "Kit"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard__kit_qty_done
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__kit_qty_done
+msgid "Kit Qty Done"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_rma__kit_qty
+msgid "Kit quantity"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__move_id
+msgid "Move"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__order_id
+msgid "Order"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard__per_kit_quantity
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__per_kit_quantity
+msgid "Per Kit Quantity"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard__phantom_bom_product
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__phantom_bom_product
+msgid "Phantom Bom Product"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard__phantom_kit_line
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__phantom_kit_line
+msgid "Phantom Kit Line"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__product_id
+msgid "Product"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__quantity
+msgid "Quantity"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model,name:rma_sale_mrp.model_rma
+msgid "RMA"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_rma__phantom_bom_product
+msgid "Related kit product"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__operation_id
+msgid "Requested operation"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_rma__rma_kit_register
+msgid "Rma Kit Register"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__sale_line_id
+msgid "Sale Line"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model,name:rma_sale_mrp.model_sale_order
+msgid "Sale Order"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model,name:rma_sale_mrp.model_sale_order_line_rma_wizard
+msgid "Sale Order Line Rma Wizard"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model,name:rma_sale_mrp.model_sale_order_rma_wizard
+msgid "Sale Order Rma Wizard"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model,name:rma_sale_mrp.model_sale_order_line
+msgid "Sales Order Line"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,help:rma_sale_mrp.field_rma__kit_qty
+msgid "To how many kits this components corresponds to. Used mainly for refunding the right quantity"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__uom_id
+msgid "Unit of Measure"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model,name:rma_sale_mrp.model_sale_order_line_rma_wizard_component
+msgid "Used to hide kit components in the wizards"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,help:rma_sale_mrp.field_sale_order_line_rma_wizard__kit_qty_done
+#: model:ir.model.fields,help:rma_sale_mrp.field_sale_order_line_rma_wizard_component__kit_qty_done
+msgid "Used to inform kit qty used in the rma. Will be useful to refund"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: model:ir.model.fields,field_description:rma_sale_mrp.field_sale_order_line_rma_wizard_component__wizard_id
+msgid "Wizard"
+msgstr ""
+
+#. module: rma_sale_mrp
+#: code:addons/rma_sale_mrp/models/rma.py:49
+#, python-format
+msgid "You can't refund a kit in wich some RMAs aren't received"
+msgstr ""
+
diff --git a/rma_sale_mrp/models/__init__.py b/rma_sale_mrp/models/__init__.py
new file mode 100644
index 00000000..7593ab7f
--- /dev/null
+++ b/rma_sale_mrp/models/__init__.py
@@ -0,0 +1,3 @@
+from . import account_invoice
+from . import rma
+from . import sale_order
diff --git a/rma_sale_mrp/models/account_invoice.py b/rma_sale_mrp/models/account_invoice.py
new file mode 100644
index 00000000..bf0d04d8
--- /dev/null
+++ b/rma_sale_mrp/models/account_invoice.py
@@ -0,0 +1,19 @@
+# Copyright 2021 Tecnativa - David Vidal
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+from odoo import models
+from odoo.tools import float_compare
+
+
+class AccountInvoice(models.Model):
+ _inherit = "account.invoice"
+
+ def _check_rma_invoice_lines_qty(self):
+ """For those with differences, check if the kit quantity is the same"""
+ precision = self.env["decimal.precision"].precision_get(
+ "Product Unit of Measure")
+ lines = super()._check_rma_invoice_lines_qty()
+ if lines:
+ return lines.sudo().filtered(
+ lambda r: (r.rma_id.phantom_bom_product and float_compare(
+ r.quantity, r.rma_id.kit_qty, precision) < 0))
+ return lines
diff --git a/rma_sale_mrp/models/rma.py b/rma_sale_mrp/models/rma.py
new file mode 100644
index 00000000..6111b4e2
--- /dev/null
+++ b/rma_sale_mrp/models/rma.py
@@ -0,0 +1,65 @@
+# Copyright 2020 Tecnativa - David Vidal
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+from odoo import _, fields, models
+from odoo.exceptions import UserError
+import odoo.addons.decimal_precision as dp
+
+
+class Rma(models.Model):
+ _inherit = "rma"
+
+ phantom_bom_product = fields.Many2one(
+ comodel_name="product.product",
+ string="Related kit product",
+ readonly=True,
+ )
+ kit_qty = fields.Float(
+ string="Kit quantity",
+ digits=dp.get_precision("Product Unit of Measure"),
+ readonly=True,
+ help="To how many kits this components corresponds to. Used mainly "
+ "for refunding the right quantity",
+ )
+ rma_kit_register = fields.Char(readonly=True)
+
+ def _get_refund_line_quantity(self):
+ """Refund the kit, not the component"""
+ if self.phantom_bom_product:
+ uom = (
+ self.sale_line_id.product_uom
+ or self.phantom_bom_product.uom_id
+ )
+ return (self.kit_qty, uom)
+ return (self.product_uom_qty, self.product_uom)
+
+ def action_refund(self):
+ """We want to process them altogether"""
+ phantom_rmas = self.filtered("phantom_bom_product")
+ phantom_rmas |= self.search([
+ ("rma_kit_register", "in", phantom_rmas.mapped("rma_kit_register")),
+ ("id", "not in", phantom_rmas.ids),
+ ])
+ self -= phantom_rmas
+ for rma_kit_register in phantom_rmas.mapped(
+ "rma_kit_register"):
+ # We want to avoid refunding kits that aren't completely processed
+ rmas_by_register = phantom_rmas.filtered(
+ lambda x: x.rma_kit_register == rma_kit_register)
+ if any(rmas_by_register.filtered(lambda x: x.state != "received")):
+ raise UserError(_(
+ "You can't refund a kit in wich some RMAs aren't received"
+ ))
+ self |= rmas_by_register[0]
+ super().action_refund()
+ # We can just link the line to an RMA but we can link several RMAs
+ # to one invoice line.
+ for rma_kit_register in set(phantom_rmas.mapped("rma_kit_register")):
+ grouped_rmas = phantom_rmas.filtered(
+ lambda x: x.rma_kit_register == rma_kit_register)
+ lead_rma = grouped_rmas.filtered("refund_line_id")
+ grouped_rmas -= lead_rma
+ grouped_rmas.write({
+ "refund_line_id": lead_rma.refund_line_id.id,
+ "refund_id": lead_rma.refund_id.id,
+ "state": "refunded",
+ })
diff --git a/rma_sale_mrp/models/sale_order.py b/rma_sale_mrp/models/sale_order.py
new file mode 100644
index 00000000..fb464d0e
--- /dev/null
+++ b/rma_sale_mrp/models/sale_order.py
@@ -0,0 +1,110 @@
+# Copyright 2020 Tecnativa - David Vidal
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+from odoo import models
+
+
+class SaleOrder(models.Model):
+ _inherit = "sale.order"
+
+ def _prepare_rma_wizard_line_vals(self, data):
+ """Set the real kit product"""
+ vals = super()._prepare_rma_wizard_line_vals(data)
+ if data.get("phantom_bom_product"):
+ vals["phantom_bom_product"] = data.get("phantom_bom_product").id
+ vals["per_kit_quantity"] = data.get("per_kit_quantity", 0)
+ vals["phantom_kit_line"] = data.get("phantom_kit_line", False)
+ return vals
+
+ def get_delivery_rma_data(self):
+ """Get the phantom lines we'll be showing in the wizard"""
+ data_list = super().get_delivery_rma_data()
+ kit_products = set(
+ [
+ (
+ x.get("phantom_bom_product"),
+ x.get("sale_line_id")
+ ) for x in data_list
+ if x.get("phantom_bom_product")
+ ]
+ )
+ # For every unique phantom product we'll create a phantom line wich
+ # will be using as the control in frontend and for display purposes
+ # in backend
+ for product, sale_line_id in kit_products:
+ order_line_obj = self.env["sale.order.line"]
+ product_obj = self.env["product.product"]
+ first_component_dict = next(
+ x for x in data_list if x.get(
+ "phantom_bom_product", product_obj
+ ) == product and x.get(
+ "sale_line_id", order_line_obj
+ ) == sale_line_id)
+ component_index = data_list.index(first_component_dict)
+ # Prevent miscalculation if there partial deliveries
+ quantity = sum([
+ x.get("quantity", 0) for x in data_list
+ if x.get("sale_line_id")
+ and x.get("product") == first_component_dict.get("product")
+ and x.get("sale_line_id") == first_component_dict.get(
+ "sale_line_id")])
+ data_list.insert(
+ component_index, {
+ "product": product,
+ "quantity": (
+ first_component_dict.get("per_kit_quantity") and (
+ quantity
+ / first_component_dict.get("per_kit_quantity")
+ )
+ ),
+ "uom": first_component_dict.get(
+ "sale_line_id", order_line_obj).product_uom,
+ "phantom_kit_line": True,
+ "picking": False,
+ "sale_line_id": first_component_dict.get(
+ "sale_line_id", order_line_obj),
+ }
+ )
+ return data_list
+
+
+class SaleOrderLine(models.Model):
+ _inherit = "sale.order.line"
+
+ def get_delivery_move(self):
+ self.ensure_one()
+ if self.product_id and not self.product_id._is_phantom_bom():
+ return super().get_delivery_move()
+ return self.move_ids.filtered(lambda m: (
+ m.state == "done" and not m.scrapped
+ and m.location_dest_id.usage == "customer"
+ and (
+ not m.origin_returned_move_id
+ or (m.origin_returned_move_id and m.to_refund))))
+
+ def prepare_sale_rma_data(self):
+ """We'll take both the sale order product and the phantom one so we
+ can play with them when filtering or showing to the customer"""
+ self.ensure_one()
+ data = super().prepare_sale_rma_data()
+ if self.product_id and self.product_id._is_phantom_bom():
+ for d in data:
+ d.update(
+ {
+ "phantom_bom_product": self.product_id,
+ "per_kit_quantity": self._get_kit_qty(d.get("product"))
+ }
+ )
+ return data
+
+ def _get_kit_qty(self, product_id):
+ """Compute how many kit components were demanded from this line. We
+ rely on the matching of sale order and pickings demands, but if those
+ were manually changed, it could lead to inconsistencies"""
+ self.ensure_one()
+ if self.product_id and not self.product_id._is_phantom_bom():
+ return 0
+ component_demand = sum(
+ self.move_ids.filtered(
+ lambda x: x.product_id == product_id
+ and not x.origin_returned_move_id).mapped("product_uom_qty"))
+ return component_demand / self.product_uom_qty
diff --git a/rma_sale_mrp/readme/CONTRIBUTORS.rst b/rma_sale_mrp/readme/CONTRIBUTORS.rst
new file mode 100644
index 00000000..e5276b37
--- /dev/null
+++ b/rma_sale_mrp/readme/CONTRIBUTORS.rst
@@ -0,0 +1,3 @@
+* `Tecnativa `__:
+
+ * David Vidal
diff --git a/rma_sale_mrp/readme/DESCRIPTION.rst b/rma_sale_mrp/readme/DESCRIPTION.rst
new file mode 100644
index 00000000..ddd47c93
--- /dev/null
+++ b/rma_sale_mrp/readme/DESCRIPTION.rst
@@ -0,0 +1,3 @@
+This module enables RMAs for kits, wich isn't compatible with the base modules.
+In the backend side, we can return separate component while in the frontend
+side, customers can return the whole kit and the proper RMAs will be generated.
diff --git a/rma_sale_mrp/readme/ROADMAP.rst b/rma_sale_mrp/readme/ROADMAP.rst
new file mode 100644
index 00000000..0f199f02
--- /dev/null
+++ b/rma_sale_mrp/readme/ROADMAP.rst
@@ -0,0 +1,14 @@
+We compute the kits from the original demanded quantity in the sale order. If
+this quantity was to change, we could loose the right components per kit
+reference. So this should be very present. Also, v12 has a very poor support
+for delivered quantities, that is very improved in v13 with the introduction
+of the link to the BoM line in the stock moves. That approach could lead to
+errors as well, as the BoM line could change in the future loosing again the
+original components per kit reference. Anyway, is to be considered in that
+version to use the same rules so they fail for the same reasons.
+
+Some extra features would be nice to have:
+
+* Add actions constraints to disallow actions on single components.
+* Show kit components in the portal wizard.
+* Allow to make an RMA directly from a kit product.
diff --git a/rma_sale_mrp/readme/USAGE.rst b/rma_sale_mrp/readme/USAGE.rst
new file mode 100644
index 00000000..0ba426e9
--- /dev/null
+++ b/rma_sale_mrp/readme/USAGE.rst
@@ -0,0 +1,12 @@
+To use this module, you need to:
+
+#. Make a a sale order with a kit on it and deliver its components.
+#. Go to the portal view for the order and launch the RMA wizard.
+#. You'll see a line for the kit.
+#. There will be a limit of kits to return that should much the number of kits
+ delivered.
+#. Once you validate the wizard with the number of kits to deliver, you'll
+ have as many RMAs as components those kits have with the proper quantities
+ for each one.
+#. If you refund the components, the kit in the sale line will be used as the
+ reference.
diff --git a/rma_sale_mrp/static/description/index.html b/rma_sale_mrp/static/description/index.html
new file mode 100644
index 00000000..606e3a07
--- /dev/null
+++ b/rma_sale_mrp/static/description/index.html
@@ -0,0 +1,461 @@
+
+
+
+
+
+
+Return Merchandise Authorization Management - Link with MRP Kits
+
+
+
+
+
Return Merchandise Authorization Management - Link with MRP Kits
+
+
+
+
This module enables RMAs for kits, wich isn’t compatible with the base modules.
+In the backend side, we can return separate component while in the frontend
+side, customers can return the whole kit and the proper RMAs will be generated.
+
Table of contents
+
+
+
+
To use this module, you need to:
+
+Make a a sale order with a kit on it and deliver its components.
+Go to the portal view for the order and launch the RMA wizard.
+You’ll see a line for the kit.
+There will be a limit of kits to return that should much the number of kits
+delivered.
+Once you validate the wizard with the number of kits to deliver, you’ll
+have as many RMAs as components those kits have with the proper quantities
+for each one.
+If you refund the components, the kit in the sale line will be used as the
+reference.
+
+
+
+
+
We compute the kits from the original demanded quantity in the sale order. If
+this quantity was to change, we could loose the right components per kit
+reference. So this should be very present. Also, v12 has a very poor support
+for delivered quantities, that is very improved in v13 with the introduction
+of the link to the BoM line in the stock moves. That approach could lead to
+errors as well, as the BoM line could change in the future loosing again the
+original components per kit reference. Anyway, is to be considered in that
+version to use the same rules so they fail for the same reasons.
+
Some extra features would be nice to have:
+
+Add actions constraints to disallow actions on single components.
+Show kit components in the portal wizard.
+Allow to make an RMA directly from a kit product.
+
+
+
+
+
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.
+
+
+
+
+
+
+
+
This module is maintained by the OCA.
+
+
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 :
+
+
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_sale_mrp/tests/__init__.py b/rma_sale_mrp/tests/__init__.py
new file mode 100644
index 00000000..6506fae1
--- /dev/null
+++ b/rma_sale_mrp/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_rma_sale_mrp
diff --git a/rma_sale_mrp/tests/test_rma_sale_mrp.py b/rma_sale_mrp/tests/test_rma_sale_mrp.py
new file mode 100644
index 00000000..a08772c0
--- /dev/null
+++ b/rma_sale_mrp/tests/test_rma_sale_mrp.py
@@ -0,0 +1,155 @@
+# Copyright 2020 Tecnativa - David Vidal
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+from odoo.tests import Form, SavepointCase
+from odoo.exceptions import UserError
+
+
+class TestRmaSaleMrp(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.res_partner = cls.env["res.partner"]
+ cls.product_product = cls.env["product.product"]
+ cls.sale_order = cls.env["sale.order"]
+ cls.product_kit = cls.product_product.create({
+ "name": "Product test 1",
+ "type": "consu",
+ })
+ cls.product_kit_comp_1 = cls.product_product.create({
+ "name": "Product Component 1",
+ "type": "product",
+ })
+ cls.product_kit_comp_2 = cls.product_product.create({
+ "name": "Product Component 2",
+ "type": "product",
+ })
+ cls.bom = cls.env["mrp.bom"].create({
+ "product_id": cls.product_kit.id,
+ "product_tmpl_id": cls.product_kit.product_tmpl_id.id,
+ "type": "phantom",
+ "bom_line_ids": [
+ (0, 0, {
+ "product_id": cls.product_kit_comp_1.id,
+ "product_qty": 2,
+ }),
+ (0, 0, {
+ "product_id": cls.product_kit_comp_2.id,
+ "product_qty": 4,
+ })
+ ]})
+ cls.product_2 = cls.product_product.create({
+ "name": "Product test 2",
+ "type": "product",
+ })
+ cls.partner = cls.res_partner.create({
+ "name": "Partner test",
+ })
+ order_form = Form(cls.sale_order)
+ order_form.partner_id = cls.partner
+ with order_form.order_line.new() as line_form:
+ line_form.product_id = cls.product_kit
+ line_form.product_uom_qty = 5
+ cls.sale_order = order_form.save()
+ cls.sale_order.action_confirm()
+ # Maybe other modules create additional lines in the create
+ # method in sale.order model, so let's find the correct line.
+ cls.order_line = cls.sale_order.order_line.filtered(
+ lambda r: r.product_id == cls.product_kit)
+ cls.order_out_picking = cls.sale_order.picking_ids
+ # Confirm but leave a backorder to split moves so we can test that
+ # the wizard correctly creates the RMAs with the proper quantities
+ for line in cls.order_out_picking.move_lines:
+ line.quantity_done = line.product_uom_qty - 7
+ wiz_act = cls.order_out_picking.button_validate()
+ wiz = cls.env["stock.backorder.confirmation"].browse(wiz_act["res_id"])
+ wiz.process()
+ cls.backorder = cls.sale_order.picking_ids - cls.order_out_picking
+ for line in cls.backorder.move_lines:
+ line.quantity_done = line.product_uom_qty
+ cls.backorder.button_validate()
+
+ def test_create_rma_from_so(self):
+ order = self.sale_order
+ out_pickings = self.order_out_picking + self.backorder
+ wizard_id = order.action_create_rma()["res_id"]
+ wizard = self.env["sale.order.rma.wizard"].browse(wizard_id)
+ wizard.line_ids.quantity = 4
+ res = wizard.create_and_open_rma()
+ rmas = self.env["rma"].search(res["domain"])
+ for rma in rmas:
+ self.assertEqual(rma.partner_id, order.partner_id)
+ self.assertEqual(rma.order_id, order)
+ self.assertTrue(rma.picking_id in out_pickings)
+ self.assertEqual(rmas.mapped("phantom_bom_product"), self.product_kit)
+ self.assertEqual(
+ rmas.mapped("product_id"),
+ self.product_kit_comp_1 + self.product_kit_comp_2
+ )
+ rma_1 = rmas.filtered(lambda x: x.product_id == self.product_kit_comp_1)
+ rma_2 = rmas.filtered(lambda x: x.product_id == self.product_kit_comp_2)
+ move_1 = out_pickings.mapped("move_lines").filtered(
+ lambda x: x.product_id == self.product_kit_comp_1
+ )
+ move_2 = out_pickings.mapped("move_lines").filtered(
+ lambda x: x.product_id == self.product_kit_comp_2
+ )
+ self.assertEqual(
+ sum(rma_1.mapped("product_uom_qty")),
+ 8
+ )
+ self.assertEqual(
+ rma_1.mapped("product_uom"),
+ move_1.mapped("product_uom")
+ )
+ self.assertEqual(
+ sum(rma_2.mapped("product_uom_qty")),
+ 16
+ )
+ self.assertEqual(
+ rma_2.mapped("product_uom"),
+ move_2.mapped("product_uom")
+ )
+ self.assertEqual(rma.state, "confirmed")
+ self.assertEqual(
+ rma_1.mapped("reception_move_id.origin_returned_move_id"),
+ move_1,
+ )
+ self.assertEqual(
+ rma_2.mapped("reception_move_id.origin_returned_move_id"),
+ move_2,
+ )
+ self.assertEqual(
+ rmas.mapped("reception_move_id.picking_id")
+ + self.order_out_picking + self.backorder,
+ order.picking_ids,
+ )
+ # Refund the RMA
+ user = self.env["res.users"].create(
+ {
+ "login": "test_refund_with_so",
+ "name": "Test",
+ }
+ )
+ order.user_id = user.id
+ rma.reception_move_id.quantity_done = rma.product_uom_qty
+ rma.reception_move_id.picking_id.action_done()
+ # All the component RMAs must be received if we want to make a refund
+ with self.assertRaises(UserError):
+ rma.action_refund()
+ rmas_left = rmas - rma
+ for additional_rma in rmas_left:
+ additional_rma.reception_move_id.quantity_done = (
+ additional_rma.product_uom_qty)
+ additional_rma.reception_move_id.picking_id.action_done()
+ rma.action_refund()
+ self.assertEqual(rma.refund_id.user_id, user)
+ # The component RMAs get automatically refunded
+ self.assertEqual(rma.refund_id, rmas_left.mapped("refund_id"))
+ # The refund product is the kit, not the components
+ self.assertEqual(
+ rma.refund_id.invoice_line_ids.product_id, self.product_kit)
+ rma.refund_id.action_invoice_open()
+ # We can still return another kit
+ wizard_id = order.action_create_rma()["res_id"]
+ wizard = self.env["sale.order.rma.wizard"].browse(wizard_id)
+ self.assertEqual(wizard.line_ids.quantity, 1)
diff --git a/rma_sale_mrp/views/report_rma.xml b/rma_sale_mrp/views/report_rma.xml
new file mode 100644
index 00000000..55b3b715
--- /dev/null
+++ b/rma_sale_mrp/views/report_rma.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+ Kit information
+
+ Related Kit Components RMAs
+
+
+
+
+ RMA
+
+
+ Product
+
+
+ Quantity
+
+
+ State
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rma_sale_mrp/views/rma_views.xml b/rma_sale_mrp/views/rma_views.xml
new file mode 100644
index 00000000..d3325c27
--- /dev/null
+++ b/rma_sale_mrp/views/rma_views.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ rma
+
+
+
+
+
+
+
+
+ rma
+
+
+
+
+
+
+
diff --git a/rma_sale_mrp/views/sale_order_portal_template.xml b/rma_sale_mrp/views/sale_order_portal_template.xml
new file mode 100644
index 00000000..191ee317
--- /dev/null
+++ b/rma_sale_mrp/views/sale_order_portal_template.xml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+ #{data.get('phantom_bom_product') and 'd-none'}
+
+
+ data['quantity'] > 0 and (data['picking'] or data.get('phantom_kit_line'))
+
+
+ not data.get('phantom_bom_product')
+
+
+
+
+
+
+
+
+ Kit Quantity
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Kit
+
+
+
+
+
+ ( )
+
+
+
+
+
diff --git a/rma_sale_mrp/wizard/__init__.py b/rma_sale_mrp/wizard/__init__.py
new file mode 100644
index 00000000..77189e50
--- /dev/null
+++ b/rma_sale_mrp/wizard/__init__.py
@@ -0,0 +1 @@
+from . import sale_order_rma_wizard
diff --git a/rma_sale_mrp/wizard/sale_order_rma_wizard.py b/rma_sale_mrp/wizard/sale_order_rma_wizard.py
new file mode 100644
index 00000000..d1c8c61b
--- /dev/null
+++ b/rma_sale_mrp/wizard/sale_order_rma_wizard.py
@@ -0,0 +1,119 @@
+# Copyright 2020 Tecnativa - David Vidal
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+from odoo import api, fields, models
+
+
+class SaleOrderRmaWizard(models.TransientModel):
+ _inherit = "sale.order.rma.wizard"
+
+ # We wan't to separate components from the main line so we can separate
+ # them and hide them in the wizard
+ component_line_ids = fields.One2many(
+ comodel_name="sale.order.line.rma.wizard.component",
+ inverse_name="wizard_id",
+ string="Component Lines",
+ )
+
+ @api.model
+ def create(self, vals):
+ """Split component lines"""
+ if "line_ids" in vals and vals.get("line_ids"):
+ line_ids = [
+ (x[0], x[1], x[2]) for x in vals.get("line_ids")
+ if not x[2].get("phantom_bom_product")
+ ]
+ component_line_ids = [
+ (x[0], x[1], x[2]) for x in vals.get("line_ids")
+ if x[2].get("phantom_bom_product")
+ ]
+ vals.update({
+ "line_ids": line_ids,
+ "component_line_ids": component_line_ids,
+ })
+ return super().create(vals)
+
+ def create_rma(self, from_portal=None):
+ """We'll decompose the RMAs and remove the phantom lines"""
+ phantom_lines = self.line_ids.filtered("phantom_kit_line")
+ # Coming from the portal, we'll compute how many per kit to receive.
+ # From backend we'll be returning them from each individual component
+ # import pdb; pdb.set_trace()
+ for line in phantom_lines:
+ kit_lines = self.component_line_ids.filtered(
+ lambda x: x.phantom_bom_product == line.product_id
+ and x.sale_line_id == line.sale_line_id
+ )
+ component_products = kit_lines.mapped("product_id")
+ for product in component_products:
+ product_kit_lines = kit_lines.filtered(
+ lambda x: x.product_id == product)
+ qty_to_return = product_kit_lines[0].per_kit_quantity * line.quantity
+ while qty_to_return:
+ for kit_line in product_kit_lines:
+ kit_line.quantity = min(qty_to_return, kit_line.quantity)
+ kit_line.operation_id = line.operation_id
+ kit_line.kit_qty_done = (
+ kit_line.quantity / kit_line.per_kit_quantity)
+ qty_to_return -= kit_line.quantity
+ # Finally we add them the main line_ids
+ kit_line_vals = [
+ (0, 0, x._convert_to_write(x._cache)) for x in kit_lines]
+ self.update({"line_ids": kit_line_vals})
+ # We don't need them anymore
+ phantom_lines.unlink()
+ return super().create_rma(from_portal=from_portal)
+
+
+class SaleOrderLineRmaWizard(models.TransientModel):
+ _inherit = "sale.order.line.rma.wizard"
+
+ phantom_bom_product = fields.Many2one(
+ comodel_name="product.product",
+ )
+ kit_qty_done = fields.Float(
+ readonly=True,
+ help="Used to inform kit qty used in the rma. Will be useful to refund",
+ )
+ per_kit_quantity = fields.Float(
+ readonly=True,
+ )
+ phantom_kit_line = fields.Boolean(readonly=True)
+
+ @api.depends("picking_id")
+ def _compute_move_id(self):
+ """We need to process kit components separately so we can match them
+ against their phantom product"""
+ not_kit = self.filtered(
+ lambda x: not x.phantom_bom_product and
+ not x.product_id._is_phantom_bom())
+ super(SaleOrderLineRmaWizard, not_kit)._compute_move_id()
+ for line in self.filtered(
+ lambda x: x.phantom_bom_product and x.picking_id):
+ line.move_id = line.picking_id.move_lines.filtered(
+ lambda ml: (
+ ml.product_id == line.product_id
+ and ml.sale_line_id == line.sale_line_id
+ and ml.sale_line_id.product_id == line.phantom_bom_product
+ and ml.sale_line_id.order_id == line.order_id))
+
+ def _prepare_rma_values(self):
+ """It will be used as a reference for the components"""
+ res = super()._prepare_rma_values()
+ if self.phantom_bom_product:
+ unique_register = "{}-{}-{}".format(
+ self.wizard_id.id,
+ self.phantom_bom_product.id,
+ self.sale_line_id.id
+ )
+ res.update({
+ "phantom_bom_product": self.phantom_bom_product.id,
+ "kit_qty": self.kit_qty_done,
+ "rma_kit_register": unique_register,
+ })
+ return res
+
+
+class SaleOrderLineRmaWizardComponent(models.TransientModel):
+ _name = "sale.order.line.rma.wizard.component"
+ _inherit = "sale.order.line.rma.wizard"
+ _description = "Used to hide kit components in the wizards"
diff --git a/rma_sale_mrp/wizard/sale_order_rma_wizard_views.xml b/rma_sale_mrp/wizard/sale_order_rma_wizard_views.xml
new file mode 100644
index 00000000..bac324da
--- /dev/null
+++ b/rma_sale_mrp/wizard/sale_order_rma_wizard_views.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ sale.order.rma.wizard
+
+
+
+
+
+ {'readonly': [('phantom_kit_line', '=', True)]}
+ 1
+
+
+
+