diff --git a/rma/README.rst b/rma/README.rst new file mode 100644 index 00000000..68a32563 --- /dev/null +++ b/rma/README.rst @@ -0,0 +1,96 @@ +.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg + :alt: License LGPL-3 + +RMA +=== + +A return merchandise authorization (RMA), is a part of the process of +returning a product in order to receive a refund, replacement, or repair +during the product's warranty period. + +The purchaser of the product must contact the manufacturer (or distributor +or retailer) to obtain authorization to return the product. + +The resulting RMA number must be displayed on or included in the returned +product's packaging. + +The issuance of an RMA is a key gatekeeping moment in the reverse logistics +cycle, providing the vendor with a final opportunity to diagnose and correct +the customer's problem with the product (such as improper installation or +configuration) before the customer permanently relinquishes ownership +of the product to the manufacturer, commonly referred to as a return. + +As returns are costly for the vendor and inconvenient for the customer, +any return that can be prevented benefits both parties. + + +Configuration +============= + +Security +-------- +Go to Settings > Users and assign the appropiate permissions to users. +Different security groups grant distinct levels of access to the RMA features. + +* Users in group "RMA Customer User" or "RMA Supplier User" can access to, + create and process RMA's associated to customers or suppliers respectively. + +* Users in group "RMA Manager" can access to, create, approve and process RMA's + associated to both customers and suppliers. + + + + +#. Go to Settings > Users and assign the appropiate permissions to users. + Users assigned to the group 'RMA +#. Go to Inventory > Settings > Return Merchandising Authorization and select + the option "Display 3 fields on rma: partner, invoice address, delivery + address" if needed. +#. Go to Inventory > Settings > Configuration > Warehouse management > + Warehouses and add a default RMA location and RMA picking type for customers + and suppliers RMA picking type. In case the warehouse is configured to + use routes, you need to create at least one route per rma type with at + least two push rules (one for inbound another for outbound) it's very + important to select the typeof operation supplier if we are moving in the + company and customer if we are moving out of the company. + +Usage +===== + +RMA are accessible though Inventory menu. There's four menus, divided by type +. Users can access to the list of RMA or RMA lines. + +Create an RMA: +#. Select a partner. Enter RMA lines associated to an existing picking, or + manually. +#. Request approval and approve. +#. Click on RMA Lines button. +#. Click on more and select an option: "Receive products", "Create Delivery + Order". +#. Go back to the RMA. Set the RMA to done if not further action is required. + + +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/__init__.py b/rma/__init__.py new file mode 100644 index 00000000..4105ff51 --- /dev/null +++ b/rma/__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/__openerp__.py b/rma/__openerp__.py new file mode 100644 index 00000000..9fc1d7f8 --- /dev/null +++ b/rma/__openerp__.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) + +{ + 'name': 'rma', + 'version': '9.0.1.0.0', + 'license': 'LGPL-3', + 'category': 'RMA', + 'summary': 'Introduces the return merchandise authorization (RMA) process ' + 'in odoo', + 'author': "Eficent", + 'website': 'http://www.github.com/OCA/rma', + 'depends': ['account', 'stock', 'mail', + 'procurement'], + 'demo': [ + 'demo/stock_demo.xml', + ], + 'data': ['security/rma.xml', + 'security/ir.model.access.csv', + + 'data/rma_sequence.xml', + 'data/stock_data.xml', + 'data/rma_operation.xml', + 'views/rma_order_view.xml', + 'views/rma_operation_view.xml', + 'views/rma_rule_view.xml', + 'views/rma_order_line_view.xml', + 'views/stock_view.xml', + 'views/stock_warehouse.xml', + 'views/product_view.xml', + 'views/procurement_view.xml', + 'views/res_company_view.xml', + 'wizards/rma_make_picking_view.xml', + 'wizards/rma_add_stock_move_view.xml', + 'wizards/stock_config_settings.xml', + 'wizards/rma_order_line_make_supplier_rma_view.xml', + ], + 'installable': True, + 'auto_install': False, +} diff --git a/rma/data/rma_operation.xml b/rma/data/rma_operation.xml new file mode 100644 index 00000000..ad39aff4 --- /dev/null +++ b/rma/data/rma_operation.xml @@ -0,0 +1,81 @@ + + + + Replace After Receive + RPLC + no + ordered + received + customer + + + + + + Replace + RPLS + no + ordered + ordered + supplier + + + + + + Refund before receive + RFC + ordered + no + no + customer + + + + + Refund only + RFS + ordered + no + no + supplier + + + + + Replace deliver to vendor + DSRPLB + no + ordered + no + customer + + + + + Replace receive in vendor + DSRPLV + no + no + received + customer + + + + + Replace deliver to customer + DSRPC + no + no + received + supplier + + + + + Always + alw + always + + + diff --git a/rma/data/rma_sequence.xml b/rma/data/rma_sequence.xml new file mode 100644 index 00000000..1162e9ad --- /dev/null +++ b/rma/data/rma_sequence.xml @@ -0,0 +1,39 @@ + + + + Customer RMA sequence + rma.order.customer + 5 + RMA/%(year)s/ + + + + + + Supplier RMA sequence + rma.order.supplier + 5 + RTV/%(year)s/ + + + + + + Customer RMA Line sequence + rma.order.line.customer + 5 + RMAL/%(year)s/ + + + + + + Supplier RMA Line sequence + rma.order.line.supplier + 5 + RTVL/%(year)s/ + + + + + diff --git a/rma/data/stock_data.xml b/rma/data/stock_data.xml new file mode 100644 index 00000000..7dd24cdc --- /dev/null +++ b/rma/data/stock_data.xml @@ -0,0 +1,212 @@ + + + + WH RMA + internal + + + + + DOC + RCO + 5 + + + + + INC + RCI + 5 + + + + + DOS + RSO + 5 + + + + + INS + RSI + 5 + + + + + DSS + DSS + 5 + + + + + RMA → Customer + + + + + outgoing + + + + Customer → RMA + + + + + incoming + + + + RMA -> Supplier + + + + + outgoing + + + + Supplier -> RMA + + + + + incoming + + + + Customer -> Supplier + + + + + incoming + + + + Supplier -> Customer + + + + + incoming + + + + + + + + + + + + RMA Customer + 10 + + + + + + + + + RMA Supplier + 10 + + + + + + + + + RMA Dropship + 10 + + + + + + + + + Customer → RMA + move + + + + make_to_stock + + + + + + RMA → Customer + move + + + + make_to_stock + + + + + + RMA → Supplier + move + + + + make_to_stock + + + + + + Supplier → RMA + move + + + + make_to_stock + + + + + + Customer → Supplier + move + + + + make_to_stock + + + + + + Supplier → Customer + move + + + + make_to_stock + + + + + diff --git a/rma/demo/stock_demo.xml b/rma/demo/stock_demo.xml new file mode 100644 index 00000000..5af76da5 --- /dev/null +++ b/rma/demo/stock_demo.xml @@ -0,0 +1,19 @@ + + + + + RMA + internal + + + + + + + + + + + + + diff --git a/rma/models/__init__.py b/rma/models/__init__.py new file mode 100644 index 00000000..ad034891 --- /dev/null +++ b/rma/models/__init__.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 . import rma_order +from . import rma_order_line +from . import rma_operation +from . import rma_rule +from . import stock +from . import stock_warehouse +from . import product +from . import procurement +from . import res_company diff --git a/rma/models/procurement.py b/rma/models/procurement.py new file mode 100644 index 00000000..a55e8910 --- /dev/null +++ b/rma/models/procurement.py @@ -0,0 +1,29 @@ +# -*- 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 ProcurementOrder(models.Model): + _inherit = 'procurement.order' + + rma_line_id = fields.Many2one('rma.order.line', 'RMA', ondelete="set null") + + @api.model + def _run_move_create(self, procurement): + res = super(ProcurementOrder, self)._run_move_create(procurement) + if procurement.rma_line_id: + line = procurement.rma_line_id + res['rma_line_id'] = line.id + if line.delivery_address_id and line.delivery_address_id.id: + res['partner_id'] = line.delivery_address_id.id + elif line.invoice_line_id.invoice_id.partner_id: + res['partner_id'] = line.invoice_id.partner_id.id + return res + + +class ProcurementGroup(models.Model): + _inherit = 'procurement.group' + + rma_id = fields.Many2one('rma.order', 'RMA', ondelete="set null") diff --git a/rma/models/product.py b/rma/models/product.py new file mode 100644 index 00000000..2a992b25 --- /dev/null +++ b/rma/models/product.py @@ -0,0 +1,19 @@ +# -*- 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 fields, models + + +class ProductCategory(models.Model): + _inherit = 'product.category' + + rma_operation_id = fields.Many2one( + comodel_name="rma.operation", string="RMA Operation") + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + rma_operation_id = fields.Many2one( + comodel_name="rma.operation", string="RMA Operation") diff --git a/rma/models/res_company.py b/rma/models/res_company.py new file mode 100644 index 00000000..efc8f43c --- /dev/null +++ b/rma/models/res_company.py @@ -0,0 +1,11 @@ +# -*- 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 + + +class ResCompany(models.Model): + _inherit = 'res.company' + + rma_rule_id = fields.Many2one('rma.rule', 'Default RMA Approval Policy') diff --git a/rma/models/rma_operation.py b/rma/models/rma_operation.py new file mode 100644 index 00000000..87d271e0 --- /dev/null +++ b/rma/models/rma_operation.py @@ -0,0 +1,63 @@ +# -*- 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): + _name = 'rma.operation' + _description = 'RMA Operation' + + @api.model + def _default_warehouse_id(self): + company = self.env.user.company_id.id + warehouse = self.env['stock.warehouse'].search( + [('company_id', '=', company)], limit=1) + return warehouse + + @api.model + def _default_customer_location_id(self): + return self.env.ref('stock.stock_location_customers') or False + + @api.model + def _default_supplier_location_id(self): + return self.env.ref('stock.stock_location_suppliers') or False + + name = fields.Char('Description', required=True) + code = fields.Char('Code', required=True) + refund_policy = fields.Selection([ + ('no', 'No refund'), ('ordered', 'Based on Ordered Quantities'), + ('received', 'Based on Received Quantities')], string="Refund Policy", + default='no') + receipt_policy = fields.Selection([ + ('no', 'Not required'), ('ordered', 'Based on Ordered Quantities'), + ('received', 'Based on Delivered Quantities')], + string="Receipts Policy", default='no') + delivery_policy = fields.Selection([ + ('no', 'Not required'), ('ordered', 'Based on Ordered Quantities'), + ('received', 'Based on Received Quantities')], + string="Delivery Policy", default='no') + in_route_id = fields.Many2one( + 'stock.location.route', string='Inbound Route', + domain=[('rma_selectable', '=', True)]) + out_route_id = fields.Many2one( + 'stock.location.route', string='Outbound Route', + domain=[('rma_selectable', '=', True)]) + customer_to_supplier= fields.Boolean( + 'The customer will send to the supplier', default=False) + supplier_to_customer = fields.Boolean( + 'The supplier will send to the customer', default=False) + in_warehouse_id = fields.Many2one('stock.warehouse', + string='Inbound Warehouse', + default=_default_warehouse_id) + out_warehouse_id = fields.Many2one('stock.warehouse', + string='Outbound Warehouse', + default=_default_warehouse_id) + location_id = fields.Many2one( + 'stock.location', 'Send To This Company Location') + type = fields.Selection([ + ('customer', 'Customer'), ('supplier', 'Supplier')], + string="Used in RMA of this type", required=True, default='customer') + rma_line_ids = fields.One2many('rma.order.line', 'operation_id', + 'RMA lines') diff --git a/rma/models/rma_order.py b/rma/models/rma_order.py new file mode 100644 index 00000000..9f99a636 --- /dev/null +++ b/rma/models/rma_order.py @@ -0,0 +1,234 @@ +# -*- 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 +from openerp.exceptions import UserError +from dateutil.relativedelta import relativedelta +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT +from datetime import datetime + + +class RmaOrder(models.Model): + _name = "rma.order" + _inherit = ['mail.thread'] + + @api.model + def _compute_rule_id(self): + if self.company_id and self.company_id.id: + if self.company_id.rma_rule_id and self.company_id.rma_rule_id.id: + self.rule_id = self.company_id.rma_rule_id + + @api.model + def _get_default_type(self): + if 'supplier' in self.env.context: + return "supplier" + else: + return "customer" + + @api.multi + def _compute_in_shipment_count(self): + for rec in self: + rec.in_shipment_count = len(rec.rma_line_ids.mapped( + 'procurement_ids.move_ids').filtered( + lambda m: m.location_dest_id.usage == 'internal').mapped( + 'picking_id')) + + @api.multi + def _compute_out_shipment_count(self): + for rec in self: + rec.out_shipment_count = len(rec.rma_line_ids.mapped( + 'procurement_ids.move_ids').filtered( + lambda m: m.location_id.usage == 'internal').mapped( + 'picking_id')) + + @api.multi + def _compute_supplier_line_count(self): + self.supplier_line_count = len(self.rma_line_ids.filtered( + lambda r: r.supplier_rma_line_ids)) + + @api.multi + def _compute_line_count(self): + self.ensure_one() + self.line_count = len(self._get_valid_lines()) + + name = fields.Char(string='Order Number', index=True, + readonly=True, + states={'progress': [('readonly', False)]}, + copy=False) + type = fields.Selection( + [('customer', 'Customer'), ('supplier', 'Supplier')], + string="Type", required=True, default=_get_default_type, readonly=True) + reference = fields.Char(string='Reference', + help="The partner reference of this RMA order.") + + comment = fields.Text('Additional Information', readonly=True, states={ + 'draft': [('readonly', False)]}) + + state = fields.Selection([('draft', 'Draft'), ('to_approve', 'To Approve'), + ('approved', 'Approved'), + ('done', 'Done')], string='State', index=True, + default='draft') + date_rma = fields.Datetime(string='Order Date', index=True, copy=False) + partner_id = fields.Many2one('res.partner', string='Partner', + required=True, readonly=True, + states={'draft': [('readonly', False)]}) + assigned_to = fields.Many2one('res.users', 'Assigned to', + track_visibility='onchange') + requested_by = fields.Many2one('res.users', 'Requested by', + track_visibility='onchange', + default=lambda self: self.env.user) + rule_id = fields.Many2one('rma.rule', string='Approval Criteria', + compute=_compute_rule_id) + rma_line_ids = fields.One2many('rma.order.line', 'rma_id', + string='RMA lines') + in_shipment_count = fields.Integer(compute=_compute_in_shipment_count, + string='# of Invoices') + out_shipment_count = fields.Integer(compute=_compute_out_shipment_count, + string='# of Outgoing Shipments') + line_count = fields.Integer(compute=_compute_line_count, + string='# of Outgoing Shipments', + copy=False) + supplier_line_count = fields.Integer(compute=_compute_supplier_line_count, + string='# of Outgoing Shipments', + copy=False) + company_id = fields.Many2one('res.company', string='Company', + required=True, default=lambda self: + self.env.user.company_id) + + @api.model + def create(self, vals): + if self.env.context.get('supplier'): + vals['name'] = self.env['ir.sequence'].next_by_code( + 'rma.order.supplier') + else: + vals['name'] = self.env['ir.sequence'].next_by_code( + 'rma.order.customer') + return super(RmaOrder, self).create(vals) + + @api.multi + def action_view_in_shipments(self): + action = self.env.ref('stock.action_picking_tree_all') + result = action.read()[0] + picking_ids = [] + suppliers = self.env.ref('stock.stock_location_suppliers') + customers = self.env.ref('stock.stock_location_customers') + for line in self.rma_line_ids: + if line.type == 'customer': + for move in line.move_ids: + if move.picking_id.location_id == customers: + picking_ids.append(move.picking_id.id) + else: + for move in line.move_ids: + if move.picking_id.location_id == suppliers: + picking_ids.append(move.picking_id.id) + shipments = list(set(picking_ids)) + # choose the view_mode accordingly + if len(shipments) != 1: + result['domain'] = "[('id', 'in', " + \ + str(shipments) + ")]" + elif len(shipments) == 1: + res = self.env.ref('stock.view_picking_form', False) + result['views'] = [(res and res.id or False, 'form')] + result['res_id'] = shipments[0] + return result + + @api.multi + def action_view_out_shipments(self): + action = self.env.ref('stock.action_picking_tree_all') + result = action.read()[0] + picking_ids = [] + suppliers = self.env.ref('stock.stock_location_suppliers') + customers = self.env.ref('stock.stock_location_customers') + for line in self.rma_line_ids: + if line.type == 'customer': + for move in line.move_ids: + if move.picking_id.location_id != customers: + picking_ids.append(move.picking_id.id) + else: + for move in line.move_ids: + if move.picking_id.location_id != suppliers: + picking_ids.append(move.picking_id.id) + shipments = list(set(picking_ids)) + # choose the view_mode accordingly + if len(shipments) != 1: + result['domain'] = "[('id', 'in', " + \ + str(shipments) + ")]" + elif len(shipments) == 1: + res = self.env.ref('stock.view_picking_form', False) + result['views'] = [(res and res.id or False, 'form')] + result['res_id'] = shipments[0] + return result + + @api.multi + def action_rma_to_approve(self): + for rec in self: + rec.state = 'to_approve' + if rec.rule_id and rec.rule_id.id: + if rec.rule_id.approval_policy == 'always': + rec.assigned_to = self.env.uid + rec.action_rma_approve() + return True + + @api.multi + def action_rma_draft(self): + for rec in self: + rec.state = 'draft' + return True + + @api.multi + def action_rma_approve(self): + # pass the supplier address in case this is a customer RMA + for rec in self: + rec.state = 'approved' + return True + + @api.multi + def action_rma_done(self): + for rec in self: + rec.state = 'done' + return True + + @api.multi + def _get_valid_lines(self): + self.ensure_one() + return self.rma_line_ids + + @api.multi + def action_view_lines(self): + if self.type == 'customer': + action = self.env.ref('rma.action_rma_customer_lines') + else: + action = self.env.ref('rma.action_rma_supplier_lines') + result = action.read()[0] + lines = self._get_valid_lines() + # choose the view_mode accordingly + if len(lines) != 1: + result['domain'] = "[('id', 'in', " + \ + str(lines.ids) + ")]" + elif len(lines) == 1: + if self.type == 'customer': + res = self.env.ref('rma.view_rma_line_form', False) + else: + res = self.env.ref('rma.view_rma_line_supplier_form', False) + + result['views'] = [(res and res.id or False, 'form')] + result['res_id'] = lines.id + return result + + @api.multi + def action_view_supplier_lines(self): + action = self.env.ref('rma.action_rma_supplier_lines') + result = action.read()[0] + lines = self.rma_line_ids + related_lines = [line.id for line in lines.children_ids] + # choose the view_mode accordingly + if len(related_lines) != 1: + result['domain'] = "[('id', 'in', " + \ + str(related_lines) + ")]" + elif len(related_lines) == 1: + res = self.env.ref('rma.view_rma_line_supplier_form', False) + result['views'] = [(res and res.id or False, 'form')] + result['res_id'] = related_lines[0] + return result diff --git a/rma/models/rma_order_line.py b/rma/models/rma_order_line.py new file mode 100644 index 00000000..a2847bb7 --- /dev/null +++ b/rma/models/rma_order_line.py @@ -0,0 +1,423 @@ +# -*- 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 +import operator +ops = {'=': operator.eq, + '!=': operator.ne} + + +class RmaOrderLine(models.Model): + _name = "rma.order.line" + _rec_name = "rma_id" + + @api.model + def _default_warehouse_id(self): + rma_id = self.env.context.get('default_rma_id', False) + warehouse = self.env['stock.warehouse'] + if rma_id: + rma = self.env['rma.order'].browse(rma_id) + warehouse = self.env['stock.warehouse'].search( + [('company_id', '=', rma.company_id.id)], limit=1) + return warehouse + + @api.model + def _default_location_id(self): + wh = self._default_warehouse_id() + return wh.lot_rma_id + + @api.model + def _default_delivery_address(self): + partner_id = self.env.context.get('partner_id', False) + if partner_id: + partner = self.env['res.partner'].browse(partner_id) + addr = partner.address_get(['delivery']) + return self.env['res.partner'].browse(addr['delivery']) + return False + + @api.multi + def _compute_in_shipment_count(self): + for line in self: + moves = line.procurement_ids.mapped('move_ids').filtered( + lambda m: m.location_dest_id.usage == 'internal') + pickings = moves.mapped('picking_id') + line.in_shipment_count = len(pickings) + + @api.multi + def _compute_out_shipment_count(self): + for line in self: + moves = line.procurement_ids.mapped('move_ids').filtered( + lambda m: m.location_dest_id.usage != 'internal') + pickings = moves.mapped('picking_id') + line.out_shipment_count = len(pickings) + + @api.multi + def _get_rma_move_qty(self, states, direction='in'): + for rec in self: + product_obj = self.env['product.uom'] + qty = 0.0 + if direction == 'in': + op = ops['='] + else: + op = ops['!='] + for move in rec.procurement_ids.mapped('move_ids').filtered( + lambda m: m.state in states and op(m.location_id.usage, + rec.type)): + qty += product_obj._compute_qty_obj( + move.product_uom, move.product_uom_qty, + rec.uom_id) + return qty + + @api.multi + @api.depends('move_ids', 'move_ids.state', 'qty_received', + 'receipt_policy', 'product_qty', 'type') + def _compute_qty_to_receive(self): + for rec in self: + rec.qty_to_receive = 0.0 + if rec.receipt_policy == 'ordered': + rec.qty_to_receive = rec.product_qty - rec.qty_received + elif self.receipt_policy == 'delivered': + self.qty_to_receive = rec.qty_delivered - rec.qty_received + + @api.multi + @api.depends('move_ids', 'move_ids.state', + 'delivery_policy', 'product_qty', 'type', 'qty_delivered', + 'qty_received') + def _compute_qty_to_deliver(self): + for rec in self: + rec.qty_to_deliver = 0.0 + if rec.delivery_policy == 'ordered': + rec.qty_to_deliver = rec.product_qty - rec.qty_delivered + elif self.delivery_policy == 'received': + self.qty_to_deliver = rec.qty_received - rec.qty_delivered + + @api.multi + @api.depends('move_ids', 'move_ids.state', 'type') + def _compute_qty_incoming(self): + for rec in self: + qty = rec._get_rma_move_qty( + ('draft', 'confirmed', 'assigned'), direction='in') + rec.qty_incoming = qty + + @api.multi + @api.depends('move_ids', 'move_ids.state', 'type') + def _compute_qty_received(self): + for rec in self: + qty = rec._get_rma_move_qty('done', direction='in') + rec.qty_received = qty + + @api.multi + @api.depends('move_ids', 'move_ids.state', 'type') + def _compute_qty_outgoing(self): + for rec in self: + qty = rec._get_rma_move_qty( + ('draft', 'confirmed', 'assigned'), direction='out') + rec.qty_outgoing = qty + + @api.multi + @api.depends('move_ids', 'move_ids.state', 'type') + def _compute_qty_delivered(self): + for rec in self: + qty = rec._get_rma_move_qty('done', direction='out') + rec.qty_delivered = qty + + @api.model + def _get_supplier_rma_qty(self): + return sum(self.supplier_rma_line_ids.filtered( + lambda r: r.state != 'cancel').mapped( + 'product_qty')) + + @api.multi + @api.depends('customer_to_supplier', 'supplier_rma_line_ids', + 'supplier_rma_line_ids.rma_id.state', + 'move_ids', 'move_ids.state', 'qty_received', + 'receipt_policy', 'product_qty', 'type') + def _compute_qty_supplier_rma(self): + for rec in self: + qty = rec._get_supplier_rma_qty() + rec.qty_to_supplier_rma = rec.qty_to_receive - qty + rec.qty_in_supplier_rma = qty + + @api.multi + def _compute_procurement_count(self): + for rec in self: + rec.procurement_count = len(rec.procurement_ids.filtered( + lambda p: p.state == 'exception')) + + delivery_address_id = fields.Many2one( + 'res.partner', string='Partner delivery address', + default=_default_delivery_address, + help="This address will be used to " + "deliver repaired or replacement products.") + + rma_id = fields.Many2one('rma.order', string='RMA', + ondelete='cascade', required=True) + name = fields.Char(string='Reference', required=True, default='/', + help='Add here the supplier RMA #. Otherwise an ' + 'internal code is assigned.') + description = fields.Text(string='Description') + origin = fields.Char(string='Source Document', + help="Reference of the document that produced " + "this rma.") + state = fields.Selection(related='rma_id.state') + operation_id = fields.Many2one( + comodel_name="rma.operation", string="Operation") + + assigned_to = fields.Many2one('res.users', related='rma_id.assigned_to') + requested_by = fields.Many2one('res.users', related='rma_id.requested_by') + partner_id = fields.Many2one('res.partner', related='rma_id.partner_id', + store=True) + sequence = fields.Integer(default=10, + help="Gives the sequence of this line " + "when displaying the rma.") + product_id = fields.Many2one('product.product', string='Product', + ondelete='restrict', required=-True) + product_tracking = fields.Selection(related="product_id.tracking") + lot_id = fields.Many2one( + comodel_name="stock.production.lot", string="Lot/Serial Number", + readonly=True, states={"new": [("readonly", False)]}, + ) + product_qty = fields.Float( + string='Ordered Qty', copy=False, + digits=dp.get_precision('Product Unit of Measure')) + uom_id = fields.Many2one('product.uom', string='Unit of Measure', + required=True) + price_unit = fields.Monetary(string='Price Unit', readonly=False, + states={'approved': [('readonly', True)], + 'done': [('readonly', True)], + 'to_approve': [('readonly', True)]}) + + procurement_count = fields.Integer(compute=_compute_procurement_count, + string='# of Procurements', copy=False, + default=0) + in_shipment_count = fields.Integer(compute=_compute_in_shipment_count, + string='# of Shipments', default=0) + out_shipment_count = fields.Integer(compute=_compute_out_shipment_count, + string='# of Deliveries', default=0) + move_ids = fields.One2many('stock.move', 'rma_line_id', + string='Stock Moves', readonly=True + , copy=False) + reference_move_id = fields.Many2one(comodel_name='stock.move', + string='Originating stock move', + readonly=True, copy=False) + procurement_ids = fields.One2many('procurement.order', 'rma_line_id', + string='Procurements', readonly=True, + states={'draft': [('readonly', False)]}, + copy=False) + currency_id = fields.Many2one('res.currency', string="Currency") + company_id = fields.Many2one('res.company', string='Company', + related='rma_id.company_id', store=True) + type = fields.Selection(related='rma_id.type') + customer_to_supplier = fields.Boolean( + 'The customer will send to the supplier') + supplier_to_customer = fields.Boolean( + 'The supplier will send to the customer') + receipt_policy = fields.Selection([ + ('no', 'Not required'), ('ordered', 'Based on Ordered Quantities'), + ('delivered', 'Based on Delivered Quantities')], + required=True, string="Receipts Policy") + delivery_policy = fields.Selection([ + ('no', 'Not required'), ('ordered', 'Based on Ordered Quantities'), + ('received', 'Based on Received Quantities')], required=True, + string="Delivery Policy") + in_route_id = fields.Many2one( + 'stock.location.route', string='Inbound Route', + required=True, + domain=[('rma_selectable', '=', True)]) + out_route_id = fields.Many2one( + 'stock.location.route', string='Outbound Route', + required=True, + domain=[('rma_selectable', '=', True)]) + in_warehouse_id = fields.Many2one('stock.warehouse', + string='Inbound Warehouse', + required=True, + default=_default_warehouse_id) + out_warehouse_id = fields.Many2one('stock.warehouse', + string='Outbound Warehouse', + required=True, + default=_default_warehouse_id) + location_id = fields.Many2one( + 'stock.location', 'Send To This Company Location', required=True, + default=_default_location_id) + customer_rma_id = fields.Many2one( + 'rma.order.line', string='Customer RMA line', ondelete='cascade') + supplier_rma_line_ids = fields.One2many('rma.order.line', 'customer_rma_id') + supplier_address_id = fields.Many2one( + 'res.partner', readonly=True, + states={'draft': [('readonly', False)]}, + string='Supplier Address', + help="This address of the supplier in case of Customer RMA operation " + "dropship.") + qty_to_receive = fields.Float( + string='Qty To Receive', + digits=dp.get_precision('Product Unit of Measure'), + compute=_compute_qty_to_receive, store=True) + qty_incoming = fields.Float( + string='Incoming Qty', copy=False, + readonly=True, digits=dp.get_precision('Product Unit of Measure'), + compute=_compute_qty_incoming, store=True) + qty_received = fields.Float( + string='Qty Received', copy=False, + digits=dp.get_precision('Product Unit of Measure'), + compute=_compute_qty_received, + store=True) + qty_to_deliver = fields.Float( + string='Qty To Deliver', copy=False, + digits=dp.get_precision('Product Unit of Measure'), + readonly=True, compute=_compute_qty_to_deliver, + store=True) + qty_outgoing = fields.Float( + string='Outgoing Qty', copy=False, + readonly=True, digits=dp.get_precision('Product Unit of Measure'), + compute=_compute_qty_outgoing, + store=True) + qty_delivered = fields.Float( + string='Qty Delivered', copy=False, + digits=dp.get_precision('Product Unit of Measure'), + readonly=True, compute=_compute_qty_delivered, + store=True) + qty_to_supplier_rma = fields.Float( + string='Qty to send to Supplier RMA', + digits=dp.get_precision('Product Unit of Measure'), + readonly=True, compute=_compute_qty_supplier_rma, + store=True) + qty_in_supplier_rma = fields.Float( + string='Qty in Supplier RMA', + digits=dp.get_precision('Product Unit of Measure'), + readonly=True, compute=_compute_qty_supplier_rma, + store=True) + + @api.model + def create(self, vals): + if self.env.context.get('supplier'): + vals['name'] = self.env['ir.sequence'].next_by_code( + 'rma.order.line.supplier') + else: + vals['name'] = self.env['ir.sequence'].next_by_code( + 'rma.order.line.customer') + return super(RmaOrderLine, self).create(vals) + + @api.onchange('product_id') + def _onchange_product_id(self): + result = {} + if not self.product_id: + return result + self.product_qty = 1 + self.uom_id = self.product_id.uom_id.id + self.price_unit = self.product_id.standard_price + self.operation_id = self.product_id.rma_operation_id or \ + self.product_id.categ_id.rma_operation_id + return result + + @api.onchange('operation_id') + def _onchange_operation_id(self): + result = {} + if not self.operation_id: + return result + self.receipt_policy = self.operation_id.receipt_policy + self.delivery_policy = self.operation_id.delivery_policy + self.in_warehouse_id = self.operation_id.in_warehouse_id + self.out_warehouse_id = self.operation_id.out_warehouse_id + self.location_id = self.operation_id.location_id or \ + self.in_warehouse_id.lot_rma_id + self.customer_to_supplier = self.operation_id.customer_to_supplier + self.supplier_to_customer = self.operation_id.supplier_to_customer + self.in_route_id = self.operation_id.in_route_id + self.out_route_id = self.operation_id.out_route_id + return result + + @api.onchange('customer_to_supplier', 'type') + def _onchange_receipt_policy(self): + if self.type == 'supplier' and self.customer_to_supplier: + self.receipt_policy = 'no' + elif self.type == 'customer' and self.supplier_to_customer: + self.delivery_policy = 'no' + + @api.onchange('product_id') + def _onchange_product_id(self): + self.uom_id = self.product_id.uom_id + if self.lot_id.product_id != self.product_id: + self.lot_id = False + if self.product_id: + return {'domain': { + 'lot_id': [('product_id', '=', self.product_id.id)]}} + return {'domain': {'lot_id': []}} + + @api.onchange("lot_id") + def _onchange_lot_id(self): + product = self.lot_id.product_id + if product: + self.product_id = product + self.uom_id = product.uom_id + + @api.multi + def action_view_in_shipments(self): + action = self.env.ref('stock.action_picking_tree_all') + result = action.read()[0] + picking_ids = [] + suppliers = self.env.ref('stock.stock_location_suppliers') + customers = self.env.ref('stock.stock_location_customers') + for line in self: + if line.type == 'customer': + for move in line.move_ids: + if move.picking_id.location_id == customers: + picking_ids.append(move.picking_id.id) + else: + for move in line.move_ids: + if move.picking_id.location_id == suppliers: + picking_ids.append(move.picking_id.id) + shipments = list(set(picking_ids)) + # choose the view_mode accordingly + if len(shipments) != 1: + result['domain'] = "[('id', 'in', " + \ + str(shipments) + ")]" + elif len(shipments) == 1: + res = self.env.ref('stock.view_picking_form', False) + result['views'] = [(res and res.id or False, 'form')] + result['res_id'] = shipments[0] + return result + + @api.multi + def action_view_out_shipments(self): + action = self.env.ref('stock.action_picking_tree_all') + result = action.read()[0] + picking_ids = [] + suppliers = self.env.ref('stock.stock_location_suppliers') + customers = self.env.ref('stock.stock_location_customers') + for line in self: + if line.type == 'customer': + for move in line.move_ids: + if move.picking_id.location_id != customers: + picking_ids.append(move.picking_id.id) + else: + for move in line.move_ids: + if move.picking_id.location_id != suppliers: + picking_ids.append(move.picking_id.id) + shipments = list(set(picking_ids)) + # choose the view_mode accordingly + if len(shipments) != 1: + result['domain'] = "[('id', 'in', " + \ + str(shipments) + ")]" + elif len(shipments) == 1: + res = self.env.ref('stock.view_picking_form', False) + result['views'] = [(res and res.id or False, 'form')] + result['res_id'] = shipments[0] + return result + + @api.multi + def action_view_procurements(self): + action = self.env.ref('procurement.procurement_order_action_exceptions') + result = action.read()[0] + procurements = self.procurement_ids.filtered( + lambda p: p.state == 'exception').ids + # choose the view_mode accordingly + if len(procurements) != 1: + result['domain'] = "[('id', 'in', " + \ + str(procurements) + ")]" + elif len(procurements) == 1: + res = self.env.ref('procurement.procurement_form_view', False) + result['views'] = [(res and res.id or False, 'form')] + result['res_id'] = procurements[0] + return result diff --git a/rma/models/rma_rule.py b/rma/models/rma_rule.py new file mode 100644 index 00000000..ec3c9255 --- /dev/null +++ b/rma/models/rma_rule.py @@ -0,0 +1,16 @@ +# -*- 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 RmaRule(models.Model): + _name = 'rma.rule' + _description = 'RMA Approval Conditions' + + name = fields.Char('Description', required=True) + code = fields.Char('Code', required=True) + approval_policy = fields.Selection([ + ('always', 'Always')], string="Approval Policy", + required=True, default='always') diff --git a/rma/models/stock.py b/rma/models/stock.py new file mode 100644 index 00000000..cc70c174 --- /dev/null +++ b/rma/models/stock.py @@ -0,0 +1,37 @@ +# -*- 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 StockPicking(models.Model): + _inherit = "stock.picking" + + @api.multi + def action_assign(self): + for picking in self: + for move in picking.move_lines: + if len(move.rma_line_id): + if move.state in ('confirmed', 'waiting', 'assigned') \ + and move.location_id.usage in ( + 'supplier', 'customer'): + move.force_assign() + return super(StockPicking, self).action_assign() + + +class StockMove(models.Model): + + _inherit = "stock.move" + + rma_line_id = fields.Many2one('rma.order.line', string='RMA', + ondelete='restrict') + + @api.model + def create(self, vals): + if vals.get('procurement_id', False): + procurement = self.env['procurement.order'].browse( + vals['procurement_id']) + if procurement.rma_line_id and procurement.rma_line_id.id: + vals['rma_line_id'] = procurement.rma_line_id.id + return super(StockMove, self).create(vals) diff --git a/rma/models/stock_warehouse.py b/rma/models/stock_warehouse.py new file mode 100644 index 00000000..3d465a60 --- /dev/null +++ b/rma/models/stock_warehouse.py @@ -0,0 +1,26 @@ +# -*- 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 StockWarehouse(models.Model): + + _inherit = "stock.warehouse" + + lot_rma_id = fields.Many2one('stock.location', 'RMA Location') + rma_cust_out_type_id = fields.Many2one('stock.picking.type', + 'RMA Customer out Type') + rma_sup_out_type_id = fields.Many2one('stock.picking.type', + 'RMA Supplier out Type') + rma_cust_in_type_id = fields.Many2one('stock.picking.type', + 'RMA Customer in Type') + rma_sup_in_type_id = fields.Many2one('stock.picking.type', + 'RMA Supplier in Type') + + +class StockLocationRoute(models.Model): + _inherit = "stock.location.route" + + rma_selectable = fields.Boolean(string="Selectable on RMA Lines") diff --git a/rma/security/ir.model.access.csv b/rma/security/ir.model.access.csv new file mode 100644 index 00000000..291e1e5d --- /dev/null +++ b/rma/security/ir.model.access.csv @@ -0,0 +1,11 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_rma_user_customer,rma.order.customer.user,model_rma_order,group_rma_customer_user,1,1,1,1 +access_rma_user_supplier,rma.order.supplier.user,model_rma_order,group_rma_supplier_user,1,1,1,1 +access_rma_manager,rma.order.manager,model_rma_order,group_rma_manager,1,1,1,1 +access_rma_line_customer_user,rma.order.line.customer.user,model_rma_order_line,group_rma_customer_user,1,1,1,1 +access_rma_line_supplier_user,rma.order.line.customer.supplier,model_rma_order_line,group_rma_supplier_user,1,1,1,1 +access_rma_line_manager,rma.order.line,model_rma_order_line,group_rma_manager,1,1,1,1 +access_rma_operation_manager,access_rma_operation,model_rma_operation,group_rma_manager,1,1,1,1 +access_rma_operation_customer_user,access_rma_operation,model_rma_operation,group_rma_customer_user,1,0,0,0 +access_rma_operation_supplier_user,access_rma_operation,model_rma_operation,group_rma_supplier_user,1,0,0,0 +access_stock_config_settings,access_stock_config_settings,model_stock_config_settings,stock.group_stock_manager,1,1,1,1 diff --git a/rma/security/rma.xml b/rma/security/rma.xml new file mode 100644 index 00000000..ab04e69c --- /dev/null +++ b/rma/security/rma.xml @@ -0,0 +1,38 @@ + + + + + + RMA + + 30 + + + + RMA Customer User + + + + + + RMA Supplier User + + + + + + RMA Manager + + + + + + + Addresses in RMA + + + + + + diff --git a/rma/tests/__init__.py b/rma/tests/__init__.py new file mode 100644 index 00000000..1c2b27c0 --- /dev/null +++ b/rma/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- 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 test_rma diff --git a/rma/tests/test_rma.py b/rma/tests/test_rma.py new file mode 100644 index 00000000..6ae75d89 --- /dev/null +++ b/rma/tests/test_rma.py @@ -0,0 +1,242 @@ +# -*- 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.tests import common + + +class TestRma(common.TransactionCase): + + """ Test the routes and the quantities """ + + def setUp(self): + super(TestRma, self).setUp() + + self.rma_make_picking = self.env['rma_make_picking.wizard'] + self.rma_add_invoice = self.env['rma_add_invoice'] + self.stockpicking = self.env['stock.picking'] + self.rma = self.env['rma.order'] + self.rma_line = self.env['rma.order.line'] + self.rma_op = self.env['rma.operation'] + self.rma_op_id = self.env.ref('rma.rma_operation_customer_replace') + self.product_id = self.env.ref('product.product_product_4') + self.product_1 = self.env.ref('product.product_product_25') + self.product_2 = self.env.ref('product.product_product_30') + self.product_3 = self.env.ref('product.product_product_33') + self.uom_unit = self.env.ref('product.product_uom_unit') + # assign an operation + self.product_1.write({'rma_operation_id': self.rma_op_id.id}) + self.product_2.write({'rma_operation_id': self.rma_op_id.id}) + self.product_3.write({'rma_operation_id': self.rma_op_id.id}) + self.partner_id = self.env.ref('base.res_partner_12') + + self.customer_location_id = self.env.ref( + 'stock.stock_location_customers' + ) + uom_unit = self.env.ref('product.product_uom_unit') + sale_values = self._prepare_sale() + self.sale_order = self.env['sale.order'].create(sale_values) + invoice_id = self.sale_order.action_invoice_create()[0] + self.invoice = self.env['account.invoice'].browse(invoice_id) + # Create the RMA from the invoice + self.rma_id = self.rma.create( + { + 'reference': '0001', + 'type': 'customer', + 'partner_id': self.env.ref('base.res_partner_2').id + }) + + for line in self.invoice.invoice_line_ids: + data = self.rma_add_invoice.with_context( + {'invoice_id': self.invoice.id} + )._prepare_rma_line_from_inv_line(line) + data.update(rma_id=self.rma_id.id) + self.rma_line.create(data) + #approve the RMA + self.rma_id.action_rma_to_approve() + self.rma_id.action_rma_approve() + + def _prepare_sale(self): + values = { + 'state': 'done', + 'partner_id': self.env.ref('base.res_partner_2').id, + 'pricelist_id': self.env.ref('product.list0').id, + 'warehouse_id': self.env.ref('stock.warehouse0').id, + 'partner_invoice_id': self.env.ref('base.res_partner_2').id, + 'partner_shipping_id': self.env.ref('base.res_partner_2').id, + 'picking_policy': 'direct', + 'order_line': [ + (0, False, { + 'name': product.name, + 'product_id': product.id, + 'product_uom_qty': qty, + 'qty_delivered': qty, + 'product_uom': self.uom_unit.id, + 'price_unit': product.list_price + + }) for product, qty in [ + (self.product_1, 3), + (self.product_2, 5), + (self.product_3, 2), + ] + ] + } + return values + + def test_00_receive_items(self): + wizard = self.rma_make_picking.with_context({ + 'active_ids': self.rma_id.rma_line_ids.ids, + 'active_model': 'rma.order.line', + 'picking_type': 'incoming', + 'active_id': 1 + }).create({}) + procurements = wizard._create_picking() + group_ids = set([proc.group_id.id for proc in procurements if + proc.group_id]) + domain = "[('group_id','in',[" + ','.join( + map(str, list(group_ids))) + "])]" + picking = self.stockpicking.search(domain) + self.assertEquals(len(picking), 1, + "Incorrect number of pickings created") + moves = picking.move_lines + self.assertEquals(len(moves), 3, + "Incorrect number of moves created") + for line in self.rma_id.rma_line_ids: + self.assertEquals(line.qty_received, 0, + "Wrong qty received") + self.assertEquals(line.qty_to_deliver, 0, + "Wrong qty to deliver") + self.assertEquals(line.qty_outgoing, 0, + "Wrong qty outgoing") + self.assertEquals(line.qty_delivered, 0, + "Wrong qty delivered") + self.assertEquals(line.qty_to_refund, 0, + "Wrong qty to refund") + self.assertEquals(line.qty_to_refunded, 0, + "Wrong qty refunded") + if line.product_id == self.product_1: + self.assertEquals(line.qty_to_receive, 3, + "Wrong qty to receive") + self.assertEquals(line.qty_incoming, 3, + "Wrong qty incoming") + if line.product_id == self.product_2: + self.assertEquals(line.qty_to_receive, 5, + "Wrong qty to receive") + self.assertEquals(line.qty_incoming, 5, + "Wrong qty incoming") + if line.product_id == self.product_3: + self.assertEquals(line.qty_to_receive, 2, + "Wrong qty to receive") + self.assertEquals(line.qty_incoming, 2, + "Wrong qty incoming") + pickings.action_assign() + pickings.do_transfer() + for line in self.rma_id.rma_line_ids: + self.assertEquals(line.qty_to_receive, 0, + "Wrong qty to_receive") + self.assertEquals(line.qty_incoming, 0, + "Wrong qty incoming") + self.assertEquals(line.qty_outgoing, 0, + "Wrong qty outgoing") + self.assertEquals(line.qty_delivered, 0, + "Wrong qty delivered") + self.assertEquals(line.qty_to_refund, 0, + "Wrong qty to refund") + self.assertEquals(line.qty_to_refunded, 0, + "Wrong qty refunded") + if line.product_id == self.product_1: + self.assertEquals(line.qty_received, 3, + "Wrong qty received") + self.assertEquals(line.qty_to_deliver, 3, + "Wrong qty to_deliver") + if line.product_id == self.product_2: + self.assertEquals(line.qty_received, 5, + "Wrong qty received") + self.assertEquals(line.qty_to_deliver, 5, + "Wrong qty to_deliver") + if line.product_id == self.product_3: + self.assertEquals(line.qty_received, 2, + "Wrong qty received") + self.assertEquals(line.qty_to_deliver, 2, + "Wrong qty to_deliver") + + wizard = self.rma_make_picking.with_context({ + 'active_id': 1, + 'active_ids': self.rma_id.rma_line_ids.ids, + 'active_model': 'rma.order.line', + 'picking_type': 'outgoing', + }).create({}) + procurements = wizard._create_picking() + group_ids = set([proc.group_id.id for proc in procurements if + proc.group_id]) + domain = "[('group_id','in',[" + ','.join( + map(str, list(group_ids))) + "])]" + picking = self.stockpicking.search(domain) + self.assertEquals(len(picking), 1, + "Incorrect number of pickings created") + moves = picking.move_lines + self.assertEquals(len(moves), 3, + "Incorrect number of moves created") + for line in self.rma_id.rma_line_ids: + self.assertEquals(line.qty_to_receive, 0, + "Wrong qty to receive") + self.assertEquals(line.qty_incoming, 0, + "Wrong qty incoming") + self.assertEquals(line.qty_delivered, 0, + "Wrong qty delivered") + self.assertEquals(line.qty_to_refund, 0, + "Wrong qty to refund") + self.assertEquals(line.qty_to_refunded, 0, + "Wrong qty refunded") + if line.product_id == self.product_1: + self.assertEquals(line.qty_to_deliver, 3, + "Wrong qty to deliver") + self.assertEquals(line.qty_outgoing, 3, + "Wrong qty outgoing") + self.assertEquals(line.qty_received, 3, + "Wrong qty received") + if line.product_id == self.product_2: + self.assertEquals(line.qty_received, 5, + "Wrong qty received") + self.assertEquals(line.qty_to_deliver, 5, + "Wrong qty to deliver") + self.assertEquals(line.qty_outgoing, 5, + "Wrong qty outgoing") + if line.product_id == self.product_3: + self.assertEquals(line.qty_received, 2, + "Wrong qty received") + self.assertEquals(line.qty_to_deliver, 2, + "Wrong qty to deliver") + self.assertEquals(line.qty_outgoing, 2, + "Wrong qty outgoing") + pickings.action_assign() + pickings.do_transfer() + for line in self.rma_id.rma_line_ids: + self.assertEquals(line.qty_to_receive, 0, + "Wrong qty to receive") + self.assertEquals(line.qty_incoming, 0, + "Wrong qty incoming") + self.assertEquals(line.qty_to_deliver, 0, + "Wrong qty to deliver") + self.assertEquals(line.qty_to_refund, 0, + "Wrong qty to refund") + self.assertEquals(line.qty_to_refunded, 0, + "Wrong qty refunded") + self.assertEquals(line.qty_outgoing, 0, + "Wrong qty outgoing") + if line.product_id == self.product_1: + self.assertEquals(line.qty_received, 3, + "Wrong qty received") + self.assertEquals(line.qty_delivered, 3, + "Wrong qty delivered") + if line.product_id == self.product_2: + self.assertEquals(line.qty_received, 5, + "Wrong qty received") + self.assertEquals(line.qty_delivered, 5, + "Wrong qty delivered") + if line.product_id == self.product_3: + self.assertEquals(line.qty_received, 2, + "Wrong qty received") + self.assertEquals(line.qty_delivered, 2, + "Wrong qty delivered") + self.rma_id.action_rma_done() diff --git a/rma/views/procurement_view.xml b/rma/views/procurement_view.xml new file mode 100644 index 00000000..a79015f6 --- /dev/null +++ b/rma/views/procurement_view.xml @@ -0,0 +1,31 @@ + + + + + + procurement.order.form + procurement.order + + + + + + + + + + procurement.order.form.stock.inherit + procurement.order + + + + [('usage', '!=', 'view')] + + + + + + + diff --git a/rma/views/product_view.xml b/rma/views/product_view.xml new file mode 100644 index 00000000..74a9fd35 --- /dev/null +++ b/rma/views/product_view.xml @@ -0,0 +1,30 @@ + + + + + + product.category.form + product.category + + + + + + + + + + product.template.stock.property.form.inherit + product.template + + + + + + + + + + + + diff --git a/rma/views/res_company_view.xml b/rma/views/res_company_view.xml new file mode 100644 index 00000000..a2f01d20 --- /dev/null +++ b/rma/views/res_company_view.xml @@ -0,0 +1,17 @@ + + + + + base_phone.company.form + res.company + + + + + + + + + + + \ No newline at end of file diff --git a/rma/views/rma_operation_view.xml b/rma/views/rma_operation_view.xml new file mode 100644 index 00000000..1fc26e7a --- /dev/null +++ b/rma/views/rma_operation_view.xml @@ -0,0 +1,69 @@ + + + + + rma.operation.tree + rma.operation + + + + + + + + + + + + rma.operation.form + rma.operation + +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + Operations + rma.operation + form + tree,form + + + + + +
+
diff --git a/rma/views/rma_order_line_view.xml b/rma/views/rma_order_line_view.xml new file mode 100644 index 00000000..649552f4 --- /dev/null +++ b/rma/views/rma_order_line_view.xml @@ -0,0 +1,387 @@ + + + + + rma.order.line.tree + rma.order.line + + + + + + + + + + + + + + + + + + + + rma.order.line.supplier.tree + rma.order.line + + + + + + + + + + + + + + + + + + + rma.order.line.supplier.form + rma.order.line + +
+ +
+
+ + + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rma.order.line.form + rma.order.line + +
+ +
+
+ + + + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rma.order.line.select + rma.order.line + + + + + + + + + + + + + + + + + + + + + + + Customer RMA Lines + rma.order.line + [('state','in', ['approved', 'done']), + ('type','=', 'customer'), + ('customer_to_supplier','=', False) + ] + {"search_default_assigned_to":uid} + form + tree,form + + + + + Supplier RMA Lines + rma.order.line + [('state','in', ['approved', 'done']), + ('type','=', 'supplier')] + {"search_default_assigned_to":uid, "supplier":1} + form + tree,form + + + + + + tree + + + + + + form + + + + + + + + + + diff --git a/rma/views/rma_order_view.xml b/rma/views/rma_order_view.xml new file mode 100644 index 00000000..b5a89702 --- /dev/null +++ b/rma/views/rma_order_view.xml @@ -0,0 +1,365 @@ + + + + + + rma.order.tree + rma.order + + + + + + + + + + + + + + + + rma.order.supplier.tree + rma.order + + + + + + + + + + + + + + + + rma.order.form + rma.order + +
+ +
+
+ +
+ + + + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+
+ + + rma.order.supplier.form + rma.order + +
+ +
+
+ +
+ + + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+
+ + + rma.order.select + rma.order + + + + + + + + + + + + + + + + + + + Customer RMA + rma.order + form + [('type','=', 'customer')] + {"search_default_assigned_to":uid, + 'customer':1} + tree,form + + + + + Supplier RMA + rma.order + form + [('type','=', 'supplier')] + {"search_default_assigned_to":uid, + 'supplier':1} + tree,form + + + + + + tree + + + + + + form + + + + + + + + + + + + + + +
+
diff --git a/rma/views/rma_rule_view.xml b/rma/views/rma_rule_view.xml new file mode 100644 index 00000000..788ffb6c --- /dev/null +++ b/rma/views/rma_rule_view.xml @@ -0,0 +1,51 @@ + + + + + rma.rule.tree + rma.rule + + + + + + + + + + rma.rule.form + rma.rule + +
+ + + + + + + + + +
+
+
+ + + Rules + rma.rule + form + tree,form + + + + + +
+
diff --git a/rma/views/stock_view.xml b/rma/views/stock_view.xml new file mode 100644 index 00000000..a2f06c74 --- /dev/null +++ b/rma/views/stock_view.xml @@ -0,0 +1,55 @@ + + + + + + rma.move.form + stock.move + + + + + + + + + + + + stock.move.form + stock.move + + + + + + + + + + + + stock.location.route.form + + stock.location.route + + + + + + + + + procurement.order.form.stock.inherit + procurement.order + + + + [('usage', '!=', 'view')] + + + + + + diff --git a/rma/views/stock_warehouse.xml b/rma/views/stock_warehouse.xml new file mode 100644 index 00000000..a626781f --- /dev/null +++ b/rma/views/stock_warehouse.xml @@ -0,0 +1,20 @@ + + + + view_warehouse_form + stock.warehouse + + + + + + + + + + + + + + + diff --git a/rma/wizards/__init__.py b/rma/wizards/__init__.py new file mode 100644 index 00000000..64d335a4 --- /dev/null +++ b/rma/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_add_stock_move +from . import rma_make_picking +from . import stock_config_settings +from . import rma_order_line_make_supplier_rma diff --git a/rma/wizards/rma_add_stock_move.py b/rma/wizards/rma_add_stock_move.py new file mode 100644 index 00000000..e5007085 --- /dev/null +++ b/rma/wizards/rma_add_stock_move.py @@ -0,0 +1,106 @@ +# -*- 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, _ +from openerp.exceptions import ValidationError + + +class RmaAddStockMove(models.TransientModel): + _name = 'rma_add_stock_move' + _description = 'Wizard to add rma lines from pickings' + + @api.model + def default_get(self, fields): + res = super(RmaAddStockMove, 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['picking_id'] = False + res['move_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) + + move_ids = fields.Many2many('stock.move', + string='Stock Moves', + domain="[('state', '=', 'done')]") + + def _prepare_rma_line_from_stock_move(self, sm, lot=False): + operation = sm.product_id.rma_operation_id or \ + sm.product_id.categ_id.rma_operation_id + data = { + 'reference_move_id': sm.id, + 'product_id': sm.product_id.id, + 'lot_id': lot and lot.id or False, + 'name': sm.product_id.name_template, + 'origin': sm.picking_id.name, + 'uom_id': sm.product_uom.id, + 'operation_id': operation.id, + 'product_qty': sm.product_uom_qty, + 'delivery_address_id': sm.picking_id.partner_id.id, + '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, + 'out_route_id': operation.out_route_id.id, + 'receipt_policy': operation.receipt_policy, + 'operation_id': operation.id, + 'refund_policy': operation.refund_policy, + 'delivery_policy': operation.delivery_policy + }) + if operation.in_warehouse_id: + data['in_warehouse_id'] = operation.in_warehouse_id.id + if operation.out_warehouse_id: + data['out_warehouse_id'] = operation.out_warehouse_id.id + if operation.location_id: + data['location_id'] = operation.location_id.id + return data + + @api.model + def _get_existing_stock_moves(self): + existing_move_lines = [] + for rma_line in self.rma_id.rma_line_ids: + existing_move_lines.append(rma_line.reference_move_id) + return existing_move_lines + + @api.multi + def add_lines(self): + rma_line_obj = self.env['rma.order.line'] + existing_stock_moves = self._get_existing_stock_moves() + for sm in self.move_ids: + if sm not in existing_stock_moves: + if sm.lot_ids: + for lot in sm.lot_ids: + data = self._prepare_rma_line_from_stock_move(sm, + lot=lot) + rma_line_obj.with_context( + default_rma_id=self.rma_id.id).create(data) + else: + data = self._prepare_rma_line_from_stock_move(sm, lot=False) + rma_line_obj.with_context( + default_rma_id=self.rma_id.id).create(data) + return {'type': 'ir.actions.act_window_close'} diff --git a/rma/wizards/rma_add_stock_move_view.xml b/rma/wizards/rma_add_stock_move_view.xml new file mode 100644 index 00000000..92df5654 --- /dev/null +++ b/rma/wizards/rma_add_stock_move_view.xml @@ -0,0 +1,128 @@ + + + + + rma.add.stock.move.customer + rma_add_stock_move + +
+ + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + + Add from Stock Move + ir.actions.act_window + rma_add_stock_move + rma.order + form + form + new + + + + + + + rma.order.line.form + rma.order + + + + + + + + rma.add.stock.move.supplier + rma_add_stock_move + +
+ + + + + +
+
+ +
+
+ + + Add from Stock Move + ir.actions.act_window + rma_add_stock_move + rma.order + form + form + new + + + + + + rma.order.line.supplier.form + rma.order + + + + + + +
diff --git a/rma/wizards/rma_make_picking.py b/rma/wizards/rma_make_picking.py new file mode 100644 index 00000000..80f2f5ba --- /dev/null +++ b/rma/wizards/rma_make_picking.py @@ -0,0 +1,233 @@ +# -*- 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 RmaMakePicking(models.TransientModel): + _name = 'rma_make_picking.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.id, + '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. + """ + 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 exceptions.Warning( + _('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)]) + res['item_ids'] = items + return res + + item_ids = fields.One2many( + 'rma_make_picking.wizard.item', + 'wiz_id', string='Items') + + def find_procurement_group(self, item): + return self.env['procurement.group'].search([('rma_id', '=', + item.line_id.rma_id.id)]) + + def _get_procurement_group_data(self, item): + group_data = { + 'name': item.line_id.rma_id.name, + 'rma_id': item.line_id.rma_id.id, + } + return group_data + + @api.model + def _get_address(self, item): + if item.line_id.delivery_address_id: + delivery_address = item.line_id.delivery_address_id or \ + item.line_id.partner_id + elif item.line_id.customer_to_supplier: + delivery_address = item.line_id.supplier_address_id + else: + raise exceptions.Warning('Unknown delivery address') + return delivery_address + + def _get_address_location(self, delivery_address_id, type): + if type == 'supplier': + return delivery_address_id.property_stock_supplier + elif type == 'customer': + return delivery_address_id.property_stock_customer + + @api.model + def _get_procurement_data(self, item, group, qty, picking_type): + line = item.line_id + delivery_address_id = self._get_address(item) + if picking_type == 'incoming': + if line.customer_to_supplier: + location = self._get_address_location( + delivery_address_id, 'supplier') + else: + location = line.location_id + warehouse = line.in_warehouse_id + route = line.in_route_id + else: + location = self._get_address_location(delivery_address_id, + line.rma_id.type) + warehouse = line.out_warehouse_id + route = line.out_route_id + if not route: + raise exceptions.Warning("No route specified") + if not warehouse: + raise exceptions.Warning("No warehouse specified") + procurement_data = { + 'name': line.rma_id.name, + 'group_id': group.id, + 'origin': line.rma_id.name, + 'warehouse_id': warehouse.id, + 'date_planned': time.strftime(DT_FORMAT), + 'product_id': item.product_id.id, + 'product_qty': qty, + 'partner_dest_id': delivery_address_id.id, + 'product_uom': line.product_id.product_tmpl_id.uom_id.id, + 'location_id': location.id, + 'rma_line_id': line.id, + 'route_ids': [(4, route.id)] + } + return procurement_data + + @api.model + def _create_procurement(self, item, picking_type): + group = self.find_procurement_group(item) + if not group: + procurement_group = self._get_procurement_group_data(item) + group = self.env['procurement.group'].create(procurement_group) + if picking_type == 'incoming': + qty = item.qty_to_receive + else: + qty = item.qty_to_deliver + procurement_data = self._get_procurement_data( + item, group, qty, picking_type) + # create picking + procurement = self.env['procurement.order'].create(procurement_data) + procurement.run() + return procurement.id + + @api.multi + def _create_picking(self): + """Method called when the user clicks on create picking""" + picking_type = self.env.context.get('picking_type') + procurement_list = [] + for item in self.item_ids: + line = item.line_id + if line.state != 'approved': + raise exceptions.Warning( + _('RMA %s is not approved') % + line.rma_id.name) + if line.receipt_policy == 'no' and picking_type == \ + 'incoming': + raise exceptions.Warning( + _('No shipments needed for this operation')) + if line.delivery_policy == 'no' and picking_type == \ + 'outgoing': + raise exceptions.Warning( + _('No deliveries needed for this operation')) + procurement = self._create_procurement(item, picking_type) + procurement_list.append(procurement) + procurements = self.env['procurement.order'].browse(procurement_list) + return procurements + + @api.model + def _get_action(self, pickings, procurements): + if pickings: + action = procurements.do_view_pickings() + else: + action = self.env.ref( + 'procurement.procurement_order_action_exceptions') + action = action.read()[0] + # choose the view_mode accordingly + procurement_ids = procurements.ids + if len(procurement_ids) != 1: + action['domain'] = "[('id', 'in', " + \ + str(procurement_ids) + ")]" + elif len(procurements) == 1: + res = self.env.ref('procurement.procurement_form_view', + False) + action['views'] = [(res and res.id or False, 'form')] + action['res_id'] = procurement_ids[0] + return action + + @api.multi + def action_create_picking(self): + procurements = self._create_picking() + groups = [] + for proc in procurements: + if proc.group_id: + groups.append(proc.group_id.id) + if len(groups): + pickings =self.env['stock.picking'].search( + [('group_id', 'in', groups)]) + + action = self._get_action(pickings, procurements) + return action + + @api.multi + def action_cancel(self): + return {'type': 'ir.actions.act_window_close'} + + +class RmaMakePickingItem(models.TransientModel): + _name = "rma_make_picking.wizard.item" + _description = "Items to receive" + + wiz_id = fields.Many2one( + 'rma_make_picking.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', + readonly=True) + product_id = fields.Many2one('product.product', string='Product', + readonly=True) + product_qty = fields.Float( + related='line_id.product_qty', + string='Quantity Ordered', copy=False, + digits=dp.get_precision('Product Unit of Measure'), readonly=True) + qty_to_receive = fields.Float( + string='Quantity to Receive', + digits=dp.get_precision('Product Unit of Measure')) + qty_to_deliver = fields.Float( + string='Quantity To Deliver', + digits=dp.get_precision('Product Unit of Measure')) + uom_id = fields.Many2one('product.uom', string='Unit of Measure', + readonly=True) diff --git a/rma/wizards/rma_make_picking_view.xml b/rma/wizards/rma_make_picking_view.xml new file mode 100644 index 00000000..da05ac71 --- /dev/null +++ b/rma/wizards/rma_make_picking_view.xml @@ -0,0 +1,139 @@ + + + + + rma_picking + rma_make_picking.wizard + +
+ + + + + + + + + + +
+
+ +
+
+ + + Create Delivery + rma_make_picking.wizard + +
+ + + + + + + + + + +
+
+ +
+
+ + + Create Incoming Shipment + ir.actions.act_window + rma_make_picking.wizard + rma.order.line + form + form + new + + + {'picking_type': 'incoming'} + + + + Create Delivery + ir.actions.act_window + rma_make_picking.wizard + rma.order.line + form + form + new + + + {'picking_type': 'outgoing'} + + + + rma.order.line.form + rma.order.line + + +
+
+
+
+ + + rma.order.line.supplier.form + rma.order.line + + +
+
+
+
+
+
+ + + + Create Incoming Shipment + client_action_multi + + action + rma.order.line + + + + + Create Delivery + client_action_multi + + action + rma.order.line + + +
diff --git a/rma/wizards/rma_order_line_make_supplier_rma.py b/rma/wizards/rma_order_line_make_supplier_rma.py new file mode 100644 index 00000000..79137bd5 --- /dev/null +++ b/rma/wizards/rma_order_line_make_supplier_rma.py @@ -0,0 +1,148 @@ +# -*- 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 openerp.addons.decimal_precision as dp +from openerp import _, api, exceptions, fields, models + + +class RmaLineMakeSupplierRma(models.TransientModel): + _name = "rma.order.line.make.supplier.rma" + _description = "RMA Line Make Supplier RMA" + + partner_id = fields.Many2one('res.partner', string='Supplier', + required=False, + domain=[('supplier', '=', True)]) + item_ids = fields.One2many( + 'rma.order.line.make.supplier.rma.item', + 'wiz_id', string='Items') + supplier_rma_id = fields.Many2one('rma.order', + string='Supplier RMA Order', + required=False, + domain=[('state', '=', 'draft')]) + + @api.model + def _prepare_item(self, line): + return { + 'line_id': line.id, + 'product_id': line.product_id.id, + 'name': line.name, + 'product_qty': line.qty_to_supplier_rma, + 'uom_id': line.uom_id.id, + } + + @api.model + def default_get(self, fields): + res = super(RmaLineMakeSupplierRma, 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)]) + suppliers = lines.mapped('supplier_address_id') + if len(suppliers) == 1: + res['partner_id'] = suppliers.id + else: + raise exceptions.Warning( + _('Only RMA lines from the same supplier address can be ' + 'processed at the same time')) + res['item_ids'] = items + return res + + @api.model + def _prepare_supplier_rma(self, company): + if not self.partner_id: + raise exceptions.Warning( + _('Enter a supplier.')) + return { + 'partner_id': self.partner_id.id, + 'delivery_address_id': self.partner_id.id, + 'type': 'supplier', + 'company_id': company.id, + } + + @api.model + def _prepare_supplier_rma_line(self, rma, item): + operation = self.env['rma.operation'].search( + [('type', '=', 'supplier')], limit=1) + return { + 'origin': item.line_id.rma_id.name, + 'delivery_address_id': + item.line_id.delivery_address_id.id, + 'product_id': item.line_id.product_id.id, + 'customer_rma_id': item.line_id.id, + 'product_qty': item.product_qty, + 'rma_id': rma.id, + 'operation_id': operation.id, + 'receipt_policy': operation.receipt_policy, + 'delivery_policy': operation.delivery_policy, + 'in_warehouse_id': operation.in_warehouse_id.id, + 'out_warehouse_id': operation.out_warehouse_id.id, + 'location_id': operation.location_id.id, + 'supplier_to_customer': operation.supplier_to_customer, + 'in_route_id': operation.in_route_id.id, + 'out_route_id': operation.out_route_id.id, + } + + @api.multi + def make_supplier_rma(self): + res = [] + rma_obj = self.env['rma.order'] + rma_line_obj = self.env['rma.order.line'] + rma = 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.supplier_rma_id: + rma = self.supplier_rma_id + if not rma: + rma_data = self._prepare_supplier_rma(line.company_id) + rma = rma_obj.create(rma_data) + + rma_line_data = self._prepare_supplier_rma_line(rma, item) + rma_line_obj.create(rma_line_data) + res.append(rma.id) + + return { + 'domain': "[('id','in', ["+','.join(map(str, res))+"])]", + 'name': _('Supplier RMA'), + 'view_type': 'form', + 'view_mode': 'tree,form', + 'res_model': 'rma.order', + 'view_id': False, + 'context': {'supplier': 1}, + 'type': 'ir.actions.act_window' + } + + +class RmaLineMakeRmaOrderItem(models.TransientModel): + _name = "rma.order.line.make.supplier.rma.item" + _description = "RMA Line Make Supplier RMA Item" + + wiz_id = fields.Many2one( + 'rma.order.line.make.supplier.rma', + 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', + related='line_id.product_id', readony=True) + name = fields.Char(related='line_id.name', readonly=True) + uom_id = fields.Many2one('product.uom', string='UoM', readonly=True) + product_qty = fields.Float(string='Quantity to sell', + digits=dp.get_precision('Product UoS')) diff --git a/rma/wizards/rma_order_line_make_supplier_rma_view.xml b/rma/wizards/rma_order_line_make_supplier_rma_view.xml new file mode 100644 index 00000000..85609494 --- /dev/null +++ b/rma/wizards/rma_order_line_make_supplier_rma_view.xml @@ -0,0 +1,89 @@ + + + + + + RMA Line Make Supplier RMA + rma.order.line.make.supplier.rma + form + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + diff --git a/rma/wizards/stock_config_settings.py b/rma/wizards/stock_config_settings.py new file mode 100644 index 00000000..ab19c4c4 --- /dev/null +++ b/rma/wizards/stock_config_settings.py @@ -0,0 +1,17 @@ +# -*- 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 + + +class StockConfigSettings(models.Model): + _inherit = 'stock.config.settings' + + group_rma_delivery_address = fields.Selection([ + (0, "Invoicing and shipping addresses are always the same " + "(Example: services companies)"), + (1, 'Display 3 fields on rma: partner, invoice address, delivery ' + 'address') + ], "Addresses", + implied_group='rma.group_rma_delivery_invoice_address') diff --git a/rma/wizards/stock_config_settings.xml b/rma/wizards/stock_config_settings.xml new file mode 100644 index 00000000..2bcb851b --- /dev/null +++ b/rma/wizards/stock_config_settings.xml @@ -0,0 +1,17 @@ + + + + + stock.config.settings.rma + stock.config.settings + + + + + + + + + + +