diff --git a/rma_sale/__init__.py b/rma_sale/__init__.py new file mode 100644 index 00000000..9b429614 --- /dev/null +++ b/rma_sale/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/rma_sale/__manifest__.py b/rma_sale/__manifest__.py new file mode 100644 index 00000000..d2a4e3ec --- /dev/null +++ b/rma_sale/__manifest__.py @@ -0,0 +1,24 @@ +# © 2018 Hibou Corp. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Hibou RMAs for Sale Orders', + 'version': '11.0.1.0.0', + 'category': 'Sale', + 'author': "Hibou Corp.", + 'license': 'AGPL-3', + 'website': 'https://hibou.io/', + 'depends': [ + 'rma', + 'sale', + 'sales_team', + ], + 'data': [ + 'security/ir.model.access.csv', + 'data/rma_demo.xml', + 'views/rma_views.xml', + 'wizard/rma_lines_views.xml', + ], + 'installable': 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..f326f1a4 --- /dev/null +++ b/rma_sale/data/rma_demo.xml @@ -0,0 +1,15 @@ + + + + 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..0031f431 --- /dev/null +++ b/rma_sale/models/__init__.py @@ -0,0 +1 @@ +from . import rma diff --git a/rma_sale/models/rma.py b/rma_sale/models/rma.py new file mode 100644 index 00000000..6b69da38 --- /dev/null +++ b/rma_sale/models/rma.py @@ -0,0 +1,118 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class RMATemplate(models.Model): + _inherit = 'rma.template' + + usage = fields.Selection(selection_add=[('sale_order', 'Sale Order')]) + + +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['res.company']._company_default_get('sale.order')) + + @api.multi + @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 + + + @api.multi + 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') + @api.multi + 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') + @api.multi + 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 + + + @api.multi + 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 + values = self.template_id._values_for_in_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) + 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 + values = self.template_id._values_for_out_picking(self) + update = {'sale_id': sale_id, 'group_id': group_id} + update_lines = {'to_refund_so': self.template_id.in_to_refund_so, 'group_id': group_id} + return self._picking_from_values(values, update, update_lines) + + lines = self.lines.filtered(lambda l: l.product_uom_qty >= 1) + 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/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..c95f5fde --- /dev/null +++ b/rma_sale/tests/__init__.py @@ -0,0 +1 @@ +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..9cb31d0a --- /dev/null +++ b/rma_sale/tests/test_rma.py @@ -0,0 +1,113 @@ +from odoo.addons.rma.tests.test_rma import TestRMA +from odoo.exceptions import UserError, ValidationError + + +class TestRMASale(TestRMA): + + def setUp(self): + super(TestRMASale, self).setUp() + self.template_sale_return = self.env.ref('rma_sale.template_sale_return') + + def test_20_sale_return(self): + self.product1.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') + wizard = self.env['rma.sale.make.lines'].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.force_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, + }) + pack_opt.qty_done = 1.0 + pack_opt.lot_id = lot + order.picking_ids.do_transfer() + self.assertEqual(order.picking_ids.state, 'done') + 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.do_transfer() + rma.action_done() + + # 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.do_transfer() + + # Assign existing lot + rma2.in_picking_id.move_line_ids.write({ + 'lot_id': lot.id + }) + + # 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/rma_views.xml b/rma_sale/views/rma_views.xml new file mode 100644 index 00000000..d3570806 --- /dev/null +++ b/rma_sale/views/rma_views.xml @@ -0,0 +1,65 @@ + + + + + + rma.rma.form.sale + rma.rma + + + + + + + +
+