[ADD] rma_sale_mrp: New module to refund kits

This commit is contained in:
david
2021-02-19 15:09:21 +01:00
committed by Víctor Martínez
parent 374295545e
commit cd7448c283
23 changed files with 1834 additions and 0 deletions

119
rma_sale_mrp/README.rst Normal file
View File

@@ -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 <https://github.com/OCA/rma/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/rma/issues/new?body=module:%20rma_sale_mrp%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Tecnativa
Contributors
~~~~~~~~~~~~
* `Tecnativa <https://www.tecnativa.com>`__:
* 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 <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-chienandalu|
This module is part of the `OCA/rma <https://github.com/OCA/rma/tree/12.0/rma_sale_mrp>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

2
rma_sale_mrp/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from . import models
from . import wizard

View File

@@ -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",
],
}

249
rma_sale_mrp/i18n/es.po Normal file
View File

@@ -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 "<span class=\"badge badge-danger label-text-align\"><i class=\"fa fa-fw fa-times\"/> Cancelled</span>"
msgstr "<span class=\"badge badge-danger label-text-align\"><i class=\"fa fa-fw fa-times\"/>Cancelado</span>"
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
msgid "<span class=\"badge badge-info label-text-align\"><i class=\"fa fa-fw fa-clock-o\"/> Draft</span>"
msgstr "<span class=\"badge badge-info label-text-align\"><i class=\"fa fa-fw fa-clock-o\"/> Borrador</span>"
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
msgid "<span class=\"badge badge-warning label-text-align\"><i class=\"fa fa-fw fa-clock-o\"/> Waiting</span>"
msgstr "<span class=\"badge badge-warning label-text-align\"><i class=\"fa fa-fw fa-clock-o\"/> En espera</span>"
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
msgid "<strong class=\"d-block mb-1\">Related Kit Components RMAs</strong>"
msgstr "<strong class=\"d-block mb-1\">RMAs relacionados de los componentes del kit</strong>"
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
msgid "<strong><i class=\"fa fa-th-large text-info\"/> Kit Quantity</strong>"
msgstr "<strong><i class=\"fa fa-th-large text-info\"/> Ctd. de kits</strong>"
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
msgid "<strong><i class=\"fa fa-th-large text-info\"/> Kit</strong>"
msgstr "<strong><i class=\"fa fa-th-large text-info\"/> Kit</strong>"
#. 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`"

View File

@@ -0,0 +1,287 @@
# 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"
"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 "<span class=\"badge badge-danger label-text-align\"><i class=\"fa fa-fw fa-times\"/> Cancelled</span>"
msgstr ""
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
msgid "<span class=\"badge badge-info label-text-align\"><i class=\"fa fa-fw fa-clock-o\"/> Draft</span>"
msgstr ""
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
msgid "<span class=\"badge badge-warning label-text-align\"><i class=\"fa fa-fw fa-clock-o\"/> Waiting</span>"
msgstr ""
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.report_rma_document
msgid "<span>Product</span>"
msgstr ""
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.report_rma_document
msgid "<span>Quantity</span>"
msgstr ""
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.report_rma_document
msgid "<span>RMA</span>"
msgstr ""
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.report_rma_document
msgid "<span>State</span>"
msgstr ""
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.report_rma_document
msgid "<strong class=\"d-block mb-1\">Related Kit Components RMAs</strong>"
msgstr ""
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.report_rma_document
msgid "<strong class=\"d-block mt32 mb-1\">Kit information</strong>"
msgstr ""
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
msgid "<strong><i class=\"fa fa-th-large text-info\"/> Kit Quantity</strong>"
msgstr ""
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.portal_rma_page
msgid "<strong><i class=\"fa fa-th-large text-info\"/> Kit</strong>"
msgstr ""
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.report_rma_document
msgid "<strong>Kit Quantity:</strong>"
msgstr ""
#. module: rma_sale_mrp
#: model_terms:ir.ui.view,arch_db:rma_sale_mrp.report_rma_document
msgid "<strong>Kit:</strong>"
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:ir.model,name:rma_sale_mrp.model_account_invoice
msgid "Invoice"
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 ""

View File

@@ -0,0 +1,3 @@
from . import account_invoice
from . import rma
from . import sale_order

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -0,0 +1,3 @@
* `Tecnativa <https://www.tecnativa.com>`__:
* David Vidal

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,461 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<title>Return Merchandise Authorization Management - Link with MRP Kits</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="return-merchandise-authorization-management-link-with-mrp-kits">
<h1 class="title">Return Merchandise Authorization Management - Link with MRP Kits</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/rma/tree/12.0/rma_sale_mrp"><img alt="OCA/rma" src="https://img.shields.io/badge/github-OCA%2Frma-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/rma-12-0/rma-12-0-rma_sale_mrp"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/145/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module enables RMAs for kits, wich isnt 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.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="id1">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id2">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
<p>To use this module, you need to:</p>
<ol class="arabic simple">
<li>Make a a sale order with a kit on it and deliver its components.</li>
<li>Go to the portal view for the order and launch the RMA wizard.</li>
<li>Youll see a line for the kit.</li>
<li>There will be a limit of kits to return that should much the number of kits
delivered.</li>
<li>Once you validate the wizard with the number of kits to deliver, youll
have as many RMAs as components those kits have with the proper quantities
for each one.</li>
<li>If you refund the components, the kit in the sale line will be used as the
reference.</li>
</ol>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id2">Known issues / Roadmap</a></h1>
<p>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.</p>
<p>Some extra features would be nice to have:</p>
<ul class="simple">
<li>Add actions constraints to disallow actions on single components.</li>
<li>Show kit components in the portal wizard.</li>
<li>Allow to make an RMA directly from a kit product.</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/rma/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/rma/issues/new?body=module:%20rma_sale_mrp%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id5">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id6">Contributors</a></h2>
<ul class="simple">
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>David Vidal</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id7">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external" href="https://github.com/chienandalu"><img alt="chienandalu" src="https://github.com/chienandalu.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/rma/tree/12.0/rma_sale_mrp">OCA/rma</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1 @@
from . import test_rma_sale_mrp

View File

@@ -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)

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<odoo>
<template id="report_rma_document" inherit_id="rma_sale.report_rma_document">
<xpath expr="//div[@id='product_information']" position="after">
<t t-if="doc.sudo().phantom_bom_product">
<strong class="d-block mt32 mb-1">Kit information</strong>
<div class="row mb32" id="kit_information">
<div t-if="doc.sudo().phantom_bom_product" class="col-auto mw-100 mb-2">
<strong>Kit:</strong>
<p class="m-0" t-field="doc.sudo().phantom_bom_product.display_name"/>
</div>
<div t-if="doc.sudo().phantom_bom_product" class="col-auto mw-100 mb-2">
<strong>Kit Quantity:</strong>
<p class="m-0" t-field="doc.kit_qty"/>
</div>
</div>
<strong class="d-block mb-1">Related Kit Components RMAs</strong>
<t t-set="related_kit_rmas" t-value="doc.search([('rma_kit_register', '=', doc.rma_kit_register), ('id', '!=', doc.id)])" />
<table class="table table-sm">
<thead>
<th>
<span>RMA</span>
</th>
<th>
<span>Product</span>
</th>
<th>
<span>Quantity</span>
</th>
<th>
<span>State</span>
</th>
</thead>
<tbody>
<tr t-foreach="related_kit_rmas" t-as="kit_rma">
<td>
<span t-field="kit_rma.name"/>
</td>
<td>
<span t-if="kit_rma.product_id" t-field="kit_rma.product_id"/>
</td>
<td>
<t t-if="kit_rma.product_id">
<span t-field="kit_rma.product_uom_qty"/>
<span t-field="kit_rma.uom_id" groups="uom.group_uom"/>
</t>
</td>
<td>
<span t-field="kit_rma.state"/>
</td>
</tr>
</tbody>
</table>
</t>
</xpath>
</template>
</odoo>
</odoo>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="rma_view_form" model="ir.ui.view">
<field name="inherit_id" ref="rma_sale.rma_view_form" />
<field name="model">rma</field>
<field name="arch" type="xml">
<field name="product_id" position="before">
<field name="phantom_bom_product" attrs="{'invisible': [('phantom_bom_product', '=', False)]}" />
</field>
</field>
</record>
<record id="rma_view_tree" model="ir.ui.view">
<field name="inherit_id" ref="rma.rma_view_tree" />
<field name="model">rma</field>
<field name="arch" type="xml">
<field name="product_uom" position="after">
<field name="phantom_bom_product"/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="sale_order_portal_template" name="Request RMA MRP" inherit_id="rma_sale.sale_order_portal_template">
<xpath expr="//input[@t-attf-name='#{data_index}-product_id']" position="after">
<input type="hidden"
t-if="data.get('phantom_bom_product')"
t-attf-name="#{data_index}-phantom_bom_product"
t-att-value="data['phantom_bom_product'].id"/>
<input type="hidden"
t-if="data.get('per_kit_quantity')"
t-attf-name="#{data_index}-per_kit_quantity"
t-att-value="data['per_kit_quantity']"/>
<input type="hidden"
t-if="data.get('phantom_kit_line')"
t-attf-name="#{data_index}-phantom_kit_line"
t-att-value="1"/>
</xpath>
<!-- TODO: We could give a clue about what's to be returned, with readonly detailed lines -->
<xpath expr="//tbody[hasclass('request-rma-tbody')]//t[@t-if=&quot;data[&apos;quantity&apos;] &gt; 0 and data[&apos;picking&apos;]&quot;]/tr" position="attributes">
<attribute name="t-attf-class">#{data.get('phantom_bom_product') and 'd-none'}</attribute>
</xpath>
<xpath expr="//tbody[hasclass('request-rma-tbody')]//t[@t-if=&quot;data[&apos;quantity&apos;] &gt; 0 and data[&apos;picking&apos;]&quot;]" position="attributes">
<attribute name="t-if">data['quantity'] > 0 and (data['picking'] or data.get('phantom_kit_line'))</attribute>
</xpath>
<xpath expr="//select[@t-attf-name='#{data_index}-operation_id']/option" position="attributes">
<attribute name="t-if">not data.get('phantom_bom_product')</attribute>
</xpath>
</template>
<template id="portal_rma_page" name="RMA Kits" inherit_id="rma.portal_rma_page">
<xpath expr="//div[@t-if='rma.sudo().product_id']" position="before">
<t t-if="rma.sudo().phantom_bom_product">
<div class="row mb-2 mb-sm-1">
<div class="col-12 col-sm-4">
<strong><i class="fa fa-th-large text-info" /> Kit</strong>
</div>
<div class="col-12 col-sm-8">
<span t-esc="rma.sudo().phantom_bom_product.display_name"/>
</div>
</div>
<div class="row mb-2 mb-sm-1">
<div class="col-12 col-sm-4">
<strong><i class="fa fa-th-large text-info" /> Kit Quantity</strong>
</div>
<div class="col-12 col-sm-8">
<span t-esc="rma.kit_qty"/>
</div>
</div>
</t>
</xpath>
<xpath expr="//section[@id='reception_section']" position="before">
<section t-if="rma.sudo().phantom_bom_product" id="kits_sections" style="page-break-inside: auto;" class="mt32">
<strong class="d-block mb-1">Related Kit Components RMAs</strong>
<t t-set="related_kit_rmas" t-value="rma.search([('rma_kit_register', '=', rma.rma_kit_register), ('id', '!=', rma.id)])" />
<t t-foreach="related_kit_rmas" t-as="kit_rma">
<a class="list-group-item list-group-item-action d-flex flex-wrap align-items-center justify-content-between py-2 px-3" t-att-href="kit_rma.get_portal_url()" t-att-title="kit_rma.name">
<div>
<i class="fa fa-reply mr-1" role="img"/>
<span t-esc="kit_rma.name" class="mr-lg-3"/>
<div class="d-lg-inline-block"><span t-field="kit_rma.sudo().product_id.display_name"/> (<t t-field="kit_rma.product_uom_qty" />)</div>
</div>
<t t-if="kit_rma.state in ['confirmed', 'received', 'returned', 'replaced', 'locked', 'refunded']">
<span class="badge badge-success label-text-align"><i class="fa fa-fw fa-reply"/> <t t-field="kit_rma.state" /></span>
</t>
<t t-if="kit_rma.state in ['waiting_return', 'waiting_replacement']">
<span class="badge badge-warning label-text-align"><i class="fa fa-fw fa-clock-o"/> Waiting</span>
</t>
<t t-if="kit_rma.state == 'cancelled'">
<span class="badge badge-danger label-text-align"><i class="fa fa-fw fa-times"/> Cancelled</span>
</t>
<t t-if="kit_rma.state == 'draft'">
<span class="badge badge-info label-text-align"><i class="fa fa-fw fa-clock-o"/> Draft</span>
</t>
</a>
</t>
</section>
</xpath>
</template>
<template id="portal_my_rmas" name="My RMA Orders MRP" inherit_id="rma.portal_my_rmas">
<xpath expr="//th[@name='th_product']" position="after">
<th name="th_kit">Kit</th>
</xpath>
<xpath expr="//td[@name='td_product']" position="after">
<td name="td_kit">
<t t-if="rma.sudo().phantom_bom_product">
<i class="fa fa-th-large text-info" /> <span t-esc="rma.sudo().phantom_bom_product.display_name"/>
<span> (<t t-esc="rma.kit_qty" />)</span>
</t>
</td>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1 @@
from . import sale_order_rma_wizard

View File

@@ -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"

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_order_rma_wizard_form_view" model="ir.ui.view">
<field name="inherit_id" ref="rma_sale.sale_order_rma_wizard_form_view" />
<field name="model">sale.order.rma.wizard</field>
<field name="arch" type="xml">
<xpath expr="//tree/field[@name='order_id']" position="after">
<field name="phantom_kit_line" invisible="1" />
</xpath>
<xpath expr="//tree/field[@name='picking_id']" position="attributes">
<attribute name="attrs">{'readonly': [('phantom_kit_line', '=', True)]}</attribute>
<attribute name="force_save">1</attribute>
</xpath>
</field>
</record>
</odoo>