From 4c941b36615ee27f01c7cf055a0686cd5bacde64 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 3 Jul 2020 09:04:44 -0700 Subject: [PATCH] [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