diff --git a/rma_reason_code/README.rst b/rma_reason_code/README.rst new file mode 100644 index 000000000..cd5604c59 --- /dev/null +++ b/rma_reason_code/README.rst @@ -0,0 +1,61 @@ +=============== +RMA Reason Code +=============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:7fe458ca9d7af3377506c22b519e03a0a8175169e7939409a36821718e03392a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-ForgeFlow%2Fstock--rma-lightgray.png?logo=github + :target: https://github.com/ForgeFlow/stock-rma/tree/17.0/rma_reason_code + :alt: ForgeFlow/stock-rma + +|badge1| |badge2| |badge3| + +Adds a reason code for RMA operations and an interface for the user to +create RMA codes + +**Table of contents** + +.. contents:: + :local: + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ForgeFlow + +Contributors +------------ + +- David Jiménez + +Maintainers +----------- + +This module is part of the `ForgeFlow/stock-rma `_ project on GitHub. + +You are welcome to contribute. diff --git a/rma_reason_code/__init__.py b/rma_reason_code/__init__.py new file mode 100644 index 000000000..c88b5c5a7 --- /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 000000000..bd9dd6d45 --- /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": "17.0.1.0.0", + "license": "AGPL-3", + "summary": "Reason code for RMA", + "author": "ForgeFlow", + "website": "https://github.com/ForgeFlow", + "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/migrations/17.0.1.0.0/pre-migration.py b/rma_reason_code/migrations/17.0.1.0.0/pre-migration.py new file mode 100644 index 000000000..2acbcdfbd --- /dev/null +++ b/rma_reason_code/migrations/17.0.1.0.0/pre-migration.py @@ -0,0 +1,17 @@ +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + if openupgrade.column_exists(env.cr, "rma_reason_code", "type"): + openupgrade.rename_fields( + env, + [ + ( + "rma.reason.code", + "rma_reason_code", + "type", + "rma_type", + ) + ], + ) diff --git a/rma_reason_code/models/__init__.py b/rma_reason_code/models/__init__.py new file mode 100644 index 000000000..915e24afb --- /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 000000000..b098241ae --- /dev/null +++ b/rma_reason_code/models/reason_code.py @@ -0,0 +1,27 @@ +# 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() + rma_type = fields.Selection( + [ + ("customer", "Customer RMA"), + ("supplier", "Supplier RTV"), + ("both", "Both Customer and Supplier"), + ], + default="both", + string="RMA Type", + required=True, + ) + color = fields.Integer(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 000000000..9167f331f --- /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([("rma_type", "in", ["customer", "both"])]) + else: + codes = codes.search([("rma_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/pyproject.toml b/rma_reason_code/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/rma_reason_code/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/rma_reason_code/readme/CONTRIBUTORS.md b/rma_reason_code/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..ac18283b8 --- /dev/null +++ b/rma_reason_code/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- David Jiménez \<\> diff --git a/rma_reason_code/readme/DESCRIPTION.md b/rma_reason_code/readme/DESCRIPTION.md new file mode 100644 index 000000000..be1008fa8 --- /dev/null +++ b/rma_reason_code/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +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 000000000..ac4253fc9 --- /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 000000000..b00fd1c29 --- /dev/null +++ b/rma_reason_code/reports/rma_reason_code_report.py @@ -0,0 +1,54 @@ +# 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") + rma_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 AS 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 f""" + {self._select()} + {self._from()} + {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 000000000..b7f58fe42 --- /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 000000000..11b0aac1c --- /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 000000000..ad97b8b0c --- /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 000000000..3a0328b51 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 000000000..d4b9528ac --- /dev/null +++ b/rma_reason_code/static/description/index.html @@ -0,0 +1,416 @@ + + + + + +RMA Reason Code + + + +
+

RMA Reason Code

+ + +

Beta License: AGPL-3 ForgeFlow/stock-rma

+

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

+

Table of contents

+ +
+

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 to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • ForgeFlow
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the ForgeFlow/stock-rma project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/rma_reason_code/tests/__init__.py b/rma_reason_code/tests/__init__.py new file mode 100644 index 000000000..a493ff1fc --- /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 000000000..904214baf --- /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.TransactionCase): + @classmethod + def setUpClass(cls): + super().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", + "rma_type": "both", + } + ) + cls.reason_code_customer = cls.env["rma.reason.code"].create( + { + "name": "Test Code 2", + "description": "Test description", + "rma_type": "customer", + } + ) + cls.reason_code_supplier = cls.env["rma.reason.code"].create( + { + "name": "Test Code 3", + "description": "Test description", + "rma_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 000000000..242f81235 --- /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 000000000..e931d23d4 --- /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 + + + + + + + + + + + +