From 1af730feace2317152902bf02832bad2e5b6303a Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 3 Jul 2020 09:04:44 -0700 Subject: [PATCH 1/6] [MOV] rma_sale: from Hibou Suite Enterprise for 13.0 --- rma_sale/__init__.py | 4 + rma_sale/__manifest__.py | 29 +++ rma_sale/data/rma_demo.xml | 18 ++ rma_sale/models/__init__.py | 5 + rma_sale/models/product.py | 16 ++ rma_sale/models/rma.py | 255 ++++++++++++++++++++++++++ rma_sale/models/sale.py | 15 ++ rma_sale/security/ir.model.access.csv | 7 + rma_sale/tests/__init__.py | 3 + rma_sale/tests/test_rma.py | 173 +++++++++++++++++ rma_sale/views/portal_templates.xml | 94 ++++++++++ rma_sale/views/product_views.xml | 30 +++ rma_sale/views/rma_views.xml | 77 ++++++++ rma_sale/views/sale_views.xml | 19 ++ rma_sale/wizard/__init__.py | 3 + rma_sale/wizard/rma_lines.py | 73 ++++++++ rma_sale/wizard/rma_lines_views.xml | 39 ++++ 17 files changed, 860 insertions(+) create mode 100644 rma_sale/__init__.py create mode 100644 rma_sale/__manifest__.py create mode 100644 rma_sale/data/rma_demo.xml create mode 100644 rma_sale/models/__init__.py create mode 100644 rma_sale/models/product.py create mode 100644 rma_sale/models/rma.py create mode 100644 rma_sale/models/sale.py create mode 100644 rma_sale/security/ir.model.access.csv create mode 100644 rma_sale/tests/__init__.py create mode 100644 rma_sale/tests/test_rma.py create mode 100644 rma_sale/views/portal_templates.xml create mode 100644 rma_sale/views/product_views.xml create mode 100644 rma_sale/views/rma_views.xml create mode 100644 rma_sale/views/sale_views.xml create mode 100644 rma_sale/wizard/__init__.py create mode 100644 rma_sale/wizard/rma_lines.py create mode 100644 rma_sale/wizard/rma_lines_views.xml diff --git a/rma_sale/__init__.py b/rma_sale/__init__.py new file mode 100644 index 00000000..c7120225 --- /dev/null +++ b/rma_sale/__init__.py @@ -0,0 +1,4 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import models +from . import wizard diff --git a/rma_sale/__manifest__.py b/rma_sale/__manifest__.py new file mode 100644 index 00000000..66961354 --- /dev/null +++ b/rma_sale/__manifest__.py @@ -0,0 +1,29 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +{ + 'name': 'Hibou RMAs for Sale Orders', + 'version': '12.0.1.1.0', + 'category': 'Sale', + 'author': "Hibou Corp.", + 'license': 'AGPL-3', + 'website': 'https://hibou.io/', + 'depends': [ + 'rma', + 'sale', + 'sales_team', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/portal_templates.xml', + 'views/product_views.xml', + 'views/rma_views.xml', + 'views/sale_views.xml', + 'wizard/rma_lines_views.xml', + ], + 'demo': [ + 'data/rma_demo.xml', + ], + 'installable': True, + 'auto_install': True, + 'application': False, + } diff --git a/rma_sale/data/rma_demo.xml b/rma_sale/data/rma_demo.xml new file mode 100644 index 00000000..24132ea6 --- /dev/null +++ b/rma_sale/data/rma_demo.xml @@ -0,0 +1,18 @@ + + + + + Sale Return + sale_order + + + + + + make_to_stock + + + + + + \ No newline at end of file diff --git a/rma_sale/models/__init__.py b/rma_sale/models/__init__.py new file mode 100644 index 00000000..d997816f --- /dev/null +++ b/rma_sale/models/__init__.py @@ -0,0 +1,5 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import product +from . import rma +from . import sale diff --git a/rma_sale/models/product.py b/rma_sale/models/product.py new file mode 100644 index 00000000..2e338a39 --- /dev/null +++ b/rma_sale/models/product.py @@ -0,0 +1,16 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + rma_sale_validity = fields.Integer(string='RMA Eligible Days (Sale)', + help='Determines the number of days from the time ' + 'of the sale that the product is eligible to ' + 'be returned. 0 (default) will allow the product ' + 'to be returned for an indefinite period of time. ' + 'A positive number will allow the product to be ' + 'returned up to that number of days. A negative ' + 'number prevents the return of the product.') diff --git a/rma_sale/models/rma.py b/rma_sale/models/rma.py new file mode 100644 index 00000000..03344398 --- /dev/null +++ b/rma_sale/models/rma.py @@ -0,0 +1,255 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, ValidationError +from datetime import timedelta + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + def _get_protected_fields(self): + res = super(SaleOrderLine, self)._get_protected_fields() + context = self._context or {} + if context.get('rma_done') and 'product_uom_qty' in res: + res.remove('product_uom_qty') + return res + + +class RMATemplate(models.Model): + _inherit = 'rma.template' + + usage = fields.Selection(selection_add=[('sale_order', 'Sale Order')]) + so_decrement_order_qty = fields.Boolean(string='SO Decrement Ordered Qty.', + help='When completing the RMA, the Ordered Quantity will be decremented by ' + 'the RMA qty.') + + def _portal_try_create(self, request_user, res_id, **kw): + if self.usage == 'sale_order': + prefix = 'line_' + line_map = {int(key[len(prefix):]): float(kw[key]) for key in kw if key.find(prefix) == 0 and kw[key]} + if line_map: + sale_order = self.env['sale.order'].with_user(request_user).browse(res_id) + if not sale_order.exists(): + raise ValidationError('Invalid user for sale order.') + lines = [] + sale_order_sudo = sale_order.sudo() + for line_id, qty in line_map.items(): + line = sale_order_sudo.order_line.filtered(lambda l: l.id == line_id) + if line: + if not qty: + continue + if qty < 0.0 or line.qty_delivered < qty: + raise ValidationError('Invalid quantity.') + validity = self._rma_sale_line_validity(line) + if not validity: + raise ValidationError('Product is not eligible for return.') + if validity == 'expired': + raise ValidationError('Product is past the return period.') + lines.append((0, 0, { + 'product_id': line.product_id.id, + 'product_uom_id': line.product_uom.id, + 'product_uom_qty': qty, + })) + if not lines: + raise ValidationError('Missing product quantity.') + rma = self.env['rma.rma'].create({ + 'name': _('New'), + 'sale_order_id': sale_order.id, + 'template_id': self.id, + 'partner_id': sale_order.partner_id.id, + 'partner_shipping_id': sale_order.partner_shipping_id.id, + 'lines': lines, + }) + return rma + return super(RMATemplate, self)._portal_try_create(request_user, res_id, **kw) + + def _portal_template(self, res_id=None): + if self.usage == 'sale_order': + return 'rma_sale.portal_new_sale_order' + return super(RMATemplate, self)._portal_template(res_id=res_id) + + def _portal_values(self, request_user, res_id=None): + if self.usage == 'sale_order': + sale_orders = None + sale_order = None + if res_id: + sale_order = self.env['sale.order'].with_user(request_user).browse(res_id) + if sale_order: + sale_order = sale_order.sudo() + else: + sale_orders = self.env['sale.order'].with_user(request_user).search([], limit=100) + return { + 'rma_template': self, + 'rma_sale_orders': sale_orders, + 'rma_sale_order': sale_order, + } + return super(RMATemplate, self)._portal_values(request_user, res_id=res_id) + + def _rma_sale_line_validity(self, so_line): + validity_days = so_line.product_id.rma_sale_validity + if validity_days < 0: + return '' + elif validity_days > 0: + sale_date = so_line.order_id.date_order + now = fields.Datetime.now() + if sale_date < (now - timedelta(days=validity_days)): + return 'expired' + return 'valid' + + +class RMA(models.Model): + _inherit = 'rma.rma' + + sale_order_id = fields.Many2one('sale.order', string='Sale Order') + sale_order_rma_count = fields.Integer('Number of RMAs for this Sale Order', compute='_compute_sale_order_rma_count') + company_id = fields.Many2one('res.company', 'Company', + default=lambda self: self.env.company) + + @api.depends('sale_order_id') + def _compute_sale_order_rma_count(self): + for rma in self: + if rma.sale_order_id: + rma_data = self.read_group([('sale_order_id', '=', rma.sale_order_id.id), ('state', '!=', 'cancel')], + ['sale_order_id'], ['sale_order_id']) + if rma_data: + rma.sale_order_rma_count = rma_data[0]['sale_order_id_count'] + else: + rma.sale_order_rma_count = 0.0 + else: + rma.sale_order_rma_count = 0.0 + + def open_sale_order_rmas(self): + return { + 'type': 'ir.actions.act_window', + 'name': _('Sale Order RMAs'), + 'res_model': 'rma.rma', + 'view_mode': 'tree,form', + 'context': {'search_default_sale_order_id': self[0].sale_order_id.id} + } + + @api.onchange('template_usage') + def _onchange_template_usage(self): + res = super(RMA, self)._onchange_template_usage() + for rma in self.filtered(lambda rma: rma.template_usage != 'sale_order'): + rma.sale_order_id = False + return res + + @api.onchange('sale_order_id') + def _onchange_sale_order_id(self): + for rma in self.filtered(lambda rma: rma.sale_order_id): + rma.partner_id = rma.sale_order_id.partner_id + rma.partner_shipping_id = rma.sale_order_id.partner_shipping_id + + def action_done(self): + res = super(RMA, self).action_done() + res2 = self._so_action_done() + if isinstance(res, dict) and isinstance(res2, dict): + if 'warning' in res and 'warning' in res2: + res['warning'] = '\n'.join([res['warning'], res2['warning']]) + return res + if 'warning' in res2: + res['warning'] = res2['warning'] + return res + elif isinstance(res2, dict): + return res2 + return res + + def _so_action_done(self): + warnings = [] + for rma in self: + sale_orders = rma.sale_order_id + if rma.template_id.so_decrement_order_qty: + sale_orders = self.env['sale.order'].browse() + for rma_line in rma.lines: + so_lines = rma.sale_order_id.order_line.filtered(lambda l: l.product_id == rma_line.product_id) + qty_remaining = rma_line.product_uom_qty + for sale_line in so_lines: + if qty_remaining == 0: + continue + sale_line_qty = sale_line.product_uom_qty + sale_line_qty = sale_line_qty - qty_remaining + if sale_line_qty < 0: + qty_remaining = abs(sale_line_qty) + sale_line_qty = 0 + else: + qty_remaining = 0 + sale_line.with_context(rma_done=True).write({'product_uom_qty': sale_line_qty}) + sale_orders += sale_line.order_id + if qty_remaining: + warnings.append((rma, rma.sale_order_id, rma_line, qty_remaining)) + # Try to invoice if we don't already have an invoice (e.g. from resetting to draft) + if sale_orders and rma.template_id.invoice_done and not rma.invoice_ids: + rma.invoice_ids += rma._sale_invoice_done(sale_orders) + if warnings: + return {'warning': _('Could not reduce all ordered qty:\n %s' % '\n'.join( + ['%s %s %s : %s' % (w[0].name, w[1].name, w[2].product_id.display_name, w[3]) for w in warnings]))} + return True + + def _sale_invoice_done(self, sale_orders): + original_invoices = sale_orders.mapped('invoice_ids') + try: + wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=sale_orders.ids).create({}) + wiz.create_invoices() + except UserError: + pass + return sale_orders.mapped('invoice_ids') - original_invoices + + def action_add_so_lines(self): + make_line_obj = self.env['rma.sale.make.lines'] + for rma in self: + lines = make_line_obj.create({ + 'rma_id': rma.id, + }) + action = self.env.ref('rma_sale.action_rma_add_lines').read()[0] + action['res_id'] = lines.id + return action + + def _create_in_picking_sale_order(self): + if not self.sale_order_id: + raise UserError(_('You must have a sale order for this RMA.')) + if not self.template_id.in_require_return: + group_id = self.sale_order_id.procurement_group_id.id if self.sale_order_id.procurement_group_id else 0 + sale_id = self.sale_order_id.id + values = self.template_id._values_for_in_picking(self) + update = {'sale_id': sale_id, 'group_id': group_id} + update_lines = {'to_refund': self.template_id.in_to_refund, 'group_id': group_id} + return self._picking_from_values(values, update, update_lines) + + lines = self.lines.filtered(lambda l: l.product_uom_qty >= 1 and l.product_id.type != 'service') + if not lines: + raise UserError(_('You have no lines with positive quantity.')) + product_ids = lines.mapped('product_id.id') + + old_picking = self._find_candidate_return_picking(product_ids, self.sale_order_id.picking_ids, self.template_id.in_location_id.id) + if not old_picking: + raise UserError('No eligible pickings were found to return (you can only return products from the same initial picking).') + + new_picking = self._new_in_picking(old_picking) + self._new_in_moves(old_picking, new_picking, {}) + return new_picking + + def _create_out_picking_sale_order(self): + if not self.sale_order_id: + raise UserError(_('You must have a sale order for this RMA.')) + if not self.template_id.out_require_return: + group_id = self.sale_order_id.procurement_group_id.id if self.sale_order_id.procurement_group_id else 0 + sale_id = self.sale_order_id.id + values = self.template_id._values_for_out_picking(self) + update = {'sale_id': sale_id, 'group_id': group_id} + update_lines = {'group_id': group_id} + return self._picking_from_values(values, update, update_lines) + + lines = self.lines.filtered(lambda l: l.product_uom_qty >= 1 and l.product_id.type != 'service') + if not lines: + raise UserError(_('You have no lines with positive quantity.')) + product_ids = lines.mapped('product_id.id') + + old_picking = self._find_candidate_return_picking(product_ids, self.sale_order_id.picking_ids, self.template_id.out_location_dest_id.id) + if not old_picking: + raise UserError( + 'No eligible pickings were found to duplicate (you can only return products from the same initial picking).') + + new_picking = self._new_out_picking(old_picking) + self._new_out_moves(old_picking, new_picking, {}) + return new_picking diff --git a/rma_sale/models/sale.py b/rma_sale/models/sale.py new file mode 100644 index 00000000..66abb68d --- /dev/null +++ b/rma_sale/models/sale.py @@ -0,0 +1,15 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + rma_count = fields.Integer(string='RMA Count', compute='_compute_rma_count', compute_sudo=True) + rma_ids = fields.One2many('rma.rma', 'sale_order_id', string='RMAs') + + @api.depends('rma_ids') + def _compute_rma_count(self): + for so in self: + so.rma_count = len(so.rma_ids) diff --git a/rma_sale/security/ir.model.access.csv b/rma_sale/security/ir.model.access.csv new file mode 100644 index 00000000..ccb878eb --- /dev/null +++ b/rma_sale/security/ir.model.access.csv @@ -0,0 +1,7 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"manage_rma sale","manage rma","rma.model_rma_rma","sales_team.group_sale_salesman",1,1,1,1 +"manage_rma_line sale","manage rma line","rma.model_rma_line","sales_team.group_sale_salesman",1,1,1,1 +"manage_rma_template sale","manage rma template","rma.model_rma_template","sales_team.group_sale_manager",1,1,1,1 +"manage_rma_tag sale","manage rma tag","rma.model_rma_tag","sales_team.group_sale_manager",1,1,1,1 +"access_rma_template sale","access rma template","rma.model_rma_template","sales_team.group_sale_salesman",1,1,0,0 +"access_rma_tag sale","access rma tag","rma.model_rma_tag","sales_team.group_sale_salesman",1,0,0,0 \ No newline at end of file diff --git a/rma_sale/tests/__init__.py b/rma_sale/tests/__init__.py new file mode 100644 index 00000000..586c5532 --- /dev/null +++ b/rma_sale/tests/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import test_rma diff --git a/rma_sale/tests/test_rma.py b/rma_sale/tests/test_rma.py new file mode 100644 index 00000000..446260d6 --- /dev/null +++ b/rma_sale/tests/test_rma.py @@ -0,0 +1,173 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo.addons.rma.tests.test_rma import TestRMA +from odoo.exceptions import UserError, ValidationError +from datetime import timedelta + + +class TestRMASale(TestRMA): + + def setUp(self): + super(TestRMASale, self).setUp() + self.template_sale_return = self.env.ref('rma_sale.template_sale_return') + # Make it possible to "see all sale orders", but not be a manager (as managers can RMA ineligible lines) + self.user1.groups_id += self.env.ref('sales_team.group_sale_salesman_all_leads') + + def test_20_sale_return(self): + self.template_sale_return.write({ + 'usage': 'sale_order', + 'invoice_done': True, + }) + self.product1.write({ + 'type': 'product', + 'invoice_policy': 'delivery', + 'tracking': 'serial', + }) + order = self.env['sale.order'].create({ + 'partner_id': self.partner1.id, + 'partner_invoice_id': self.partner1.id, + 'partner_shipping_id': self.partner1.id, + 'order_line': [(0, 0, { + 'product_id': self.product1.id, + 'product_uom_qty': 1.0, + 'product_uom': self.product1.uom_id.id, + 'price_unit': 10.0, + })] + }) + order.action_confirm() + self.assertTrue(order.state in ('sale', 'done')) + self.assertEqual(len(order.picking_ids), 1, 'Tests only run with single stage delivery.') + + # Try to RMA item not delivered yet + rma = self.env['rma.rma'].create({ + 'template_id': self.template_sale_return.id, + 'partner_id': self.partner1.id, + 'partner_shipping_id': self.partner1.id, + 'sale_order_id': order.id, + }) + self.assertEqual(rma.state, 'draft') + # Do not allow return. + self.product1.rma_sale_validity = -1 + wizard = self.env['rma.sale.make.lines'].with_user(self.user1).create({'rma_id': rma.id}) + self.assertEqual(wizard.line_ids.qty_delivered, 0.0) + wizard.line_ids.product_uom_qty = 1.0 + + with self.assertRaises(UserError): + wizard.add_lines() + + # Allows returns, but not forever + self.product1.rma_sale_validity = 5 + original_date_order = order.date_order + order.write({'date_order': original_date_order - timedelta(days=6)}) + wizard = self.env['rma.sale.make.lines'].with_user(self.user1).create({'rma_id': rma.id}) + self.assertEqual(wizard.line_ids.qty_delivered, 0.0) + wizard.line_ids.product_uom_qty = 1.0 + with self.assertRaises(UserError): + wizard.add_lines() + + # Allows returns due to date + order.write({'date_order': original_date_order}) + wizard = self.env['rma.sale.make.lines'].with_user(self.user1).create({'rma_id': rma.id}) + self.assertEqual(wizard.line_ids.qty_delivered, 0.0) + wizard.line_ids.product_uom_qty = 1.0 + wizard.add_lines() + + self.assertEqual(len(rma.lines), 1) + with self.assertRaises(UserError): + rma.action_confirm() + + order.picking_ids.action_assign() + pack_opt = order.picking_ids.move_line_ids[0] + lot = self.env['stock.production.lot'].create({ + 'product_id': self.product1.id, + 'name': 'X100', + 'product_uom_id': self.product1.uom_id.id, + 'company_id': self.env.user.company_id.id, + }) + pack_opt.qty_done = 1.0 + pack_opt.lot_id = lot + order.picking_ids.button_validate() + self.assertEqual(order.picking_ids.state, 'done') + + # Invoice order so that the return is invoicable + wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=order.ids).create({}) + wiz.create_invoices() + # Odoo 13 Not flushing the order here will cause delivered_qty to be incorrect later + order.flush() + self.assertTrue(order.invoice_ids, 'Order did not create an invoice.') + + wizard = self.env['rma.sale.make.lines'].create({ + 'rma_id': rma.id, + }) + self.assertEqual(wizard.line_ids.qty_delivered, 1.0) + + # Confirm RMA + rma.action_confirm() + self.assertEqual(rma.in_picking_id.state, 'assigned') + pack_opt = rma.in_picking_id.move_line_ids[0] + + with self.assertRaises(UserError): + rma.action_done() + + pack_opt.lot_id = lot + pack_opt.qty_done = 1.0 + rma.in_picking_id.button_validate() + self.assertEqual(rma.in_picking_id.move_lines.sale_line_id, order.order_line) + self.assertEqual(rma.in_picking_id.state, 'done') + for move in order.order_line.mapped('move_ids'): + # Additional testing like this may not be needed in the future. Was added troubleshooting new 13 ORM + self.assertEqual(move.state, 'done', 'Move not done ' + str(move.name)) + self.assertEqual(order.order_line.qty_delivered, 0.0) + rma.action_done() + self.assertEqual(order.order_line.qty_delivered, 0.0) + + # Finishing the RMA should have made an invoice + self.assertTrue(rma.invoice_ids, 'Finishing RMA did not create an invoice(s).') + + # Test Ordered Qty was decremented. + self.assertEqual(order.order_line.product_uom_qty, 0.0) + self.assertEqual(order.order_line.qty_delivered, 0.0) + + # Make another RMA for the same sale order + rma2 = self.env['rma.rma'].create({ + 'template_id': self.template_sale_return.id, + 'partner_id': self.partner1.id, + 'partner_shipping_id': self.partner1.id, + 'sale_order_id': order.id, + }) + wizard = self.env['rma.sale.make.lines'].create({ + 'rma_id': rma2.id, + }) + # The First completed RMA will have "un-delivered" it for invoicing purposes. + self.assertEqual(wizard.line_ids.qty_delivered, 0.0) + wizard.line_ids.product_uom_qty = 1.0 + wizard.add_lines() + self.assertEqual(len(rma2.lines), 1) + + rma2.action_confirm() + + # In Odoo 10, this would not have been able to reserve. + # In Odoo 11, reservation can still happen, but at least we can't move the same lot twice! + #self.assertEqual(rma2.in_picking_id.state, 'confirmed') + + # Requires Lot + with self.assertRaises(UserError): + rma2.in_picking_id.move_line_ids.write({'qty_done': 1.0}) + rma2.in_picking_id.button_validate() + + self.assertTrue(rma2.in_picking_id.move_line_ids) + self.assertFalse(rma2.in_picking_id.move_line_ids.lot_id.name) + + # Assign existing lot + rma2.in_picking_id.move_line_ids.write({ + 'lot_id': lot.id, + 'qty_done': 1.0, + }) + + # Existing lot cannot be re-used. + with self.assertRaises(ValidationError): + rma2.in_picking_id.action_done() + + # RMA cannot be completed because the inbound picking state is confirmed + with self.assertRaises(UserError): + rma2.action_done() diff --git a/rma_sale/views/portal_templates.xml b/rma_sale/views/portal_templates.xml new file mode 100644 index 00000000..6340352d --- /dev/null +++ b/rma_sale/views/portal_templates.xml @@ -0,0 +1,94 @@ + + + + + + + + + diff --git a/rma_sale/views/product_views.xml b/rma_sale/views/product_views.xml new file mode 100644 index 00000000..4fa17000 --- /dev/null +++ b/rma_sale/views/product_views.xml @@ -0,0 +1,30 @@ + + + + + product.template.product.form.inherit + product.template + + + + + + + + + + + + product.product.form.inherit + product.product + + + + + + + + + + + diff --git a/rma_sale/views/rma_views.xml b/rma_sale/views/rma_views.xml new file mode 100644 index 00000000..f97ce8a8 --- /dev/null +++ b/rma_sale/views/rma_views.xml @@ -0,0 +1,77 @@ + + + + + + rma.template.form.sale + rma.template + + + + + + + + + + + rma.rma.form.sale + rma.rma + + + + + + + +
+ +
+
+
+
\ No newline at end of file diff --git a/rma_sale/wizard/__init__.py b/rma_sale/wizard/__init__.py new file mode 100644 index 00000000..1cbabc08 --- /dev/null +++ b/rma_sale/wizard/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import rma_lines diff --git a/rma_sale/wizard/rma_lines.py b/rma_sale/wizard/rma_lines.py new file mode 100644 index 00000000..c0733624 --- /dev/null +++ b/rma_sale/wizard/rma_lines.py @@ -0,0 +1,73 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class RMASaleMakeLines(models.TransientModel): + _name = 'rma.sale.make.lines' + _description = 'Add SO Lines' + + rma_id = fields.Many2one('rma.rma', string='RMA') + line_ids = fields.One2many('rma.sale.make.lines.line', 'rma_make_lines_id', string='Lines') + + + @api.model + def create(self, vals): + maker = super(RMASaleMakeLines, self).create(vals) + maker._create_lines() + return maker + + def _line_values(self, so_line): + return { + 'rma_make_lines_id': self.id, + 'product_id': so_line.product_id.id, + 'qty_ordered': so_line.product_uom_qty, + 'qty_delivered': so_line.qty_delivered, + 'qty_invoiced': so_line.qty_invoiced, + 'product_uom_qty': 0.0, + 'product_uom_id': so_line.product_uom.id, + 'validity': self.rma_id.template_id._rma_sale_line_validity(so_line), + } + + def _create_lines(self): + make_lines_obj = self.env['rma.sale.make.lines.line'] + + if self.rma_id.template_usage == 'sale_order' and self.rma_id.sale_order_id: + for l in self.rma_id.sale_order_id.order_line: + self.line_ids |= make_lines_obj.create(self._line_values(l)) + + def add_lines(self): + rma_line_obj = self.env['rma.line'] + for o in self: + lines = o.line_ids.filtered(lambda l: l.product_uom_qty > 0.0) + if not self.env.user.has_group('sales_team.group_sale_manager'): + if lines.filtered(lambda l: not l.validity): + raise UserError('One or more items are not eligible for return.') + if lines.filtered(lambda l: l.validity == 'expired'): + raise UserError('One or more items are past their return period.') + for l in lines: + rma_line_obj.create({ + 'rma_id': o.rma_id.id, + 'product_id': l.product_id.id, + 'product_uom_id': l.product_uom_id.id, + 'product_uom_qty': l.product_uom_qty, + }) + + +class RMASOMakeLinesLine(models.TransientModel): + _name = 'rma.sale.make.lines.line' + _description = 'RMA Sale Make Lines Line' + + rma_make_lines_id = fields.Many2one('rma.sale.make.lines') + product_id = fields.Many2one('product.product', string="Product") + qty_ordered = fields.Float(string='Ordered') + qty_invoiced = fields.Float(string='Invoiced') + qty_delivered = fields.Float(string='Delivered') + product_uom_qty = fields.Float(string='QTY') + product_uom_id = fields.Many2one('uom.uom', 'UOM') + validity = fields.Selection([ + ('', 'Not Eligible'), + ('valid', 'Eligible'), + ('expired', 'Expired'), + ], string='Validity') diff --git a/rma_sale/wizard/rma_lines_views.xml b/rma_sale/wizard/rma_lines_views.xml new file mode 100644 index 00000000..3f332f42 --- /dev/null +++ b/rma_sale/wizard/rma_lines_views.xml @@ -0,0 +1,39 @@ + + + + view.rma.add.lines.form + rma.sale.make.lines + form + +
+ + + + + + + + + + + +
+
+
+
+
+ + Add RMA Lines + rma.sale.make.lines + form + + new + +
\ No newline at end of file From 4451d193779f295266114717c8a3ca9fb6805f5a Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 3 Jul 2020 09:06:59 -0700 Subject: [PATCH 2/6] [FIX] rma_sale: license and version in manifest --- rma_sale/__manifest__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rma_sale/__manifest__.py b/rma_sale/__manifest__.py index 66961354..2cbf748b 100644 --- a/rma_sale/__manifest__.py +++ b/rma_sale/__manifest__.py @@ -2,10 +2,10 @@ { 'name': 'Hibou RMAs for Sale Orders', - 'version': '12.0.1.1.0', + 'version': '13.0.1.1.0', 'category': 'Sale', - 'author': "Hibou Corp.", - 'license': 'AGPL-3', + 'author': 'Hibou Corp.', + 'license': 'OPL-1', 'website': 'https://hibou.io/', 'depends': [ 'rma', From 3c0b22b47e196f963f84594574bf2eef8fb1b348 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 11 Sep 2020 14:25:40 -0700 Subject: [PATCH 3/6] [IMP] rma_sale: Release 13.0.1.2.0! Add additional support for Sale Warranty return/exchange. --- rma_sale/__manifest__.py | 2 +- rma_sale/models/product.py | 10 +++ rma_sale/models/rma.py | 12 ++- rma_sale/tests/test_rma.py | 135 +++++++++++++++++++++++++++- rma_sale/views/portal_templates.xml | 4 +- rma_sale/views/product_views.xml | 2 + rma_sale/views/rma_views.xml | 1 + 7 files changed, 161 insertions(+), 5 deletions(-) diff --git a/rma_sale/__manifest__.py b/rma_sale/__manifest__.py index 2cbf748b..e9fa3f5b 100644 --- a/rma_sale/__manifest__.py +++ b/rma_sale/__manifest__.py @@ -2,7 +2,7 @@ { 'name': 'Hibou RMAs for Sale Orders', - 'version': '13.0.1.1.0', + 'version': '13.0.1.2.0', 'category': 'Sale', 'author': 'Hibou Corp.', 'license': 'OPL-1', diff --git a/rma_sale/models/product.py b/rma_sale/models/product.py index 2e338a39..06308b76 100644 --- a/rma_sale/models/product.py +++ b/rma_sale/models/product.py @@ -14,3 +14,13 @@ class ProductTemplate(models.Model): 'A positive number will allow the product to be ' 'returned up to that number of days. A negative ' 'number prevents the return of the product.') + + rma_sale_warranty_validity = fields.Integer(string='RMA Eligible Days (Sale Warranty)', + help='Determines the number of days from the time ' + 'of the sale that the product is eligible to ' + 'be returned for warranty claims. ' + '0 (default) will allow the product to be ' + 'returned for an indefinite period of time. ' + 'A positive number will allow the product to be ' + 'returned up to that number of days. A negative ' + 'number prevents the return of the product.') diff --git a/rma_sale/models/rma.py b/rma_sale/models/rma.py index 03344398..fb800915 100644 --- a/rma_sale/models/rma.py +++ b/rma_sale/models/rma.py @@ -20,6 +20,9 @@ class RMATemplate(models.Model): _inherit = 'rma.template' usage = fields.Selection(selection_add=[('sale_order', 'Sale Order')]) + sale_order_warranty = fields.Boolean(string='Sale Order Warranty', + help='Determines if the regular return validity or ' + 'Warranty validity is used.') so_decrement_order_qty = fields.Boolean(string='SO Decrement Ordered Qty.', help='When completing the RMA, the Ordered Quantity will be decremented by ' 'the RMA qty.') @@ -87,7 +90,10 @@ class RMATemplate(models.Model): return super(RMATemplate, self)._portal_values(request_user, res_id=res_id) def _rma_sale_line_validity(self, so_line): - validity_days = so_line.product_id.rma_sale_validity + if self.sale_order_warranty: + validity_days = so_line.product_id.rma_sale_warranty_validity + else: + validity_days = so_line.product_id.rma_sale_validity if validity_days < 0: return '' elif validity_days > 0: @@ -195,6 +201,10 @@ class RMA(models.Model): pass return sale_orders.mapped('invoice_ids') - original_invoices + def _invoice_values_sale_order(self): + # the RMA invoice API will not be used as invoicing will happen at the SO level + return False + def action_add_so_lines(self): make_line_obj = self.env['rma.sale.make.lines'] for rma in self: diff --git a/rma_sale/tests/test_rma.py b/rma_sale/tests/test_rma.py index 446260d6..7cd4d329 100644 --- a/rma_sale/tests/test_rma.py +++ b/rma_sale/tests/test_rma.py @@ -167,7 +167,140 @@ class TestRMASale(TestRMA): # Existing lot cannot be re-used. with self.assertRaises(ValidationError): rma2.in_picking_id.action_done() - + # RMA cannot be completed because the inbound picking state is confirmed with self.assertRaises(UserError): rma2.action_done() + + def test_30_product_sale_return_warranty(self): + self.template_sale_return.write({ + 'usage': 'sale_order', + 'invoice_done': True, + 'sale_order_warranty': True, + 'in_to_refund': True, + 'so_decrement_order_qty': False, # invoice on decremented delivered not decremented order + 'next_rma_template_id': self.template_rtv.id, + }) + + validity = 100 # eligible for 100 days + warranty_validity = validity + 100 # eligible for 200 days + + self.product1.write({ + 'rma_sale_validity': validity, + 'rma_sale_warranty_validity': warranty_validity, + 'type': 'product', + 'invoice_policy': 'delivery', + 'tracking': 'serial', + 'standard_price': 1.5, + }) + + order = self.env['sale.order'].create({ + 'partner_id': self.partner1.id, + 'partner_invoice_id': self.partner1.id, + 'partner_shipping_id': self.partner1.id, + 'user_id': self.user1.id, + 'order_line': [(0, 0, { + 'product_id': self.product1.id, + 'product_uom_qty': 1.0, + 'product_uom': self.product1.uom_id.id, + 'price_unit': 10.0, + })] + }) + order.action_confirm() + self.assertTrue(order.state in ('sale', 'done')) + self.assertEqual(len(order.picking_ids), 1, 'Tests only run with single stage delivery.') + + # Try to RMA item not delivered yet + rma = self.env['rma.rma'].create({ + 'template_id': self.template_sale_return.id, + 'partner_id': self.partner1.id, + 'partner_shipping_id': self.partner1.id, + 'sale_order_id': order.id, + }) + self.assertEqual(rma.state, 'draft') + # Do not allow warranty return. + self.product1.rma_sale_warranty_validity = -1 + wizard = self.env['rma.sale.make.lines'].with_user(self.user1).create({'rma_id': rma.id}) + self.assertEqual(wizard.line_ids.qty_delivered, 0.0) + wizard.line_ids.product_uom_qty = 1.0 + + with self.assertRaises(UserError): + wizard.add_lines() + + # Allows returns, but not forever + self.product1.rma_sale_warranty_validity = warranty_validity + original_date_order = order.date_order + order.write({'date_order': original_date_order - timedelta(days=warranty_validity+1)}) + wizard = self.env['rma.sale.make.lines'].with_user(self.user1).create({'rma_id': rma.id}) + self.assertEqual(wizard.line_ids.qty_delivered, 0.0) + wizard.line_ids.product_uom_qty = 1.0 + with self.assertRaises(UserError): + wizard.add_lines() + + # Allows returns due to date, due to warranty option + order.write({'date_order': original_date_order - timedelta(days=validity+1)}) + wizard = self.env['rma.sale.make.lines'].with_user(self.user1).create({'rma_id': rma.id}) + self.assertEqual(wizard.line_ids.qty_delivered, 0.0) + wizard.line_ids.product_uom_qty = 1.0 + wizard.add_lines() + + # finish outbound so that we can invoice. + order.picking_ids.action_assign() + pack_opt = order.picking_ids.move_line_ids[0] + lot = self.env['stock.production.lot'].create({ + 'product_id': self.product1.id, + 'name': 'X100', + 'product_uom_id': self.product1.uom_id.id, + 'company_id': self.env.user.company_id.id, + }) + pack_opt.qty_done = 1.0 + pack_opt.lot_id = lot + order.picking_ids.button_validate() + self.assertEqual(order.picking_ids.state, 'done') + self.assertEqual(order.order_line.qty_delivered, 1.0) + + # Invoice the order so that only the core product is invoiced at the end... + self.assertFalse(order.invoice_ids) + wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=order.ids).create({}) + wiz.create_invoices() + order.flush() + self.assertTrue(order.invoice_ids) + order_invoice = order.invoice_ids + + self.assertEqual(rma.lines.product_id, self.product1) + rma.action_confirm() + self.assertTrue(rma.in_picking_id) + self.assertEqual(rma.in_picking_id.state, 'assigned') + pack_opt = rma.in_picking_id.move_line_ids[0] + pack_opt.lot_id = lot.id + pack_opt.qty_done = 1.0 + rma.in_picking_id.button_validate() + self.assertEqual(rma.in_picking_id.state, 'done') + order.flush() + # self.assertEqual(order.order_line.qty_delivered, 0.0) + rma.action_done() + self.assertEqual(rma.state, 'done') + order.flush() + + rma_invoice = rma.invoice_ids + self.assertTrue(rma_invoice) + sale_line = rma_invoice.invoice_line_ids.filtered(lambda l: l.sale_line_ids) + so_line = sale_line.sale_line_ids + self.assertTrue(sale_line) + self.assertEqual(sale_line.price_unit, so_line.price_unit) + + # Invoices do not have their anglo-saxon cost lines until they post + order_invoice.post() + rma_invoice.post() + + # Find the return to vendor RMA + rtv_rma = self.env['rma.rma'].search([('parent_id', '=', rma.id)]) + self.assertTrue(rtv_rma) + self.assertFalse(rtv_rma.out_picking_id) + + wiz = self.env['rma.make.rtv'].with_context(active_model='rma.rma', active_ids=rtv_rma.ids).create({}) + self.assertTrue(wiz.rma_line_ids) + wiz.partner_id = self.partner2 + wiz.create_batch() + self.assertTrue(rtv_rma.out_picking_id) + self.assertEqual(rtv_rma.out_picking_id.partner_id, self.partner2) diff --git a/rma_sale/views/portal_templates.xml b/rma_sale/views/portal_templates.xml index 6340352d..bb97b9b7 100644 --- a/rma_sale/views/portal_templates.xml +++ b/rma_sale/views/portal_templates.xml @@ -57,8 +57,8 @@
- Product image + Product Image + Product Image
diff --git a/rma_sale/views/product_views.xml b/rma_sale/views/product_views.xml index 4fa17000..41ea3e62 100644 --- a/rma_sale/views/product_views.xml +++ b/rma_sale/views/product_views.xml @@ -9,6 +9,7 @@ + @@ -22,6 +23,7 @@ + diff --git a/rma_sale/views/rma_views.xml b/rma_sale/views/rma_views.xml index f97ce8a8..9c39bf9b 100644 --- a/rma_sale/views/rma_views.xml +++ b/rma_sale/views/rma_views.xml @@ -9,6 +9,7 @@ + From 670d67ed684ab96a79533cfa6effca04f82215e7 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 30 Sep 2020 11:29:00 -0700 Subject: [PATCH 4/6] [REL] rma, rma_product_cores, rma_sale: require hibou_professional --- rma_sale/__manifest__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rma_sale/__manifest__.py b/rma_sale/__manifest__.py index e9fa3f5b..2f3f70ff 100644 --- a/rma_sale/__manifest__.py +++ b/rma_sale/__manifest__.py @@ -8,6 +8,7 @@ 'license': 'OPL-1', 'website': 'https://hibou.io/', 'depends': [ + 'hibou_professional', 'rma', 'sale', 'sales_team', From e251950f184a9ec47e29d8919d19e6ed437b1b40 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Sat, 20 Mar 2021 13:27:19 -0700 Subject: [PATCH 5/6] [IMP] rma: more functionality in portal, include ACL's for wizards --- rma/__manifest__.py | 2 +- rma/controllers/portal.py | 134 ++++++++++++++++++++----------- rma/security/ir.model.access.csv | 4 + rma/views/portal_templates.xml | 57 ++++++++----- 4 files changed, 129 insertions(+), 68 deletions(-) diff --git a/rma/__manifest__.py b/rma/__manifest__.py index bb567cce..7ef60374 100644 --- a/rma/__manifest__.py +++ b/rma/__manifest__.py @@ -2,7 +2,7 @@ { 'name': 'Hibou RMAs', - 'version': '14.0.1.0.0', + 'version': '14.0.1.0.1', 'category': 'Warehouse', 'author': 'Hibou Corp.', 'license': 'OPL-1', diff --git a/rma/controllers/portal.py b/rma/controllers/portal.py index f88c499c..81b501c6 100644 --- a/rma/controllers/portal.py +++ b/rma/controllers/portal.py @@ -1,14 +1,68 @@ # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. -from collections import OrderedDict +from operator import itemgetter from odoo import http, fields -from odoo.exceptions import AccessError, MissingError, ValidationError +from odoo.exceptions import AccessError, MissingError, UserError, ValidationError from odoo.http import request +from odoo.tools import groupby as groupbyelem from odoo.tools.translate import _ from odoo.addons.portal.controllers.portal import pager as portal_pager, CustomerPortal +def rma_portal_searchbar_sortings(): + # Override to add more sorting + return { + 'date': {'label': _('Newest'), 'order': 'create_date desc, id desc'}, + 'name': {'label': _('Name'), 'order': 'name asc, id asc'}, + } + + +def rma_portal_searchbar_filters(): + # Override to add more filters + return { + 'all': {'label': _('All'), 'domain': [('state', 'in', ['draft', 'confirmed', 'done', 'cancel'])]}, + 'draft': {'label': _('Draft'), 'domain': [('state', '=', 'draft')]}, + 'confirmed': {'label': _('Confirmed'), 'domain': [('state', '=', 'confirmed')]}, + 'cancel': {'label': _('Cancelled'), 'domain': [('state', '=', 'cancel')]}, + 'done': {'label': _('Done'), 'domain': [('state', '=', 'done')]}, + } + + +def rma_portal_searchbar_inputs(): + # Override to add more search fields + return { + 'name': {'input': 'name', 'label': _('Search in Name')}, + 'all': {'input': 'all', 'label': _('Search in All')}, + } + + +def rma_portal_searchbar_groupby(): + # Override to add more options for grouping + return { + 'none': {'input': 'none', 'label': _('None')}, + 'state': {'input': 'state', 'label': _('State')}, + 'template': {'input': 'template', 'label': _('Type')}, + } + + +def rma_portal_search_domain(search_in, search): + # Override if you added search inputs + search_domain = [] + if search_in in ('name', 'all'): + search_domain.append(('name', 'ilike', search)) + return search_domain + + +def rma_portal_group_rmas(rmas, groupby): + # Override to check groupby and perform a different grouping + if groupby == 'state': + return [request.env['rma.rma'].concat(*g) for k, g in groupbyelem(rmas, itemgetter('state'))] + if groupby == 'template': + return [request.env['rma.rma'].concat(*g) for k, g in groupbyelem(rmas, itemgetter('template_id'))] + return [rmas] + + class CustomerPortal(CustomerPortal): def _prepare_portal_layout_values(self): @@ -25,72 +79,58 @@ class CustomerPortal(CustomerPortal): return self._get_page_view_values(rma, access_token, values, 'my_rma_history', True, **kwargs) @http.route(['/my/rma', '/my/rma/page/'], type='http', auth="user", website=True) - def portal_my_rma(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, **kw): + def portal_my_rma(self, page=1, date_begin=None, date_end=None, sortby='date', filterby='all', groupby='none', search_in='all', search=None, **kw): values = self._prepare_portal_layout_values() - RMA = request.env['rma.rma'] - domain = [] - fields = ['name', 'create_date'] + searchbar_sortings = rma_portal_searchbar_sortings() + searchbar_filters = rma_portal_searchbar_filters() + searchbar_inputs = rma_portal_searchbar_inputs() + searchbar_groupby = rma_portal_searchbar_groupby() - archive_groups = self._get_archive_groups('rma.rma', domain, fields) + if sortby not in searchbar_sortings: + raise UserError(_("Unknown sorting option.")) + order = searchbar_sortings[sortby]['order'] + + if filterby not in searchbar_filters: + raise UserError(_("Unknown filter option.")) + domain = searchbar_filters[filterby]['domain'] if date_begin and date_end: domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)] - searchbar_sortings = { - 'date': {'label': _('Newest'), 'order': 'create_date desc, id desc'}, - 'name': {'label': _('Name'), 'order': 'name asc, id asc'}, - } - # default sort by value - if not sortby: - sortby = 'date' - order = searchbar_sortings[sortby]['order'] + if search_in and search: + domain += rma_portal_search_domain(search_in, search) - searchbar_filters = { - 'all': {'label': _('All'), 'domain': [('state', 'in', ['draft', 'confirmed', 'done', 'cancel'])]}, - 'draft': {'label': _('Draft'), 'domain': [('state', '=', 'draft')]}, - 'purchase': {'label': _('Confirmed'), 'domain': [('state', '=', 'confirmed')]}, - 'cancel': {'label': _('Cancelled'), 'domain': [('state', '=', 'cancel')]}, - 'done': {'label': _('Done'), 'domain': [('state', '=', 'done')]}, - } - # default filter by value - if not filterby: - filterby = 'all' - domain += searchbar_filters[filterby]['domain'] - - # count for pager - rma_count = RMA.search_count(domain) - # make pager + RMA = request.env['rma.rma'] + rma_count = len(RMA.search(domain)) pager = portal_pager( url="/my/rma", - url_args={'date_begin': date_begin, 'date_end': date_end}, + url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'search_in': search_in, 'search': search}, total=rma_count, page=page, step=self._items_per_page ) - # search the rmas to display, according to the pager data - rmas = RMA.search( - domain, - order=order, - limit=self._items_per_page, - offset=pager['offset'] - ) + rmas = RMA.search(domain, order=order, limit=self._items_per_page, offset=pager['offset']) request.session['my_rma_history'] = rmas.ids[:100] rma_templates = request.env['rma.template'].sudo().search([('portal_ok', '=', True)]) + grouped_rmas = rma_portal_group_rmas(rmas, groupby) values.update({ - 'request': request, - 'date': date_begin, - 'rma_list': rmas, 'rma_templates': rma_templates, + 'date': date_begin, + 'grouped_rmas': grouped_rmas, 'page_name': 'rma', - 'pager': pager, - 'archive_groups': archive_groups, - 'searchbar_sortings': searchbar_sortings, - 'sortby': sortby, - 'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())), - 'filterby': filterby, 'default_url': '/my/rma', + 'pager': pager, + 'searchbar_sortings': searchbar_sortings, + 'searchbar_filters': searchbar_filters, + 'searchbar_inputs': searchbar_inputs, + 'searchbar_groupby': searchbar_groupby, + 'sortby': sortby, + 'groupby': groupby, + 'search_in': search_in, + 'search': search, + 'filterby': filterby, }) return request.render("rma.portal_my_rma", values) diff --git a/rma/security/ir.model.access.csv b/rma/security/ir.model.access.csv index f02ceb7f..f0289a2c 100644 --- a/rma/security/ir.model.access.csv +++ b/rma/security/ir.model.access.csv @@ -8,3 +8,7 @@ access_rma_portal,rma.rma.portal,rma.model_rma_rma,base.group_portal,1,0,0,0 access_rma_line_portal,rma.line.portal,rma.model_rma_line,base.group_portal,1,0,0,0 access_rma_template_portal,rma.template.portal,rma.model_rma_template,base.group_portal,1,0,0,0 +"access_rma_picking_make_lines","access rma.picking.make.lines","rma.model_rma_picking_make_lines","stock.group_stock_user",1,1,1,1 +"access_rma_picking_make_lines_line","access rma.picking.make.lines.line","rma.model_rma_picking_make_lines_line","stock.group_stock_user",1,1,1,1 +"access_rma_make_rtv","access rma.make.rtv","rma.model_rma_make_rtv","stock.group_stock_user",1,1,1,1 +"access_rma_make_rtv_line","access rma.make.rtv.line","rma.model_rma_make_rtv_line","stock.group_stock_user",1,1,1,1 diff --git a/rma/views/portal_templates.xml b/rma/views/portal_templates.xml index 0692be42..a27a456a 100644 --- a/rma/views/portal_templates.xml +++ b/rma/views/portal_templates.xml @@ -32,34 +32,51 @@