diff --git a/rma_reason_code/README.rst b/rma_reason_code/README.rst new file mode 100644 index 00000000..7b1d0aba --- /dev/null +++ b/rma_reason_code/README.rst @@ -0,0 +1,47 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :alt: License LGPL-3 + +============ +RMA Put Away +============ + +This module allows you to put away the products after you have received them. + +Configuration +============= + +Go to *RMA / Configuration / Customer Operations* and define there: + +#. The Put Away Policy +#. The route that you wish to use to put away the products. +#. The default destination location (optional). + +Usage +===== + +#. Go to a Customer RMA. +#. Click on *Put Away*. +#. Indicate the quantity that you want to put away and destination location. + +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. + +Credits +======= + +Contributors +------------ + +* Jordi Ballester Alomar +* David Jimenez + + +Maintainer +---------- + +This module is maintained by ForgeFlow diff --git a/rma_reason_code/__init__.py b/rma_reason_code/__init__.py new file mode 100644 index 00000000..c88b5c5a --- /dev/null +++ b/rma_reason_code/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2024 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import models +from . import reports diff --git a/rma_reason_code/__manifest__.py b/rma_reason_code/__manifest__.py new file mode 100644 index 00000000..b12431e0 --- /dev/null +++ b/rma_reason_code/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2024 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "RMA Reason Code", + "version": "14.0.1.1.0", + "license": "AGPL-3", + "summary": "Reason code for RMA", + "author": "ForgeFlow", + "website": "https://github.com/ForgeFlow/stock-rma", + "category": "Warehouse Management", + "depends": ["rma"], + "data": [ + "security/ir.model.access.csv", + "security/security.xml", + "views/reason_code_view.xml", + "views/rma_order_line_views.xml", + "reports/rma_reason_code_report_views.xml", + ], + "installable": True, +} diff --git a/rma_reason_code/models/__init__.py b/rma_reason_code/models/__init__.py new file mode 100644 index 00000000..915e24af --- /dev/null +++ b/rma_reason_code/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2024 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import reason_code +from . import rma_order_line diff --git a/rma_reason_code/models/reason_code.py b/rma_reason_code/models/reason_code.py new file mode 100644 index 00000000..b52ef0a7 --- /dev/null +++ b/rma_reason_code/models/reason_code.py @@ -0,0 +1,26 @@ +# Copyright 2024 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from random import randint + +from odoo import fields, models + + +class RMAReasonCode(models.Model): + _name = "rma.reason.code" + _description = "RMA Reason Code" + + def _get_default_color(self): + return randint(1, 11) + + name = fields.Char("Code", required=True) + description = fields.Text("Description") + type = fields.Selection( + [ + ("customer", "Customer RMA"), + ("supplier", "Supplier RTV"), + ("both", "Both Customer and Supplier"), + ], + default="both", + required=True, + ) + color = fields.Integer("Color", default=_get_default_color) diff --git a/rma_reason_code/models/rma_order_line.py b/rma_reason_code/models/rma_order_line.py new file mode 100644 index 00000000..5233085f --- /dev/null +++ b/rma_reason_code/models/rma_order_line.py @@ -0,0 +1,44 @@ +# Copyright 2024 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class RMAOrderLine(models.Model): + _inherit = "rma.order.line" + + reason_code_ids = fields.Many2many( + "rma.reason.code", + "rma_order_line_reason_code_rel", + string="Reason Code", + domain="[('id', 'in', allowed_reason_code_ids)]", + ) + allowed_reason_code_ids = fields.Many2many( + comodel_name="rma.reason.code", + compute="_compute_allowed_reason_code_ids", + ) + + @api.depends("type") + def _compute_allowed_reason_code_ids(self): + for rec in self: + codes = self.env["rma.reason.code"] + if rec.type == "customer": + codes = codes.search([("type", "in", ["customer", "both"])]) + else: + codes = codes.search([("type", "in", ["supplier", "both"])]) + rec.allowed_reason_code_ids = codes + + @api.constrains("reason_code_ids", "product_id") + def _check_reason_code_ids(self): + for rec in self: + if rec.reason_code_ids and not any( + rc in rec.allowed_reason_code_ids for rc in rec.reason_code_ids + ): + raise ValidationError( + _( + "Any of the reason code selected is not allowed for " + "this type of RMA (%s)." + ) + % rec.type + ) diff --git a/rma_reason_code/readme/CONTRIBUTORS.rst b/rma_reason_code/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..fcd08101 --- /dev/null +++ b/rma_reason_code/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* David Jiménez diff --git a/rma_reason_code/readme/DESCRIPTION.rst b/rma_reason_code/readme/DESCRIPTION.rst new file mode 100644 index 00000000..041c4317 --- /dev/null +++ b/rma_reason_code/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Adds a reason code for RMA operations and an interface for the user to create RMA codes diff --git a/rma_reason_code/reports/__init__.py b/rma_reason_code/reports/__init__.py new file mode 100644 index 00000000..ac4253fc --- /dev/null +++ b/rma_reason_code/reports/__init__.py @@ -0,0 +1 @@ +from . import rma_reason_code_report diff --git a/rma_reason_code/reports/rma_reason_code_report.py b/rma_reason_code/reports/rma_reason_code_report.py new file mode 100644 index 00000000..b0f87364 --- /dev/null +++ b/rma_reason_code/reports/rma_reason_code_report.py @@ -0,0 +1,55 @@ +# Copyright 2022 ForgeFlow S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class RmaReasonCodeReport(models.Model): + _name = "rma.reason.code.report" + _auto = False + _description = "Rma Reason Code Report" + + rma_order_line_id = fields.Many2one(comodel_name="rma.order.line") + reason_code_id = fields.Many2one(comodel_name="rma.reason.code") + date_rma = fields.Datetime(string="Order Date") + type = fields.Selection([("customer", "Customer"), ("supplier", "Supplier")]) + company_id = fields.Many2one(comodel_name="res.company") + + def _select(self): + return """ + SELECT + row_number() OVER () AS id, + rma.id as rma_order_line_id, + rma.type, + rrc.id as reason_code_id, + rma.date_rma, + rma.company_id + + """ + + def _from(self): + return """ + FROM + rma_order_line rma + INNER JOIN + rma_order_line_reason_code_rel rolr ON rma.id = rolr.rma_order_line_id + INNER JOIN + rma_reason_code rrc ON rolr.rma_reason_code_id = rrc.id + + """ + + def _order_by(self): + return """ + ORDER BY + rma.id, rrc.id + """ + + @property + def _table_query(self): + return """ + {_select} + {_from} + {_order_by} + """.format( + _select=self._select(), _from=self._from(), _order_by=self._order_by() + ) diff --git a/rma_reason_code/reports/rma_reason_code_report_views.xml b/rma_reason_code/reports/rma_reason_code_report_views.xml new file mode 100644 index 00000000..dd72fb65 --- /dev/null +++ b/rma_reason_code/reports/rma_reason_code_report_views.xml @@ -0,0 +1,122 @@ + + + + rma.reason.code.report.tree + rma.reason.code.report + + + + + + + + + + + + + rma.reason.code.report.graph + rma.reason.code.report + + + + + + + + + rma.reason.code.report.search + rma.reason.code.report + + + + + + + + + + + + + + + + + + + + RMA Reason Code Analysis + rma.reason.code.report + graph,pivot,tree + + { + 'search_default_group_rma_date': 1, + 'search_default_group_reason_code_id': 2, + 'search_default_is_customer': 1, + } + +

+ No data yet! +

+ Assign a Reason Code to a RMA +

+
+
+ + + RTV Reason Code Analysis + rma.reason.code.report + graph,pivot,tree + + { + 'search_default_group_rma_date': 1, + 'search_default_group_reason_code_id': 2, + 'search_default_is_supplier': 1, + } + +

+ No data yet! +

+ Assign a Reason Code to a RTV +

+
+
+ + + + + +
diff --git a/rma_reason_code/security/ir.model.access.csv b/rma_reason_code/security/ir.model.access.csv new file mode 100644 index 00000000..11b0aac1 --- /dev/null +++ b/rma_reason_code/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_rma_reason_code_user,rma.reason.code,model_rma_reason_code,rma.group_rma_customer_user,1,0,0,0 +access_rma_reason_code_manager,rma.reason.code,model_rma_reason_code,rma.group_rma_manager,1,1,1,1 +access_rma_reason_code_report_user,rma.reason.code.report,model_rma_reason_code_report,rma.group_rma_customer_user,1,0,0,0 diff --git a/rma_reason_code/security/security.xml b/rma_reason_code/security/security.xml new file mode 100644 index 00000000..ad97b8b0 --- /dev/null +++ b/rma_reason_code/security/security.xml @@ -0,0 +1,14 @@ + + + + + RMA Reason Code Report + + ['|',('company_id','=',False),('company_id', 'in', company_ids)] + + diff --git a/rma_reason_code/static/description/icon.png b/rma_reason_code/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/rma_reason_code/static/description/icon.png differ diff --git a/rma_reason_code/static/description/index.html b/rma_reason_code/static/description/index.html new file mode 100644 index 00000000..12dd1e4c --- /dev/null +++ b/rma_reason_code/static/description/index.html @@ -0,0 +1,451 @@ + + + + + + +Scrap Reason Code + + + +
+

Scrap Reason Code

+ + +

Beta License: AGPL-3 OCA/stock-logistics-warehouse Translate me on Weblate Try me on Runbot

+

Adds a reason code for scrapping operations and an interface for the user +to create scrap codes

+

Table of contents

+ +
+

Configuration

+

Go to Inventory > Configuration > Scrap Reason Codes

+

Create a required scrap reason code and provide scrap location.

+
+
+

Usage

+
    +
  • Go to Inventory > Operations > Scrap
  • +
  • Create a scarp order and select reason code.
  • +
  • A scrap location will be readonly and auto fill based on selected reason +code.
  • +
+
+
+

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

+
    +
  • Open Source Integrators
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The development of this module has been financially supported by:

+
    +
  • Open Source Integrators
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

bodedra

+

This module is part of the OCA/stock-logistics-warehouse project on GitHub.

+

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

+
+
+
+ + diff --git a/rma_reason_code/tests/__init__.py b/rma_reason_code/tests/__init__.py new file mode 100644 index 00000000..a493ff1f --- /dev/null +++ b/rma_reason_code/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2024 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import test_scrap_reason_code diff --git a/rma_reason_code/tests/test_scrap_reason_code.py b/rma_reason_code/tests/test_scrap_reason_code.py new file mode 100644 index 00000000..1c896ab3 --- /dev/null +++ b/rma_reason_code/tests/test_scrap_reason_code.py @@ -0,0 +1,160 @@ +# Copyright 2024 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api +from odoo.exceptions import ValidationError +from odoo.tests import common + + +class RMAOrderLine(common.SavepointCase): + @classmethod + def setUpClass(cls): + super(RMAOrderLine, cls).setUpClass() + cls.user_admin = cls.env.ref("base.user_admin") + cls.env = api.Environment(cls.cr, cls.user_admin.id, {}) + cls.user_admin.tz = False # Make sure there's no timezone in user + cls.warehouse = cls.env.ref("stock.warehouse0") + cls.location = cls.env.ref("rma.location_rma") + cls.cust_location = cls.env.ref("stock.stock_location_customers") + cls.vend_location = cls.env.ref("stock.stock_location_suppliers") + cls.product = cls.env["product.product"].create( + { + "name": "TEST Product", + "type": "product", + } + ) + cls.partner = cls.env["res.partner"].create({"name": "Test partner"}) + + cls.route = cls.env.ref("rma.route_rma_customer") + + cls.operation1 = cls.env["rma.operation"].create( + { + "code": "TEST1", + "name": "Replace after receive", + "type": "customer", + "receipt_policy": "ordered", + "delivery_policy": "received", + "in_route_id": cls.route.id, + "out_route_id": cls.route.id, + "location_id": cls.location.id, + "in_warehouse_id": cls.warehouse.id, + "out_warehouse_id": cls.warehouse.id, + } + ) + + cls.operation2 = cls.env["rma.operation"].create( + { + "code": "TEST2", + "name": "Refund after receive", + "type": "supplier", + "receipt_policy": "ordered", + "delivery_policy": "no", + "in_route_id": cls.route.id, + "out_route_id": cls.route.id, + "location_id": cls.location.id, + "in_warehouse_id": cls.warehouse.id, + "out_warehouse_id": cls.warehouse.id, + } + ) + + cls.rma_line_1 = cls.env["rma.order.line"].create( + { + "partner_id": cls.partner.id, + "requested_by": False, + "assigned_to": False, + "type": "customer", + "product_id": cls.product.id, + "uom_id": cls.product.uom_id.id, + "product_qty": 1, + "price_unit": 10, + "operation_id": cls.operation1.id, + "delivery_address_id": cls.partner.id, + "receipt_policy": cls.operation1.receipt_policy, + "delivery_policy": cls.operation1.delivery_policy, + "in_warehouse_id": cls.operation1.in_warehouse_id.id, + "out_warehouse_id": cls.operation1.out_warehouse_id.id, + "in_route_id": cls.operation1.in_route_id.id, + "out_route_id": cls.operation1.out_route_id.id, + "location_id": cls.operation1.location_id.id, + } + ) + + cls.rma_line_2 = cls.env["rma.order.line"].create( + { + "partner_id": cls.partner.id, + "requested_by": False, + "assigned_to": False, + "type": "supplier", + "product_id": cls.product.id, + "uom_id": cls.product.uom_id.id, + "product_qty": 1, + "price_unit": 10, + "operation_id": cls.operation2.id, + "delivery_address_id": cls.partner.id, + "receipt_policy": cls.operation2.receipt_policy, + "delivery_policy": cls.operation2.delivery_policy, + "in_warehouse_id": cls.operation2.in_warehouse_id.id, + "out_warehouse_id": cls.operation2.out_warehouse_id.id, + "in_route_id": cls.operation2.in_route_id.id, + "out_route_id": cls.operation2.out_route_id.id, + "location_id": cls.operation2.location_id.id, + } + ) + cls.env["rma.reason.code"].search([]).unlink() + cls.reason_code_both = cls.env["rma.reason.code"].create( + { + "name": "Test Code 1", + "description": "Test description", + "type": "both", + } + ) + cls.reason_code_customer = cls.env["rma.reason.code"].create( + { + "name": "Test Code 2", + "description": "Test description", + "type": "customer", + } + ) + cls.reason_code_supplier = cls.env["rma.reason.code"].create( + { + "name": "Test Code 3", + "description": "Test description", + "type": "supplier", + } + ) + + def test_01_reason_code_customer(self): + self.rma_line_1.action_rma_to_approve() + self.assertEqual( + self.rma_line_1.allowed_reason_code_ids.ids, + [self.reason_code_both.id, self.reason_code_customer.id], + ) + with self.assertRaises(ValidationError): + self.rma_line_1.write( + { + "reason_code_ids": [self.reason_code_supplier.id], + } + ) + self.rma_line_1.write( + { + "reason_code_ids": [self.reason_code_customer.id], + } + ) + + def test_02_reason_code_supplier(self): + self.rma_line_2.action_rma_to_approve() + self.assertEqual( + self.rma_line_2.allowed_reason_code_ids.ids, + [self.reason_code_both.id, self.reason_code_supplier.id], + ) + with self.assertRaises(ValidationError): + self.rma_line_2.write( + { + "reason_code_ids": [self.reason_code_customer.id], + } + ) + self.rma_line_2.write( + { + "reason_code_ids": [self.reason_code_supplier.id], + } + ) diff --git a/rma_reason_code/views/reason_code_view.xml b/rma_reason_code/views/reason_code_view.xml new file mode 100644 index 00000000..60bc433c --- /dev/null +++ b/rma_reason_code/views/reason_code_view.xml @@ -0,0 +1,55 @@ + + + + + + rma.reason.code.form + rma.reason.code + +
+ +
+

+
+ + + + + +
+
+
+
+ + rma.reason.code.list + rma.reason.code + + + + + + + + + + + RMA Reason Codes + rma.reason.code + tree,form + + +
diff --git a/rma_reason_code/views/rma_order_line_views.xml b/rma_reason_code/views/rma_order_line_views.xml new file mode 100644 index 00000000..9e010d0a --- /dev/null +++ b/rma_reason_code/views/rma_order_line_views.xml @@ -0,0 +1,74 @@ + + + + + + rma.order.line.tree - rma_reason_code + rma.order.line + + + + + + + + + + + rma.order.line.supplier.tree - rma_reason_code + rma.order.line + + + + + + + + + + + rma.order.line.form - rma_reason_code + rma.order.line + + + + + + + + + + + rma.order.line.search - rma_reason_code + rma.order.line + + + + + + + + + + + + diff --git a/setup/rma_reason_code/odoo/addons/rma_reason_code b/setup/rma_reason_code/odoo/addons/rma_reason_code new file mode 120000 index 00000000..ce20f13d --- /dev/null +++ b/setup/rma_reason_code/odoo/addons/rma_reason_code @@ -0,0 +1 @@ +../../../../rma_reason_code \ No newline at end of file diff --git a/setup/rma_reason_code/setup.py b/setup/rma_reason_code/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/rma_reason_code/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)