diff --git a/rma_internal_transfer/README.rst b/rma_internal_transfer/README.rst new file mode 100644 index 00000000..1fcc070d --- /dev/null +++ b/rma_internal_transfer/README.rst @@ -0,0 +1,44 @@ +.. image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: https://www.gnu.org/licenses/lgpl + :alt: License: LGPL-3 + +===================== +RMA Internal Transfer +===================== + +Sometimes the repair location is not the same as the receiving location +and useres wants to perform this action manual instead of putaway +strategies. In this case an internal tranfer has to be done. + +This module just adds a button that eases this task + + +Usage +===== + +In the RMA from view there is a new button "Internal Transfer". The user +just need to change the default location according its needs. + + +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 smash it by providing detailed and welcomed feedback. + + +Credits +======= + +Contributors +------------ + +* Aaron Henriquez + + +Maintainer +---------- + +This module is maintained by Eficent. diff --git a/rma_internal_transfer/__init__.py b/rma_internal_transfer/__init__.py new file mode 100644 index 00000000..97bf97f6 --- /dev/null +++ b/rma_internal_transfer/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from . import wizards diff --git a/rma_internal_transfer/__manifest__.py b/rma_internal_transfer/__manifest__.py new file mode 100644 index 00000000..c413b7bd --- /dev/null +++ b/rma_internal_transfer/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +{ + 'name': 'RMA Internal Transfer', + 'version': '12.0.2.3.0', + 'license': 'LGPL-3', + 'category': 'RMA', + 'summary': 'Internal transfers. Made easy.', + 'author': "Eficent, Odoo Community Association (OCA)", + 'website': 'https://github.com/Eficent/stock-rma', + 'depends': ['rma'], + 'data': ['wizards/rma_internal_transfer.xml', + ], + 'installable': True, +} diff --git a/rma_internal_transfer/tests/__init__.py b/rma_internal_transfer/tests/__init__.py new file mode 100644 index 00000000..ccc2c961 --- /dev/null +++ b/rma_internal_transfer/tests/__init__.py @@ -0,0 +1 @@ +from . import test_internal_transfer diff --git a/rma_internal_transfer/tests/test_internal_transfer.py b/rma_internal_transfer/tests/test_internal_transfer.py new file mode 100644 index 00000000..dbf11968 --- /dev/null +++ b/rma_internal_transfer/tests/test_internal_transfer.py @@ -0,0 +1,132 @@ +# © 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo.tests import common + + +class TestRmaInternalTransfer(common.SavepointCase): + """ Test the routes and the quantities """ + + @classmethod + def setUpClass(cls): + super(TestRmaInternalTransfer, cls).setUpClass() + + cls.rma_make_picking = cls.env['rma_internal_transfer.wizard'] + cls.stockpicking = cls.env['stock.picking'] + cls.rma_add_stock_move = cls.env['rma_add_stock_move'] + cls.rma = cls.env['rma.order'] + cls.rma_line = cls.env['rma.order.line'] + cls.product_1 = cls.env.ref('product.product_product_25') + cls.partner_id = cls.env.ref('base.res_partner_2') + cls.stock_location = cls.env.ref('stock.stock_location_stock') + wh = cls.env.ref('stock.warehouse0') + cls.product_uom_id = cls.env.ref('uom.product_uom_unit') + cls.stock_rma_location = wh.lot_rma_id + cls.customer_location = cls.env.ref( + 'stock.stock_location_customers') + cls.supplier_location = cls.env.ref( + 'stock.stock_location_suppliers') + # Customer RMA: + products2move = [(cls.product_1, 3)] + cls.rma_customer_id = cls._create_rma_from_move( + products2move, 'customer', cls.env.ref('base.res_partner_2'), + dropship=False) + cls.rma_customer_id.rma_line_ids._onchange_operation_id() + seq = cls.env['ir.sequence'].search([], limit=1) + cls.env['stock.picking.type'].create({ + "name": "TEST INTERNAL", + "sequence_id": seq.id, + "default_location_src_id": cls.stock_rma_location.id, + "default_location_dest_id": cls.stock_rma_location.id, + "warehouse_id": wh.id, + "code": "internal"}) + + @classmethod + def _create_picking(cls, partner): + return cls.stockpicking.create({ + 'partner_id': partner.id, + 'picking_type_id': cls.env.ref('stock.picking_type_in').id, + 'location_id': cls.stock_location.id, + 'location_dest_id': cls.supplier_location.id + }) + + @classmethod + def _prepare_move(cls, product, qty, src, dest, picking_in): + res = { + 'partner_id': cls.partner_id.id, + 'product_id': product.id, + 'name': product.partner_ref, + 'state': 'confirmed', + 'product_uom': cls.product_uom_id.id or product.uom_id.id, + 'product_uom_qty': qty, + 'origin': 'Test RMA', + 'location_id': src.id, + 'location_dest_id': dest.id, + 'picking_id': picking_in.id + } + return res + + @classmethod + def _create_rma_from_move(cls, products2move, type, partner, dropship, + supplier_address_id=None): + picking_in = cls._create_picking(partner) + + moves = [] + for item in products2move: + move_values = cls._prepare_move( + item[0], item[1], cls.stock_location, + cls.customer_location, picking_in) + moves.append(cls.env['stock.move'].create(move_values)) + + # Create the RMA from the stock_move + rma_id = cls.rma.create( + { + 'reference': '0001', + 'type': type, + 'partner_id': partner.id, + 'company_id': cls.env.ref('base.main_company').id + }) + for move in moves: + if type == 'customer': + wizard = cls.rma_add_stock_move.new( + {'stock_move_id': move.id, 'customer': True, + 'active_ids': rma_id.id, + 'rma_id': rma_id.id, + 'partner_id': move.partner_id.id, + 'active_model': 'rma.order', + } + ) + wizard.with_context({ + 'stock_move_id': move.id, 'customer': True, + 'active_ids': rma_id.id, + 'partner_id': move.partner_id.id, + 'active_model': 'rma.order', + }).default_get([str(move.id), + str(cls.partner_id.id)]) + data = wizard.with_context(customer=1).\ + _prepare_rma_line_from_stock_move(move) + wizard.add_lines() + + for operation in move.product_id.rma_customer_operation_id: + operation.in_route_id = False + move.product_id.categ_id.rma_customer_operation_id = False + move.product_id.rma_customer_operation_id = False + wizard._prepare_rma_line_from_stock_move(move) + + cls.line = cls.rma_line.create(data) + cls.line.action_rma_to_approve() + cls.line.action_rma_approve() + return rma_id + + def test_0_internal_transfer(cls): + wizard = cls.rma_make_picking.with_context({ + 'active_ids': cls.rma_customer_id.rma_line_ids.ids, + 'active_model': 'rma.order.line', + 'picking_type': 'internal', + 'active_id': cls.rma_customer_id.id + }).create({}) + + wizard.action_create_picking() + res = cls.rma_customer_id.rma_line_ids.action_view_in_shipments() + cls.assertTrue('res_id' in res, + "Incorrect number of pickings created") diff --git a/rma_internal_transfer/wizards/__init__.py b/rma_internal_transfer/wizards/__init__.py new file mode 100644 index 00000000..2c540743 --- /dev/null +++ b/rma_internal_transfer/wizards/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from . import rma_internal_transfer diff --git a/rma_internal_transfer/wizards/rma_internal_transfer.py b/rma_internal_transfer/wizards/rma_internal_transfer.py new file mode 100644 index 00000000..5a4ae09d --- /dev/null +++ b/rma_internal_transfer/wizards/rma_internal_transfer.py @@ -0,0 +1,147 @@ +# Copyright (C) 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError, ValidationError +import odoo.addons.decimal_precision as dp + + +class RmaMakePicking(models.TransientModel): + _name = 'rma_internal_transfer.wizard' + _description = 'Wizard to create pickings from rma lines' + + @api.returns('rma.order.line') + def _prepare_item(self, line): + values = {'product_id': line.product_id.id, + 'product_qty': line.product_qty, + 'uom_id': line.uom_id.id, + 'qty_to_receive': line.qty_to_receive, + 'qty_to_deliver': line.qty_to_deliver, + 'line_id': line.id, + 'rma_id': line.rma_id and line.rma_id.id or False, + 'wiz_id': self.env.context['active_id'], + } + return values + + @api.model + def default_get(self, fields): + """Default values for wizard, if there is more than one supplier on + lines the supplier field is empty otherwise is the unique line + supplier. + """ + context = self._context.copy() + res = super(RmaMakePicking, self).default_get(fields) + rma_line_obj = self.env['rma.order.line'] + rma_line_ids = self.env.context['active_ids'] or [] + active_model = self.env.context['active_model'] + + if not rma_line_ids: + return res + assert active_model == 'rma.order.line', \ + 'Bad context propagation' + + items = [] + lines = rma_line_obj.browse(rma_line_ids) + if len(lines.mapped('partner_id')) > 1: + raise ValidationError( + _('Only RMA lines from the same partner can be processed at ' + 'the same time')) + for line in lines: + items.append([0, 0, self._prepare_item(line)]) + + types = self.env['stock.picking.type'].search( + [('code', '=', 'internal'), + ('warehouse_id.company_id', '=', + line.in_warehouse_id.id)])[:1] + if not types: + raise ValidationError(_('Please define an internal picking type ' + 'for this warehouse')) + res['location_id'] = lines.mapped('location_id').ids[0] + res['picking_type_id'] = types.id + res['location_dest_id'] = lines.mapped('location_id').ids[0] + res['item_ids'] = items + context.update({'items_ids': items}) + return res + + item_ids = fields.One2many( + 'rma_internal_transfer.wizard.item', + 'wiz_id', string='Items') + picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type', + required=True) + location_id = fields.Many2one('stock.location', 'Source Location', + required=True) + location_dest_id = fields.Many2one( + 'stock.location', 'Destination Location', required=True) + + def find_procurement_group(self): + if self.mapped('item_ids.line_id.rma_id'): + return self.env['procurement.group'].search([ + ('rma_id', 'in', self.mapped('item_ids.line_id.rma_id').ids)]) + else: + return self.env['procurement.group'].search([ + ('rma_line_id', 'in', self.mapped('item_ids.line_id').ids)]) + + @api.multi + def action_create_picking(self): + self.ensure_one() + picking_obj = self.env['stock.picking'] + type_obj = self.env['stock.picking.type'] + user_obj = self.env['res.users'] + company_id = user_obj.browse(self._uid).company_id.id + types = type_obj.search([('code', '=', 'internal'), + ('warehouse_id.company_id', '=', company_id)]) + if not types: + raise UserError(_("Make sure you have at least an internal picking" + "type defined for your company's warehouse.")) + picking_type = types[0] + group_id = self.find_procurement_group().id + picking = picking_obj.create({ + 'picking_type_id': picking_type.id, + 'location_dest_id': self.location_dest_id.id, + 'location_id': self.location_id.id, + 'move_lines': [(0, 0, { + 'name': item.line_id.name, + 'product_id': item.product_id.id, + 'product_uom_qty': item.product_qty, + 'product_uom': item.uom_id.id, + 'state': 'draft', + 'group_id': group_id, + 'rma_line_id': item.line_id.id, + }) for item in self.item_ids] + }) + return { + 'type': 'ir.actions.act_window', + 'name': 'Stock Picking', + 'res_model': 'stock.picking', + 'res_id': picking.id, + 'view_mode': 'form', + 'view_type': 'form', + } + + @api.multi + def action_cancel(self): + return {'type': 'ir.actions.act_window_close'} + + +class RmaMakePickingItem(models.TransientModel): + _name = "rma_internal_transfer.wizard.item" + _description = "Items to receive" + + wiz_id = fields.Many2one( + 'rma_internal_transfer.wizard', + string='Wizard', required=True) + line_id = fields.Many2one('rma.order.line', + string='RMA order Line', + readonly=True, + ondelete='cascade') + rma_id = fields.Many2one('rma.order', + related='line_id.rma_id', + string='RMA Group', + readonly=True) + product_id = fields.Many2one('product.product', string='Product', + required=True) + product_qty = fields.Float( + related='line_id.product_qty', + string='Quantity Ordered', copy=False, + digits=dp.get_precision('Product Unit of Measure')) + uom_id = fields.Many2one('uom.uom', string='Unit of Measure') diff --git a/rma_internal_transfer/wizards/rma_internal_transfer.xml b/rma_internal_transfer/wizards/rma_internal_transfer.xml new file mode 100755 index 00000000..a9cceffc --- /dev/null +++ b/rma_internal_transfer/wizards/rma_internal_transfer.xml @@ -0,0 +1,78 @@ + + + + + + Internal Transfer + rma_internal_transfer.wizard + +
+ + + + + + + + + + + + + + + + +
+
+ +
+
+ + + Create Internal Transfer + ir.actions.act_window + rma_internal_transfer.wizard + form + form + new + + + {'picking_type': 'internal'} + + + + + rma.order.line.form + rma.order.line + + + + + + + + rma.order.line.supplier.form + rma.order.line + + + + + + +