diff --git a/rma_sale/README.rst b/rma_sale/README.rst new file mode 100644 index 00000000..f7d43adc --- /dev/null +++ b/rma_sale/README.rst @@ -0,0 +1,47 @@ +.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg + :alt: License LGPL-3 + +RMA Sale +======== + +This module allows you to: + +# . Import sales order lines into RMA lines +# . Create a sales order and/or sales order line from one or more RMA lines + +Usage +===== + +Import existing sales order lines into an RMA +--------------------------------------------- +This feature is useful when you create an RMA associated to a product that +was shipped and you have as a reference the customer PO number. + +#. Access to a customer RMA +#. Press the button "Add from Sales Order" + + + +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 smashing it by providing a detailed and welcomed feedback. + + +Credits +======= + +Contributors +------------ + +* Jordi Ballester Alomar +* Aaron Henriquez + + +Maintainer +---------- + +This module is maintained by Eficent diff --git a/rma_sale/__init__.py b/rma_sale/__init__.py new file mode 100644 index 00000000..4105ff51 --- /dev/null +++ b/rma_sale/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# © 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) +from . import models +from . import wizards diff --git a/rma_sale/__openerp__.py b/rma_sale/__openerp__.py new file mode 100644 index 00000000..68635df2 --- /dev/null +++ b/rma_sale/__openerp__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# © 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +{ + 'name': 'RMA Sale', + 'version': '9.0.1.0.0', + 'license': 'LGPL-3', + 'category': 'RMA', + 'summary': 'Links RMA with Sales Orders', + 'author': "Eficent", + 'website': 'http://www.github.com/OCA/rma', + 'depends': ['rma_account', 'sale_stock'], + 'data': ['views/rma_order_view.xml', + 'views/rma_operation_view.xml', + 'views/sale_order_view.xml', + 'wizards/rma_order_line_make_sale_order_view.xml', + 'wizards/rma_add_sale.xml', + 'views/rma_order_line_view.xml'], + 'installable': True, + 'auto_install': True, +} diff --git a/rma_sale/models/__init__.py b/rma_sale/models/__init__.py new file mode 100644 index 00000000..0ec7edb9 --- /dev/null +++ b/rma_sale/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# © 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) +from . import sale_order_line +from . import rma_order_line +from . import rma_order +from . import rma_operation diff --git a/rma_sale/models/rma_operation.py b/rma_sale/models/rma_operation.py new file mode 100644 index 00000000..08ad757c --- /dev/null +++ b/rma_sale/models/rma_operation.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# © 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) +from openerp import _, api, fields, models + + +class RmaOperation(models.Model): + _inherit = 'rma.operation' + + sale_type = fields.Selection([ + ('no', 'Not required'), ('ordered', 'Based on Ordered Quantities'), + ('received', 'Based on Received Quantities')], + string="Sale Policy", default='no') diff --git a/rma_sale/models/rma_order.py b/rma_sale/models/rma_order.py new file mode 100644 index 00000000..a97124b6 --- /dev/null +++ b/rma_sale/models/rma_order.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# © 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) +from openerp import _, api, fields, models +from openerp.addons import decimal_precision as dp + + +class RmaOrder(models.Model): + _inherit = "rma.order" + + @api.one + def _compute_sales_count(self): + sales_list = [] + for rma_line in self.rma_line_ids: + if rma_line.sale_line_id and rma_line.sale_line_id.id: + sales_list.append(rma_line.sale_line_id.order_id.id) + self.sale_count = len(list(set(sales_list))) + + sale_count = fields.Integer(compute=_compute_sales_count, + string='# of Sales', copy=False, default=0) + + @api.model + def _get_line_domain(self, rma_id, line): + if line.sale_line_id and line.sale_line_id.id: + domain = [('rma_id', '=', rma_id.id), + ('type', '=', 'supplier'), + ('sale_line_id', '=', line.sale_line_id.id)] + else: + domain = super(RmaOrder, self)._get_line_domain(rma_id, line) + return domain + + @api.multi + def action_view_sale_order(self): + action = self.env.ref('sale.action_quotations') + result = action.read()[0] + order_ids = [] + for rma_line in self.rma_line_ids: + if rma_line.sale_line_id and rma_line.sale_line_id.id: + order_ids.append(rma_line.sale_line_id.order_id.id) + result['domain'] = [('id', 'in', order_ids)] + return result \ No newline at end of file diff --git a/rma_sale/models/rma_order_line.py b/rma_sale/models/rma_order_line.py new file mode 100644 index 00000000..e6be59ec --- /dev/null +++ b/rma_sale/models/rma_order_line.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# © 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) +from openerp import _, api, fields, models +from openerp.addons import decimal_precision as dp + +class RmaOrderLine(models.Model): + _inherit = "rma.order.line" + + @api.model + def _default_sale_type(self): + return self.sale_type or False + + sale_type = fields.Selection([ + ('no', 'Not required'), ('ordered', 'Based on Ordered Quantities'), + ('received', 'Based on Received Quantities')], + string="Sale Policy", default=_default_sale_type) + + @api.one + @api.depends('sale_line_ids', 'sale_type', 'sales_count', + 'sale_line_ids.state') + def _compute_qty_to_sell(self): + if self.sale_type == 'no': + self.qty_to_sell = 0.0 + elif self.sale_type == 'ordered': + qty = self._get_rma_sold_qty() + self.qty_to_sell = self.product_qty - qty + elif self.sale_type == 'received': + qty = self._get_rma_sold_qty() + self.qty_to_sell = self.qty_received - qty + else: + self.qty_to_sell = 0.0 + + @api.one + @api.depends('sale_line_ids', 'sale_type', 'sales_count', + 'sale_line_ids.state') + def _compute_qty_sold(self): + self.qty_sold = self._get_rma_sold_qty() + + @api.one + def _compute_sales_count(self): + sales_list = [] + for sale_order_line in self.sale_line_ids: + sales_list.append(sale_order_line.order_id.id) + self.sales_count = len(list(set(sales_list))) + + sale_line_id = fields.Many2one(comodel_name='sale.order.line', + string='Originating Sales Order Line', + ondelete='restrict') + sale_line_ids = fields.One2many(comodel_name='sale.order.line', + inverse_name='rma_line_id', + string='Sales Order Lines', readonly=True, + states={'draft': [('readonly', False)]}, + copy=False) + qty_to_sell = fields.Float( + string='Qty To Sell', copy=False, + digits=dp.get_precision('Product Unit of Measure'), + readonly=True, compute=_compute_qty_to_sell, + store=True) + + qty_sold = fields.Float( + string='Qty Sold', copy=False, + digits=dp.get_precision('Product Unit of Measure'), + readonly=True, compute=_compute_qty_sold, + store=True) + + sale_type = fields.Selection([ + ('no', 'Not required'), ('ordered', 'Based on Ordered Quantities'), + ('received', 'Based on Received Quantities')], + string="Sale Policy", default='no', required=True) + + sales_count = fields.Integer(compute=_compute_sales_count, + string='# of Sales', copy=False, default=0) + + @api.multi + def action_view_sale_order(self): + action = self.env.ref('sale.action_quotations') + result = action.read()[0] + order_ids = [] + for sale_line in self.sale_line_ids: + order_ids.append(sale_line.order_id.id) + result['domain'] = [('id', 'in', order_ids)] + return result + + @api.multi + def _get_rma_sold_qty(self): + self.ensure_one() + qty = 0.0 + for sale_line in self.sale_line_ids.filtered( + lambda p: p.state not in ('draft', 'sent', 'cancel')): + qty += sale_line.product_uom_qty + return qty diff --git a/rma_sale/models/sale_order_line.py b/rma_sale/models/sale_order_line.py new file mode 100644 index 00000000..b54f8d81 --- /dev/null +++ b/rma_sale/models/sale_order_line.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# © 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from openerp import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + rma_line_id = fields.Many2one('rma.order.line', string='RMA', + ondelete='restrict') + + @api.multi + def _prepare_order_line_procurement(self, group_id=False): + vals = super(SaleOrderLine, self)._prepare_order_line_procurement( + group_id=group_id) + vals.update({ + 'rma_line_id': self.rma_line_id.id + }) + return vals diff --git a/rma_sale/views/rma_operation_view.xml b/rma_sale/views/rma_operation_view.xml new file mode 100644 index 00000000..cd97ee1d --- /dev/null +++ b/rma_sale/views/rma_operation_view.xml @@ -0,0 +1,28 @@ + + + + + + rma.operation.tree + rma.operation + + + + + + + + + + rma.operation.form + rma.operation + + + + + + + + + + diff --git a/rma_sale/views/rma_order_line_view.xml b/rma_sale/views/rma_order_line_view.xml new file mode 100644 index 00000000..9930bfd8 --- /dev/null +++ b/rma_sale/views/rma_order_line_view.xml @@ -0,0 +1,93 @@ + + + + + + rma.order.line.form + rma.order.line + + +
+ +
+ + + + + + + + + + + + + + + +
+ + + rma.order.line.supplier.form + rma.order.line + + +
+ +
+ + + + + + + + + + + + + + + +
+ + + rma.order.line.form + rma.order.line + + +
+
+
+
+ + rma.order.line.supplier.form + rma.order.line + + +
+
+
+
+ +
+
diff --git a/rma_sale/views/rma_order_view.xml b/rma_sale/views/rma_order_view.xml new file mode 100644 index 00000000..48ff6f61 --- /dev/null +++ b/rma_sale/views/rma_order_view.xml @@ -0,0 +1,21 @@ + + + + + rma.order.form + rma.order + + +
+ +
+
+
+
+
diff --git a/rma_sale/views/sale_order_view.xml b/rma_sale/views/sale_order_view.xml new file mode 100644 index 00000000..2bfe7ca4 --- /dev/null +++ b/rma_sale/views/sale_order_view.xml @@ -0,0 +1,21 @@ + + + + + sale.order.form + sale.order + + + + + + + + + + + diff --git a/rma_sale/wizards/__init__.py b/rma_sale/wizards/__init__.py new file mode 100644 index 00000000..40e8ecb8 --- /dev/null +++ b/rma_sale/wizards/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# © 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_order_line_make_sale_order +from . import rma_make_picking +from . import rma_refund +from . import rma_add_sale diff --git a/rma_sale/wizards/rma_add_sale.py b/rma_sale/wizards/rma_add_sale.py new file mode 100644 index 00000000..1ed460fd --- /dev/null +++ b/rma_sale/wizards/rma_add_sale.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# © 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +import time +from openerp import models, fields, exceptions, api, _ +from openerp.exceptions import ValidationError +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DT_FORMAT +import openerp.addons.decimal_precision as dp + + +class RmaAddSale(models.TransientModel): + _name = 'rma_add_sale' + _description = 'Wizard to add rma lines' + + @api.model + def default_get(self, fields): + res = super(RmaAddSale, self).default_get(fields) + rma_obj = self.env['rma.order'] + rma_id = self.env.context['active_ids'] or [] + active_model = self.env.context['active_model'] + if not rma_id: + return res + assert active_model == 'rma.order', 'Bad context propagation' + + rma = rma_obj.browse(rma_id) + res['rma_id'] = rma.id + res['partner_id'] = rma.partner_id.id + res['sale_id'] = False + res['sale_line_ids'] = False + return res + + rma_id = fields.Many2one('rma.order', + string='RMA Order', + readonly=True, + ondelete='cascade') + + partner_id = fields.Many2one(comodel_name='res.partner', string='Partner', + readonly=True) + sale_id = fields.Many2one(comodel_name='sale.order', string='Order') + sale_line_ids = fields.Many2many('sale.order.line', + 'rma_add_sale_add_line_rel', + 'sale_line_id', 'rma_add_sale_id', + readonly=False, + string='Sale Lines') + + def _prepare_rma_line_from_sale_order_line(self, line): + operation = line.product_id.rma_operation_id and \ + line.product_id.rma_operation_id.id or False + if not operation: + operation = line.product_id.categ_id.rma_operation_id and \ + line.product_id.categ_id.rma_operation_id.id or False + data = { + 'sale_line_id': line.id, + 'product_id': line.product_id.id, + 'origin': line.order_id.name, + 'uom_id': line.product_uom.id, + 'operation_id': operation, + 'product_qty': line.product_uom_qty, + 'delivery_address_id': self.sale_id.partner_id.id, + 'invoice_address_id': self.sale_id.partner_id.id, + 'price_unit': line.currency_id.compute( + line.price_unit, line.currency_id, round=False), + 'rma_id': self.rma_id.id + } + if not operation: + operation = self.env['rma.operation'].search( + [('type', '=', self.rma_id.type)], limit=1) + if not operation: + raise ValidationError("Please define an operation first") + if not operation.in_route_id or not operation.out_route_id: + route = self.env['stock.location.route'].search( + [('rma_selectable', '=', True)], limit=1) + if not route: + raise ValidationError("Please define an rma route") + data.update( + {'in_route_id': operation.in_route_id.id or route, + 'out_route_id': operation.out_route_id.id or route, + 'receipt_policy': operation.receipt_policy, + 'location_id': operation.location_id.id or + self.env.ref('stock.stock_location_stock').id, + 'operation_id': operation.id, + 'refund_policy': operation.refund_policy, + 'delivery_policy': operation.delivery_policy + }) + return data + + @api.model + def _get_rma_data(self): + data = { + 'date_rma': fields.Datetime.now(), + 'delivery_address_id': self.sale_id.partner_id.id, + 'invoice_address_id': self.sale_id.partner_id.id + } + return data + + @api.model + def _get_existing_sale_lines(self): + existing_sale_lines = [] + for rma_line in self.rma_id.rma_line_ids: + existing_sale_lines.append(rma_line.sale_line_id) + return existing_sale_lines + + @api.multi + def add_lines(self): + rma_line_obj = self.env['rma.order.line'] + existing_sale_lines = self._get_existing_sale_lines() + for line in self.sale_line_ids: + # Load a PO line only once + if line not in existing_sale_lines: + data = self._prepare_rma_line_from_sale_order_line(line) + rma_line_obj.create(data) + rma = self.rma_id + data_rma = self._get_rma_data() + rma.write(data_rma) + return {'type': 'ir.actions.act_window_close'} diff --git a/rma_sale/wizards/rma_add_sale.xml b/rma_sale/wizards/rma_add_sale.xml new file mode 100644 index 00000000..280dcbbc --- /dev/null +++ b/rma_sale/wizards/rma_add_sale.xml @@ -0,0 +1,80 @@ + + + + + rma.add.sale + rma_add_sale + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + + Add Sale Order + ir.actions.act_window + rma_add_sale + rma.order + form + form + new + + + + + + + rma.order.line.form + rma.order + + + + + + +
diff --git a/rma_sale/wizards/rma_make_picking.py b/rma_sale/wizards/rma_make_picking.py new file mode 100644 index 00000000..7291d4be --- /dev/null +++ b/rma_sale/wizards/rma_make_picking.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# © 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) +from openerp import models, fields, exceptions, api, _ + + +class RmaMakePicking(models.TransientModel): + _inherit = 'rma_make_picking.wizard' + + @api.returns('rma.order.line') + def _prepare_item(self, line): + res = super(RmaMakePicking, self)._prepare_item(line) + res['sale_line_id'] = line.sale_line_id.id + return res + +class RmaMakePickingItem(models.TransientModel): + _inherit = "rma_make_picking.wizard.item" + + sale_line_id = fields.Many2one('sale.order.line', + string='Sale Line') diff --git a/rma_sale/wizards/rma_order_line_make_sale_order.py b/rma_sale/wizards/rma_order_line_make_sale_order.py new file mode 100644 index 00000000..68057f59 --- /dev/null +++ b/rma_sale/wizards/rma_order_line_make_sale_order.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl-3.0). + +import openerp.addons.decimal_precision as dp +from openerp import _, api, exceptions, fields, models + + +class RmaLineMakeSaleOrder(models.TransientModel): + _name = "rma.order.line.make.sale.order" + _description = "RMA Line Make Sales Order" + + partner_id = fields.Many2one('res.partner', string='Customer', + required=False, + domain=[('supplier', '=', True)]) + item_ids = fields.One2many( + 'rma.order.line.make.sale.order.item', + 'wiz_id', string='Items') + sale_order_id = fields.Many2one('sale.order', + string='Sales Order', + required=False, + domain=[('state', '=', 'draft')]) + + @api.model + def _prepare_item(self, line): + return { + 'line_id': line.id, + 'rma_line_id': line.id, + 'product_id': line.product_id.id, + 'name': line.product_id.name, + 'product_qty': line.qty_to_sell, + 'rma_id': line.rma_id.id, + 'out_warehouse_id': line.out_warehouse_id.id, + 'out_route_id': line.out_route_id.id, + 'product_uom_id': line.uom_id.id, + } + + @api.model + def default_get(self, fields): + res = super(RmaLineMakeSaleOrder, 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) + for line in lines: + items.append([0, 0, self._prepare_item(line)]) + customers = lines.mapped('partner_id') + if len(customers) == 1: + res['partner_id'] = customers.id + else: + raise exceptions.Warning( + _('Only RMA lines from the same partner can be processed at ' + 'the same time')) + res['item_ids'] = items + return res + + @api.model + def _prepare_sale_order(self, out_warehouse, company): + if not self.partner_id: + raise exceptions.Warning( + _('Enter a customer.')) + customer = self.partner_id + data = { + 'origin': '', + 'partner_id': customer.id, + 'warehouse_id': out_warehouse.id, + 'company_id': company.id, + } + return data + + @api.model + def _prepare_sale_order_line(self, so, item): + product = item.product_id + vals = { + 'name': product.name, + 'order_id': so.id, + 'product_id': product.id, + 'product_uom': product.uom_po_id.id, + 'route_id': item.out_route_id.id, + 'product_uom_qty': item.product_qty, + 'rma_line_id': item.line_id.id + } + if item.free_of_charge: + vals['price_unit'] = 0.0 + return vals + + @api.multi + def make_sale_order(self): + res = [] + sale_obj = self.env['sale.order'] + so_line_obj = self.env['sale.order.line'] + sale = False + + for item in self.item_ids: + line = item.line_id + if item.product_qty <= 0.0: + raise exceptions.Warning( + _('Enter a positive quantity.')) + + if self.sale_order_id: + sale = self.sale_order_id + if not sale: + po_data = self._prepare_sale_order(line.out_warehouse_id, + line.company_id) + sale = sale_obj.create(po_data) + + so_line_data = self._prepare_sale_order_line(sale, item) + so_line_obj.create(so_line_data) + res.append(sale.id) + + return { + 'domain': "[('id','in', ["+','.join(map(str, res))+"])]", + 'name': _('Quotations'), + 'view_type': 'form', + 'view_mode': 'tree,form', + 'res_model': 'sale.order', + 'view_id': False, + 'context': False, + 'type': 'ir.actions.act_window' + } + + +class RmaLineMakeSaleOrderItem(models.TransientModel): + _name = "rma.order.line.make.sale.order.item" + _description = "RMA Line Make Sale Order Item" + + wiz_id = fields.Many2one( + 'rma.order.line.make.sale.order', + string='Wizard', required=True, ondelete='cascade', + readonly=True) + line_id = fields.Many2one('rma.order.line', + string='RMA Line', + required=True) + rma_id = fields.Many2one('rma.order', related='line_id.rma_id', + string='RMA Order', readonly=True) + product_id = fields.Many2one('product.product', string='Product', + readonly=True) + name = fields.Char(string='Description', required=True, readonly=True) + product_qty = fields.Float(string='Quantity to sell', + digits=dp.get_precision('Product UoS')) + product_uom_id = fields.Many2one('product.uom', string='UoM', + readonly=True) + out_warehouse_id = fields.Many2one('stock.warehouse', + string='Outbound Warehouse') + free_of_charge = fields.Boolean('Free of Charge') + out_route_id = fields.Many2one( + 'stock.location.route', string='Outbound Route', + domain=[('rma_selectable', '=', True)]) diff --git a/rma_sale/wizards/rma_order_line_make_sale_order_view.xml b/rma_sale/wizards/rma_order_line_make_sale_order_view.xml new file mode 100644 index 00000000..fea59e77 --- /dev/null +++ b/rma_sale/wizards/rma_order_line_make_sale_order_view.xml @@ -0,0 +1,75 @@ + + + + + + RMA Line Make Sale Order + rma.order.line.make.sale.order + form + +
+ + + + /> + + + + + + + + + + + + + + + + + + + + + + +