diff --git a/rma_operating_unit/README.rst b/rma_operating_unit/README.rst new file mode 100644 index 00000000..700136ef --- /dev/null +++ b/rma_operating_unit/README.rst @@ -0,0 +1,34 @@ +.. image:: https://img.shields.io/badge/license-LGPLv3-blue.svg + :target: https://www.gnu.org/licenses/lgpl.html + :alt: License: LGPL-3 + +======================== +RMA with Operating Units +======================== + +This module introduces the following features: + +* Adds the Operating Unit (OU) to the RMA order. + +* Users can only view and manage the rma orders associated to their operating + unit. + + +Usage +===== + +* The default operating unit of the RMA comes from the default operating unit + of the user + + +Contributors +------------ + +* Aaron Henriquez +* BeƱat Jimenez +* Juany Davila + +Maintainer +---------- + +This module is maintained by ForgeFlow. diff --git a/rma_operating_unit/__init__.py b/rma_operating_unit/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/rma_operating_unit/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/rma_operating_unit/__manifest__.py b/rma_operating_unit/__manifest__.py new file mode 100644 index 00000000..c89cc576 --- /dev/null +++ b/rma_operating_unit/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2017-23 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +{ + "name": "Operating Unit in RMA Groups", + "version": "15.0.1.0.0", + "author": "ForgeFlow", + "license": "LGPL-3", + "website": "https://github.com/ForgeFlow/stock-rma", + "category": "Operating Units", + "depends": ["rma", "stock_operating_unit"], + "data": [ + "security/rma_security.xml", + "views/rma_order_view.xml", + "views/rma_order_line_view.xml", + ], +} diff --git a/rma_operating_unit/models/__init__.py b/rma_operating_unit/models/__init__.py new file mode 100644 index 00000000..1bbd239d --- /dev/null +++ b/rma_operating_unit/models/__init__.py @@ -0,0 +1,2 @@ +from . import rma_order +from . import rma_order_line diff --git a/rma_operating_unit/models/rma_order.py b/rma_operating_unit/models/rma_order.py new file mode 100644 index 00000000..2eb0889e --- /dev/null +++ b/rma_operating_unit/models/rma_order.py @@ -0,0 +1,35 @@ +# Copyright 2017-23 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class RmaOrder(models.Model): + + _inherit = "rma.order" + + @api.constrains("rma_line_ids", "rma_line_ids.operating_unit_id") + def _check_operating_unit(self): + for rma in self: + bad_lines = rma.rma_line_ids.filtered( + lambda l: l.operating_unit_id != rma.operating_unit_id + ) + if bad_lines: + raise ValidationError( + _( + "The operating unit of the rma lines have to match the" + " one of the group" + ) + ) + return True + + @api.model + def _default_operating_unit(self): + return self.env.user.default_operating_unit_id + + operating_unit_id = fields.Many2one( + comodel_name="operating.unit", + string="Operating Unit", + default=_default_operating_unit, + ) diff --git a/rma_operating_unit/models/rma_order_line.py b/rma_operating_unit/models/rma_order_line.py new file mode 100644 index 00000000..f7cf802c --- /dev/null +++ b/rma_operating_unit/models/rma_order_line.py @@ -0,0 +1,21 @@ +# Copyright 2017-23 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + + +class RmaOrderLine(models.Model): + + _inherit = "rma.order.line" + + @api.model + def _default_operating_unit(self): + if self.rma_id.operating_unit_id: + return self.rma_id.operating_unit_id.id + return self.env.user.default_operating_unit_id + + operating_unit_id = fields.Many2one( + comodel_name="operating.unit", + string="Operating Unit", + default=_default_operating_unit, + ) diff --git a/rma_operating_unit/security/rma_security.xml b/rma_operating_unit/security/rma_security.xml new file mode 100644 index 00000000..f6553097 --- /dev/null +++ b/rma_operating_unit/security/rma_security.xml @@ -0,0 +1,16 @@ + + + + + + ['|', ('operating_unit_id', '=', False), + ('operating_unit_id', 'in', [g.id for g in user.operating_unit_ids])] + RMA from allowed operating units + + + + + + + diff --git a/rma_operating_unit/tests/__init__.py b/rma_operating_unit/tests/__init__.py new file mode 100644 index 00000000..d588b49b --- /dev/null +++ b/rma_operating_unit/tests/__init__.py @@ -0,0 +1 @@ +from . import test_rma_operating_unit diff --git a/rma_operating_unit/tests/test_rma_operating_unit.py b/rma_operating_unit/tests/test_rma_operating_unit.py new file mode 100644 index 00000000..fbcabc28 --- /dev/null +++ b/rma_operating_unit/tests/test_rma_operating_unit.py @@ -0,0 +1,138 @@ +# Copyright 2017-23 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). +from odoo import exceptions +from odoo.tests import common + + +class TestRmaOperatingUnit(common.TransactionCase): + def setUp(self): + super(TestRmaOperatingUnit, self).setUp() + self.res_users_model = self.env["res.users"] + self.rma_model = self.env["rma.order"] + self.rma_line_model = self.env["rma.order.line"] + + self.company = self.env.ref("base.main_company") + self.partner = self.env.ref("base.res_partner_1") + self.grp_rma_manager = self.env.ref("rma.group_rma_manager") + self.grp_ou = self.env.ref("operating_unit.group_multi_operating_unit") + self.grp_stock = self.env.ref("stock.group_stock_manager") + self.product = self.env.ref("product.product_product_12") + + # Main Operating Unit + self.main_OU = self.env.ref("operating_unit.main_operating_unit") + # B2C Operating Unit + self.b2c_OU = self.env.ref("operating_unit.b2c_operating_unit") + + # Users + self.user1 = self._create_user( + "user_1", + [self.grp_rma_manager, self.grp_ou, self.grp_stock], + self.company, + [self.main_OU, self.b2c_OU], + ) + self.user2 = self._create_user( + "user_2", + [self.grp_rma_manager, self.grp_ou, self.grp_stock], + self.company, + [self.b2c_OU], + ) + self.user3 = self._create_user( + "user_3", + [self.grp_rma_manager, self.grp_ou, self.grp_stock], + self.company, + [self.main_OU, self.b2c_OU], + ) + + # RMA Orders + self.rma_order1 = self._create_rma(self.user1.id, self.main_OU) + self.rma_order2 = self._create_rma(self.user2.id, self.b2c_OU) + self.rma_order3 = self._create_rma(self.user3.id) + + def _create_user(self, login, groups, company, operating_units): + """Creates a user.""" + group_ids = [group.id for group in groups] + user = self.res_users_model.create( + { + "name": login, + "login": login, + "password": "demo", + "email": "example@yourcompany.com", + "company_id": company.id, + "company_ids": [(4, company.id)], + "operating_unit_ids": [(4, ou.id) for ou in operating_units], + "groups_id": [(6, 0, group_ids)], + } + ) + return user + + def _create_rma(self, uid, operating_unit=False): + """Creates an RMA""" + if not operating_unit: + operating_unit = ( + self.rma_model.sudo().with_user(uid)._default_operating_unit() + ) + rma_order = ( + self.rma_model.sudo() + .with_user(uid) + .create( + { + "operating_unit_id": operating_unit.id, + "partner_id": self.partner.id, + } + ) + ) + return rma_order + + def _create_rma_line(self, rma, uid, operating_unit): + """Creates an RMA""" + rma_order_line = ( + self.rma_line_model.sudo() + .with_user(uid) + .create( + { + "operating_unit_id": operating_unit.id, + "operation_id": self.env.ref( + "rma.rma_operation_supplier_replace" + ).id, + "rma_id": rma.id, + "partner_id": self.partner.id, + "in_route_id": 1, + "out_route_id": 1, + "in_warehouse_id": 1, + "out_warehouse_id": 1, + "location_id": 1, + "receipt_policy": "ordered", + "delivery_policy": "ordered", + "name": self.product.name, + "product_id": self.product.id, + "uom_id": self.product.uom_id.id, + } + ) + ) + return rma_order_line + + def test_security(self): + # User 2 is only assigned to Operating Unit B2C, and cannot + # access RMA of Main Operating Unit. + record = ( + self.rma_model.sudo() + .with_user(self.user2.id) + .search( + [ + ("id", "=", self.rma_order1.id), + ("operating_unit_id", "=", self.main_OU.id), + ] + ) + ) + self.assertEqual( + record.ids, + [], + "User 2 should not have access to " "OU %s." % self.main_OU.name, + ) + + def test_constraint(self): + # RMA group should contain rma lines for the same OU + with self.assertRaises(exceptions.ValidationError): + self._create_rma_line(self.rma_order1, self.user1.id, self.main_OU) + self._create_rma_line(self.rma_order1, self.user1.id, self.b2c_OU) + self.rma_order1._check_operating_unit() diff --git a/rma_operating_unit/views/rma_order_line_view.xml b/rma_operating_unit/views/rma_order_line_view.xml new file mode 100644 index 00000000..114be881 --- /dev/null +++ b/rma_operating_unit/views/rma_order_line_view.xml @@ -0,0 +1,84 @@ + + + + + rma.order.line.tree + rma.order.line + + + + + + + + + + rma.order.line.supplier.tree + rma.order.line + + + + + + + + + + rma.order.line.supplier.form + rma.order.line + + + + + + + [('type','=','supplier'),('out_warehouse_id.operating_unit_id', '=', operating_unit_id)] + + + + + + rma.order.line.form + rma.order.line + + + + + + + [('type','=','customer'),('in_warehouse_id.operating_unit_id', '=', operating_unit_id)] + + + + + + rma.order.line.select + rma.order.line + + + + + + + + diff --git a/rma_operating_unit/views/rma_order_view.xml b/rma_operating_unit/views/rma_order_view.xml new file mode 100644 index 00000000..0506fe96 --- /dev/null +++ b/rma_operating_unit/views/rma_order_view.xml @@ -0,0 +1,85 @@ + + + + + rma.order.tree + rma.order + + + + + + + + + + rma.order.supplier.tree + rma.order + + + + + + + + + + rma.order.supplier.form + rma.order + + + + + + + + + + + + + rma.order.form + rma.order + + + + + + + + + + + + + rma.order.select + rma.order + + + + + + + + + diff --git a/rma_operating_unit/wizards/rma_add_stock_move.py b/rma_operating_unit/wizards/rma_add_stock_move.py new file mode 100644 index 00000000..98fe8656 --- /dev/null +++ b/rma_operating_unit/wizards/rma_add_stock_move.py @@ -0,0 +1,45 @@ +# Copyright 2017-23 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import _, models +from odoo.exceptions import ValidationError + + +class RmaAddStockMove(models.TransientModel): + _inherit = "rma_add_stock_move" + _description = "Wizard to add rma lines from pickings" + + def _prepare_rma_line_from_stock_move(self, sm, lot=False): + res = super(RmaAddStockMove, self)._prepare_rma_line_from_stock_move(sm, lot) + if self.env.context.get("customer"): + operation = ( + sm.product_id.rma_customer_operation_id + or sm.product_id.categ_id.rma_customer_operation_id + ) + else: + operation = ( + sm.product_id.rma_supplier_operation_id + or sm.product_id.categ_id.rma_supplier_operation_id + ) + if not operation: + operation = self.env["rma.operation"].search( + [("type", "=", self.rma_id.type)], limit=1 + ) + if not operation: + raise ValidationError(_("Please define an operation first")) + + if not operation.in_warehouse_id or not operation.out_warehouse_id: + warehouse = self.env["stock.warehouse"].search( + [ + ("company_id", "=", self.rma_id.company_id.id), + ("lot_rma_id", "!=", False), + ("operating_unit_id", "=", self.rma_id.operating_unit_id.id), + ], + limit=1, + ) + if not warehouse: + raise ValidationError( + _("Please define a warehouse with a default RMA location") + ) + res.update(warehouse_id=warehouse.id) + return res diff --git a/setup/rma_operating_unit/odoo/addons/rma_operating_unit b/setup/rma_operating_unit/odoo/addons/rma_operating_unit new file mode 120000 index 00000000..dbb4c397 --- /dev/null +++ b/setup/rma_operating_unit/odoo/addons/rma_operating_unit @@ -0,0 +1 @@ +../../../../rma_operating_unit \ No newline at end of file diff --git a/setup/rma_operating_unit/setup.py b/setup/rma_operating_unit/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/rma_operating_unit/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)