diff --git a/rma/__init__.py b/rma/__init__.py new file mode 100644 index 00000000..9e5827f9 --- /dev/null +++ b/rma/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import controllers +from . import models diff --git a/rma/__manifest__.py b/rma/__manifest__.py new file mode 100644 index 00000000..99ea96da --- /dev/null +++ b/rma/__manifest__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# © 2018 Hibou Corp. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Hibou RMAs', + 'version': '10.0.1.0.0', + 'category': 'Warehouse', + 'author': "Hibou Corp.", + 'license': 'AGPL-3', + 'website': 'https://hibou.io/', + 'depends': [ + 'stock', + 'delivery', + ], + 'data': [ + 'data/ir_sequence_data.xml', + 'security/ir.model.access.csv', + 'views/rma_views.xml', + 'views/stock_picking_views.xml', + ], + 'demo': [ + 'data/rma_demo.xml', + ], + 'installable': True, + 'application': True, + } diff --git a/rma/controllers/__init__.py b/rma/controllers/__init__.py new file mode 100644 index 00000000..a84d81a7 --- /dev/null +++ b/rma/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +import main diff --git a/rma/controllers/main.py b/rma/controllers/main.py new file mode 100644 index 00000000..16d3e041 --- /dev/null +++ b/rma/controllers/main.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +from odoo import http, exceptions +from base64 import b64decode +import hmac +from hashlib import sha256 +from datetime import datetime +from time import mktime + + +def create_hmac(secret, a_attchment_id, e_expires): + return hmac.new(secret, str(str(a_attchment_id) + str(e_expires)), sha256).hexdigest() + + +def check_hmac(secret, hash_, a_attachment_id, e_expires): + myh = hmac.new(secret, str(str(a_attachment_id) + str(e_expires)), sha256) + return hmac.compare_digest(str(hash_), myh.hexdigest()) + + +class RMAController(http.Controller): + + @http.route(['/rma_label'], type='http', auth='public', website=True) + def index(self, *args, **request): + a_attachment_id = request.get('a') + e_expires = request.get('e') + hash = request.get('h') + + if not all([a_attachment_id, e_expires, hash]): + return http.Response('Invalid Request', status=400) + + now = datetime.utcnow() + now = int(mktime(now.timetuple())) + + config = http.request.env['ir.config_parameter'].sudo() + secret = str(config.search([('key', '=', 'database.secret')], limit=1).value) + + if not check_hmac(secret, hash, a_attachment_id, e_expires): + return http.Response('Invalid Request', status=400) + + if now > int(e_expires): + return http.Response('Expired', status=404) + + attachment = http.request.env['ir.attachment'].sudo().search([('id', '=', int(a_attachment_id))], limit=1) + if attachment: + data = attachment.datas + filename = attachment.name + mimetype = attachment.mimetype + return http.request.make_response(b64decode(data), [ + ('Content-Type', mimetype), + ('Content-Disposition', 'attachment; filename="' + filename + '"')]) + return http.Response('Invalid Attachment', status=404) diff --git a/rma/data/ir_sequence_data.xml b/rma/data/ir_sequence_data.xml new file mode 100644 index 00000000..b021bdee --- /dev/null +++ b/rma/data/ir_sequence_data.xml @@ -0,0 +1,14 @@ + + + + + + RMA + rma.rma + RMA + 3 + + + + + diff --git a/rma/data/rma_demo.xml b/rma/data/rma_demo.xml new file mode 100644 index 00000000..5ec8bc2f --- /dev/null +++ b/rma/data/rma_demo.xml @@ -0,0 +1,12 @@ + + + + Missing Item + + + + + + make_to_stock + + \ No newline at end of file diff --git a/rma/models/__init__.py b/rma/models/__init__.py new file mode 100644 index 00000000..309c58ca --- /dev/null +++ b/rma/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import rma +from . import stock_picking diff --git a/rma/models/rma.py b/rma/models/rma.py new file mode 100644 index 00000000..39370714 --- /dev/null +++ b/rma/models/rma.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError +from ..controllers.main import create_hmac +from datetime import timedelta, datetime +from time import mktime + + +class RMATemplate(models.Model): + _name = 'rma.template' + + name = fields.Char(string='Name') + usage = fields.Selection([], string='Applies To') + description = fields.Html(string='Internal Instructions') + customer_description = fields.Html(string='Customer Instructions') + valid_days = fields.Integer(string='Expire in Days') + + create_in_picking = fields.Boolean(string='Create Inbound Picking') + create_out_picking = fields.Boolean(string='Create Outbound Picking') + + in_type_id = fields.Many2one('stock.picking.type', string='Inbound Picking Type') + out_type_id = fields.Many2one('stock.picking.type', string='Outbound Picking Type') + + in_location_id = fields.Many2one('stock.location', string='Inbound Source Location') + in_location_dest_id = fields.Many2one('stock.location', string='Inbound Destination Location') + in_carrier_id = fields.Many2one('delivery.carrier', string='Inbound Carrier') + in_require_return = fields.Boolean(string='Inbound Require return of picking') + in_procure_method = fields.Selection([ + ('make_to_stock', 'Take from Stock'), + ('make_to_order', 'Apply Procurements') + ], string="Inbound Procurement Method", default='make_to_stock') + in_to_refund_so = fields.Boolean(string='Inbound mark refund SO') + + out_location_id = fields.Many2one('stock.location', string='Outbound Source Location') + out_location_dest_id = fields.Many2one('stock.location', string='Outbound Destination Location') + out_carrier_id = fields.Many2one('delivery.carrier', string='Outbound Carrier') + out_require_return = fields.Boolean(string='Outbound Require picking to duplicate') + out_procure_method = fields.Selection([ + ('make_to_stock', 'Take from Stock'), + ('make_to_order', 'Apply Procurements') + ], string="Outbound Procurement Method", default='make_to_stock') + + def _values_for_in_picking(self, rma): + return { + 'origin': rma.name, + 'partner_id': rma.partner_shipping_id.id, + 'picking_type_id': self.in_type_id.id, + 'location_id': self.in_location_id.id, + 'location_dest_id': self.in_location_dest_id.id, + 'carrier_id': self.in_carrier_id.id if self.in_carrier_id else False, + 'move_lines': [(0, None, { + 'name': rma.name + ' IN: ' + l.product_id.name_get()[0][1], + 'product_id': l.product_id.id, + 'product_uom_qty': l.product_uom_qty, + 'product_uom': l.product_uom_id.id, + 'procure_method': self.in_procure_method, + }) for l in rma.lines], + } + + def _values_for_out_picking(self, rma): + return { + 'origin': rma.name, + 'partner_id': rma.partner_shipping_id.id, + 'picking_type_id': self.out_type_id.id, + 'location_id': self.out_location_id.id, + 'location_dest_id': self.out_location_dest_id.id, + 'carrier_id': self.out_carrier_id.id if self.out_carrier_id else False, + 'move_lines': [(0, None, { + 'name': rma.name + ' OUT: ' + l.product_id.name_get()[0][1], + 'product_id': l.product_id.id, + 'product_uom_qty': l.product_uom_qty, + 'product_uom': l.product_uom_id.id, + 'procure_method': self.out_procure_method, + }) for l in rma.lines], + } + + +class RMATag(models.Model): + _name = "rma.tag" + _description = "RMA Tag" + + name = fields.Char('Tag Name', required=True) + color = fields.Integer('Color Index') + + _sql_constraints = [ + ('name_uniq', 'unique (name)', "Tag name already exists !"), + ] + + +class RMA(models.Model): + _name = 'rma.rma' + _inherit = ['mail.thread'] + _description = 'RMA' + _order = 'id desc' + + name = fields.Char(string='Number', copy=False) + state = fields.Selection([ + ('draft', 'New'), + ('confirmed', 'Confirmed'), + ('done', 'Done'), + ('cancel', 'Cancelled'), + ], string='State', default='draft', copy=False) + company_id = fields.Many2one('res.company', 'Company') + template_id = fields.Many2one('rma.template', string='Type', required=True) + partner_id = fields.Many2one('res.partner', string='Partner') + partner_shipping_id = fields.Many2one('res.partner', string='Shipping') + lines = fields.One2many('rma.line', 'rma_id', string='Lines') + tag_ids = fields.Many2many('rma.tag', 'rma_tags_rel', 'rma_id', 'tag_id', string='Tags') + description = fields.Html(string='Internal Instructions', related='template_id.description') + customer_description = fields.Html(string='Customer Instructions', related='template_id.customer_description') + template_usage = fields.Selection(string='Template Usage', related='template_id.usage') + validity_date = fields.Datetime(string='Expiration Date') + + in_picking_id = fields.Many2one('stock.picking', string='Inbound Picking', copy=False) + out_picking_id = fields.Many2one('stock.picking', string='Outbound Picking', copy=False) + + in_picking_state = fields.Selection(string='In Picking State', related='in_picking_id.state') + out_picking_state = fields.Selection(string='Out Picking State', related='out_picking_id.state') + + in_picking_carrier_id = fields.Many2one('delivery.carrier', related='in_picking_id.carrier_id') + out_picking_carrier_id = fields.Many2one('delivery.carrier', related='out_picking_id.carrier_id') + + in_carrier_tracking_ref = fields.Char(related='in_picking_id.carrier_tracking_ref') + in_label_url = fields.Char(compute='_compute_in_label_url') + out_carrier_tracking_ref = fields.Char(related='out_picking_id.carrier_tracking_ref') + + + @api.onchange('template_usage') + @api.multi + def _onchange_template_usage(self): + now = datetime.now() + for rma in self.filtered(lambda r: r.template_id.valid_days): + rma.validity_date = now + timedelta(days=rma.template_id.valid_days) + + + @api.onchange('in_carrier_tracking_ref', 'validity_date') + @api.multi + def _compute_in_label_url(self): + config = self.env['ir.config_parameter'].sudo() + secret = config.search([('key', '=', 'database.secret')], limit=1) + secret = str(secret.value) if secret else '' + base_url = config.search([('key', '=', 'web.base.url')], limit=1) + base_url = str(base_url.value) if base_url else '' + for rma in self: + if not rma.in_picking_id: + rma.in_label_url = '' + continue + if rma.validity_date: + e_expires = int(mktime(fields.Datetime.from_string(rma.validity_date).timetuple())) + else: + year = datetime.now() + timedelta(days=365) + e_expires = int(mktime(year.timetuple())) + attachment = self.env['ir.attachment'].search([ + ('res_model', '=', 'stock.picking'), + ('res_id', '=', rma.in_picking_id.id), + ('name', 'like', 'Label%')], limit=1) + if not attachment: + rma.in_label_url = '' + continue + rma.in_label_url = base_url + '/rma_label?a=' + \ + str(attachment.id) + '&e=' + str(e_expires) + \ + '&h=' + create_hmac(secret, attachment.id, e_expires) + + @api.model + def create(self, vals): + if vals.get('name', _('New')) == _('New'): + if 'company_id' in vals: + vals['name'] = self.env['ir.sequence'].with_context(force_company=vals['company_id']).next_by_code('rma.rma') or _('New') + else: + vals['name'] = self.env['ir.sequence'].next_by_code('rma.rma') or _('New') + + result = super(RMA, self).create(vals) + return result + + @api.multi + def action_confirm(self): + for rma in self: + in_picking_id = False + out_picking_id = False + if any((not rma.template_id, not rma.lines, not rma.partner_id, not rma.partner_shipping_id)): + raise UserError(_('You can only confirm RMAs with lines, and partner information.')) + if rma.template_id.create_in_picking: + in_picking_id = rma._create_in_picking() + if in_picking_id: + in_picking_id.action_confirm() + in_picking_id.action_assign() + if rma.template_id.create_out_picking: + out_picking_id = rma._create_out_picking() + if out_picking_id: + out_picking_id.action_confirm() + out_picking_id.action_assign() + rma.write({'state': 'confirmed', + 'in_picking_id': in_picking_id.id if in_picking_id else False, + 'out_picking_id': out_picking_id.id if out_picking_id else False}) + + @api.multi + def action_done(self): + for rma in self: + if rma.in_picking_id and rma.in_picking_id.state not in ('done', 'cancel'): + raise UserError(_('Inbound picking not complete or cancelled.')) + if rma.out_picking_id and rma.out_picking_id.state not in ('done', 'cancel'): + raise UserError(_('Outbound picking not complete or cancelled.')) + self.write({'state': 'done'}) + + @api.multi + def action_cancel(self): + for rma in self: + rma.in_picking_id.action_cancel() + rma.out_picking_id.action_cancel() + self.write({'state': 'cancel'}) + + @api.multi + def action_draft(self): + self.filtered(lambda l: l.state == 'cancel').write({ + 'state': 'draft', 'in_picking_id': False, 'out_picking_id': False}) + + @api.multi + def _create_in_picking(self): + if self.template_usage and hasattr(self, '_create_in_picking_' + self.template_usage): + return getattr(self, '_create_in_picking_' + self.template_usage)() + values = self.template_id._values_for_in_picking(self) + return self.env['stock.picking'].sudo().create(values) + + def _create_out_picking(self): + if self.template_usage and hasattr(self, '_create_out_picking_' + self.template_usage): + return getattr(self, '_create_out_picking_' + self.template_usage)() + values = self.template_id._values_for_out_picking(self) + return self.env['stock.picking'].sudo().create(values) + + def _find_candidate_return_picking(self, product_ids, pickings, location_id): + done_pickings = pickings.filtered(lambda p: p.state == 'done' and p.location_dest_id.id == location_id) + for p in done_pickings: + p_product_ids = p.move_lines.filtered(lambda l: l.state == 'done').mapped('product_id.id') + if set(product_ids) & set(p_product_ids) == set(product_ids): + return p + return None + + @api.multi + def action_in_picking_send_to_shipper(self): + for rma in self: + if rma.in_picking_id and rma.in_picking_carrier_id: + rma.in_picking_id.send_to_shipper() + + @api.multi + def unlink(self): + for rma in self: + if rma.state not in ('draft'): + raise UserError(_('You can not delete a non-draft RMA.')) + return super(RMA, self).unlink() + + +class RMALine(models.Model): + _name = 'rma.line' + + rma_id = fields.Many2one('rma.rma', string='RMA') + product_id = fields.Many2one('product.product', 'Product') + product_uom_id = fields.Many2one('product.uom', 'UOM') + product_uom_qty = fields.Float(string='QTY') + rma_template_usage = fields.Selection(related='rma_id.template_usage') + + @api.onchange('product_id') + @api.multi + def _onchange_product_id(self): + for line in self: + line.product_uom_id = line.product_id.uom_id diff --git a/rma/models/stock_picking.py b/rma/models/stock_picking.py new file mode 100644 index 00000000..93c5403b --- /dev/null +++ b/rma/models/stock_picking.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from odoo import api, models, _ +from odoo.exceptions import UserError + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + @api.multi + def send_to_shipper(self): + if self.filtered(lambda p: p.carrier_tracking_ref): + raise UserError(_('Unable to send to shipper with existing tracking numbers.')) + return super(StockPicking, self).send_to_shipper() diff --git a/rma/security/ir.model.access.csv b/rma/security/ir.model.access.csv new file mode 100644 index 00000000..0b27a8cf --- /dev/null +++ b/rma/security/ir.model.access.csv @@ -0,0 +1,7 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"manage_rma stock","manage rma","model_rma_rma","stock.group_stock_user",1,1,1,1 +"manage_rma_line stock","manage rma line","model_rma_line","stock.group_stock_user",1,1,1,1 +"manage_rma_template stock","manage rma template","model_rma_template","stock.group_stock_manager",1,1,1,1 +"manage_rma_tag stock","manage rma tag","model_rma_tag","stock.group_stock_manager",1,1,1,1 +"access_rma_template stock","access rma template","model_rma_template","stock.group_stock_user",1,1,0,0 +"access_rma_tag stock","access rma tag","model_rma_tag","stock.group_stock_user",1,0,0,0 \ No newline at end of file diff --git a/rma/tests/__init__.py b/rma/tests/__init__.py new file mode 100644 index 00000000..eee28d2a --- /dev/null +++ b/rma/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import test_rma diff --git a/rma/tests/test_rma.py b/rma/tests/test_rma.py new file mode 100644 index 00000000..a64fffca --- /dev/null +++ b/rma/tests/test_rma.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +from odoo.tests import common +from odoo.exceptions import UserError + + +class TestRMA(common.TransactionCase): + def setUp(self): + super(TestRMA, self).setUp() + self.product1 = self.env.ref('product.product_product_24') + self.template1 = self.env.ref('rma.template_missing_item') + self.partner1 = self.env.ref('base.res_partner_2') + + def test_00_basic_rma(self): + rma = self.env['rma.rma'].create({ + 'template_id': self.template1.id, + 'partner_id': self.partner1.id, + 'partner_shipping_id': self.partner1.id, + }) + self.assertEqual(rma.state, 'draft') + rma_line = self.env['rma.line'].create({ + 'rma_id': rma.id, + 'product_id': self.product1.id, + 'product_uom_id': self.product1.uom_id.id, + 'product_uom_qty': 2.0, + }) + rma.action_confirm() + # Should have made pickings + self.assertEqual(rma.state, 'confirmed') + # No inbound picking + self.assertFalse(rma.in_picking_id) + # Good outbound picking + self.assertTrue(rma.out_picking_id) + self.assertEqual(rma_line.product_id, rma.out_picking_id.move_lines.product_id) + self.assertEqual(rma_line.product_uom_qty, rma.out_picking_id.move_lines.product_uom_qty) + + with self.assertRaises(UserError): + rma.action_done() + + rma.out_picking_id.action_done() + rma.action_done() + self.assertEqual(rma.state, 'done') + + def test_10_rma_cancel(self): + rma = self.env['rma.rma'].create({ + 'template_id': self.template1.id, + 'partner_id': self.partner1.id, + 'partner_shipping_id': self.partner1.id, + }) + self.assertEqual(rma.state, 'draft') + rma_line = self.env['rma.line'].create({ + 'rma_id': rma.id, + 'product_id': self.product1.id, + 'product_uom_id': self.product1.uom_id.id, + 'product_uom_qty': 2.0, + }) + rma.action_confirm() + # Good outbound picking + self.assertEqual(rma.out_picking_id.move_lines.state, 'assigned') + rma.action_cancel() + self.assertEqual(rma.out_picking_id.move_lines.state, 'cancel') diff --git a/rma/views/rma_views.xml b/rma/views/rma_views.xml new file mode 100644 index 00000000..c20401e8 --- /dev/null +++ b/rma/views/rma_views.xml @@ -0,0 +1,263 @@ + + + + rma.rma.form + rma.rma + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Inbound Picking: + + + + + + + + + + + + + + + + + + + + + Outbound Picking: + + + + + + + + + + + + + + + + + + + + + rma.rma.tree + rma.rma + + + + + + + + + + + + + + rma.rma.tree + rma.rma + + + + + + + + + + + + + + + + + + + rma.template.form + rma.template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rma.template.tree + rma.template + + + + + + + + + + + + + RMA + rma.rma + form + tree,form + + + + + + + + + RMA Tag + rma.tag + form + tree,form + + + + RMA Templates + rma.template + form + tree,form + + + + + + + + + diff --git a/rma/views/stock_picking_views.xml b/rma/views/stock_picking_views.xml new file mode 100644 index 00000000..4fdea8e0 --- /dev/null +++ b/rma/views/stock_picking_views.xml @@ -0,0 +1,13 @@ + + + + delivery.stock.picking_withcarrier.form.view + stock.picking + + + + {'invisible':['|','|','|',('carrier_tracking_ref','!=',False),('delivery_type','in', ['fixed', 'base_on_rule']),('delivery_type','=',False)]} + + + + diff --git a/rma_sale/__init__.py b/rma_sale/__init__.py new file mode 100644 index 00000000..408a6001 --- /dev/null +++ b/rma_sale/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import models +from . import wizard diff --git a/rma_sale/__manifest__.py b/rma_sale/__manifest__.py new file mode 100644 index 00000000..b2582c51 --- /dev/null +++ b/rma_sale/__manifest__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# © 2018 Hibou Corp. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Hibou RMAs for Sale Orders', + 'version': '10.0.1.0.0', + 'category': 'Sale', + 'author': "Hibou Corp.", + 'license': 'AGPL-3', + 'website': 'https://hibou.io/', + 'depends': [ + 'rma', + 'sale', + 'sales_team', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/rma_views.xml', + 'wizard/rma_lines_views.xml', + ], + 'installable': True, + 'application': False, + } diff --git a/rma_sale/models/__init__.py b/rma_sale/models/__init__.py new file mode 100644 index 00000000..d88c3371 --- /dev/null +++ b/rma_sale/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import rma diff --git a/rma_sale/models/rma.py b/rma_sale/models/rma.py new file mode 100644 index 00000000..a3d4e549 --- /dev/null +++ b/rma_sale/models/rma.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class RMATemplate(models.Model): + _inherit = 'rma.template' + + usage = fields.Selection(selection_add=[('sale_order', 'Sale Order')]) + + +class RMA(models.Model): + _inherit = 'rma.rma' + + sale_order_id = fields.Many2one('sale.order', string='Sale Order') + sale_order_rma_count = fields.Integer('Number of RMAs for this Sale Order', compute='_compute_sale_order_rma_count') + company_id = fields.Many2one('res.company', 'Company', + default=lambda self: self.env['res.company']._company_default_get('sale.order')) + + @api.multi + @api.depends('sale_order_id') + def _compute_sale_order_rma_count(self): + for rma in self: + if rma.sale_order_id: + rma_data = self.read_group([('sale_order_id', '=', rma.sale_order_id.id), ('state', '!=', 'cancel')], + ['sale_order_id'], ['sale_order_id']) + if rma_data: + rma.sale_order_rma_count = rma_data[0]['sale_order_id_count'] + else: + rma.sale_order_rma_count = 0.0 + + + @api.multi + def open_sale_order_rmas(self): + return { + 'type': 'ir.actions.act_window', + 'name': _('Sale Order RMAs'), + 'res_model': 'rma.rma', + 'view_mode': 'tree,form', + 'context': {'search_default_sale_order_id': self[0].sale_order_id.id} + } + + @api.onchange('template_usage') + @api.multi + def _onchange_template_usage(self): + res = super(RMA, self)._onchange_template_usage() + for rma in self.filtered(lambda rma: rma.template_usage != 'sale_order'): + rma.sale_order_id = False + return res + + @api.onchange('sale_order_id') + @api.multi + def _onchange_sale_order_id(self): + for rma in self.filtered(lambda rma: rma.sale_order_id): + rma.partner_id = rma.sale_order_id.partner_id + rma.partner_shipping_id = rma.sale_order_id.partner_shipping_id + + + @api.multi + def action_add_so_lines(self): + make_line_obj = self.env['rma.sale.make.lines'] + for rma in self: + lines = make_line_obj.create({ + 'rma_id': rma.id, + }) + action = self.env.ref('rma_sale.action_rma_add_lines').read()[0] + action['res_id'] = lines.id + return action + + def _create_in_picking_sale_order(self): + if not self.sale_order_id: + raise UserError(_('You must have a sale order for this RMA.')) + if not self.template_id.in_require_return: + group_id = self.sale_order_id.procurement_group_id.id if self.sale_order_id.procurement_group_id else 0 + sale_id = self.sale_order_id.id if self.sale_order_id else 0 + + values = self.template_id._values_for_in_picking(self) + values.update({'sale_id': sale_id, 'group_id': group_id}) + move_lines = [] + for l1, l2, vals in values['move_lines']: + vals.update({'to_refund_so': self.template_id.in_to_refund_so, 'group_id': group_id}) + move_lines.append((l1, l2, vals)) + values['move_lines'] = move_lines + return self.env['stock.picking'].sudo().create(values) + + lines = self.lines.filtered(lambda l: l.product_uom_qty >= 1) + if not lines: + raise UserError(_('You have no lines with positive quantity.')) + product_ids = lines.mapped('product_id.id') + + old_picking = self._find_candidate_return_picking(product_ids, self.sale_order_id.picking_ids, self.template_id.in_location_id.id) + if not old_picking: + raise UserError('No eligible pickings were found to return (you can only return products from the same initial picking).') + + new_picking = old_picking.copy({ + 'move_lines': [], + 'picking_type_id': self.template_id.in_type_id.id, + 'state': 'draft', + 'origin': old_picking.name + ' ' + self.name, + 'location_id': self.template_id.in_location_id.id, + 'location_dest_id': self.template_id.in_location_dest_id.id, + 'carrier_id': self.template_id.in_carrier_id.id if self.template_id.in_carrier_id else 0, + 'carrier_tracking_ref': False, + 'carrier_price': False + }) + new_picking.message_post_with_view('mail.message_origin_link', + values={'self': new_picking, 'origin': self}, + subtype_id=self.env.ref('mail.mt_note').id) + + for l in lines: + return_move = old_picking.move_lines.filtered(lambda ol: ol.state == 'done' and ol.product_id.id == l.product_id.id)[0] + return_move.copy({ + 'name': self.name + ' IN: ' + l.product_id.name_get()[0][1], + 'product_id': l.product_id.id, + 'product_uom_qty': l.product_uom_qty, + 'picking_id': new_picking.id, + 'state': 'draft', + 'location_id': return_move.location_dest_id.id, + 'location_dest_id': self.template_id.in_location_dest_id.id, + 'picking_type_id': new_picking.picking_type_id.id, + 'warehouse_id': new_picking.picking_type_id.warehouse_id.id, + 'origin_returned_move_id': return_move.id, + 'procure_method': self.template_id.in_procure_method, + 'move_dest_id': False, + 'to_refund_so': self.template_id.in_to_refund_so, + }) + + return new_picking + + def _create_out_picking_sale_order(self): + if not self.sale_order_id: + raise UserError(_('You must have a sale order for this RMA.')) + if not self.template_id.out_require_return: + group_id = self.sale_order_id.procurement_group_id.id if self.sale_order_id.procurement_group_id else 0 + sale_id = self.sale_order_id.id if self.sale_order_id else 0 + + values = self.template_id._values_for_out_picking(self) + values.update({'sale_id': sale_id, 'group_id': group_id}) + move_lines = [] + for l1, l2, vals in values['move_lines']: + vals.update({'group_id': group_id}) + move_lines.append((l1, l2, vals)) + values['move_lines'] = move_lines + return self.env['stock.picking'].sudo().create(values) + + lines = self.lines.filtered(lambda l: l.product_uom_qty >= 1) + if not lines: + raise UserError(_('You have no lines with positive quantity.')) + product_ids = lines.mapped('product_id.id') + + old_picking = self._find_candidate_return_picking(product_ids, self.sale_order_id.picking_ids, self.template_id.out_location_dest_id.id) + if not old_picking: + raise UserError( + 'No eligible pickings were found to duplicate (you can only return products from the same initial picking).') + + new_picking = old_picking.copy({ + 'move_lines': [], + 'picking_type_id': self.template_id.out_type_id.id, + 'state': 'draft', + 'origin': old_picking.name + ' ' + self.name, + 'location_id': self.template_id.out_location_id.id, + 'location_dest_id': self.template_id.out_location_dest_id.id, + 'carrier_id': self.template_id.out_carrier_id.id if self.template_id.out_carrier_id else 0, + 'carrier_tracking_ref': False, + 'carrier_price': False + }) + new_picking.message_post_with_view('mail.message_origin_link', + values={'self': new_picking, 'origin': self}, + subtype_id=self.env.ref('mail.mt_note').id) + + for l in lines: + return_move = old_picking.move_lines.filtered(lambda ol: ol.state == 'done' and ol.product_id.id == l.product_id.id)[0] + return_move.copy({ + 'name': self.name + ' OUT: ' + l.product_id.name_get()[0][1], + 'product_id': l.product_id.id, + 'product_uom_qty': l.product_uom_qty, + 'picking_id': new_picking.id, + 'state': 'draft', + 'location_id': self.template_id.out_location_id.id, + 'location_dest_id': self.template_id.out_location_dest_id.id, + 'picking_type_id': new_picking.picking_type_id.id, + 'warehouse_id': new_picking.picking_type_id.warehouse_id.id, + 'origin_returned_move_id': False, + 'procure_method': self.template_id.out_procure_method, + 'move_dest_id': False, + }) + + return new_picking + + diff --git a/rma_sale/security/ir.model.access.csv b/rma_sale/security/ir.model.access.csv new file mode 100644 index 00000000..ccb878eb --- /dev/null +++ b/rma_sale/security/ir.model.access.csv @@ -0,0 +1,7 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"manage_rma sale","manage rma","rma.model_rma_rma","sales_team.group_sale_salesman",1,1,1,1 +"manage_rma_line sale","manage rma line","rma.model_rma_line","sales_team.group_sale_salesman",1,1,1,1 +"manage_rma_template sale","manage rma template","rma.model_rma_template","sales_team.group_sale_manager",1,1,1,1 +"manage_rma_tag sale","manage rma tag","rma.model_rma_tag","sales_team.group_sale_manager",1,1,1,1 +"access_rma_template sale","access rma template","rma.model_rma_template","sales_team.group_sale_salesman",1,1,0,0 +"access_rma_tag sale","access rma tag","rma.model_rma_tag","sales_team.group_sale_salesman",1,0,0,0 \ No newline at end of file diff --git a/rma_sale/views/rma_views.xml b/rma_sale/views/rma_views.xml new file mode 100644 index 00000000..f40d786b --- /dev/null +++ b/rma_sale/views/rma_views.xml @@ -0,0 +1,63 @@ + + + + rma.rma.form.sale + rma.rma + + + + + + + + + + + + + + + + + rma.rma.tree.sale + rma.rma + + + + + + + + + + rma.rma.tree.sale + rma.rma + + + + + + + + + + + + + diff --git a/rma_sale/wizard/__init__.py b/rma_sale/wizard/__init__.py new file mode 100644 index 00000000..450b15cd --- /dev/null +++ b/rma_sale/wizard/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import rma_lines diff --git a/rma_sale/wizard/rma_lines.py b/rma_sale/wizard/rma_lines.py new file mode 100644 index 00000000..69eb20bd --- /dev/null +++ b/rma_sale/wizard/rma_lines.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models + + +class RMASaleMakeLines(models.TransientModel): + _name = 'rma.sale.make.lines' + _description = 'Add SO Lines' + + rma_id = fields.Many2one('rma.rma', string='RMA') + line_ids = fields.One2many('rma.sale.make.lines.line', 'rma_make_lines_id', string='Lines') + + + @api.model + def create(self, vals): + maker = super(RMASaleMakeLines, self).create(vals) + maker._create_lines() + return maker + + def _line_values(self, so_line): + return { + 'rma_make_lines_id': self.id, + 'product_id': so_line.product_id.id, + 'qty_ordered': so_line.product_uom_qty, + 'qty_delivered': so_line.qty_delivered, + 'qty_invoiced': so_line.qty_invoiced, + 'product_uom_qty': 0.0, + 'product_uom_id': so_line.product_uom.id, + } + + def _create_lines(self): + make_lines_obj = self.env['rma.sale.make.lines.line'] + + if self.rma_id.template_usage == 'sale_order' and self.rma_id.sale_order_id: + for l in self.rma_id.sale_order_id.order_line: + self.line_ids |= make_lines_obj.create(self._line_values(l)) + + @api.multi + def add_lines(self): + rma_line_obj = self.env['rma.line'] + for o in self: + lines = o.line_ids.filtered(lambda l: l.product_uom_qty > 0.0) + for l in lines: + rma_line_obj.create({ + 'rma_id': o.rma_id.id, + 'product_id': l.product_id.id, + 'product_uom_id': l.product_uom_id.id, + 'product_uom_qty': l.product_uom_qty, + }) + + +class RMASOMakeLinesLine(models.TransientModel): + _name = 'rma.sale.make.lines.line' + + rma_make_lines_id = fields.Many2one('rma.make.lines') + product_id = fields.Many2one('product.product', string="Product") + qty_ordered = fields.Float(string='Ordered') + qty_invoiced = fields.Float(string='Invoiced') + qty_delivered = fields.Float(string='Delivered') + product_uom_qty = fields.Float(string='QTY') + product_uom_id = fields.Many2one('product.uom', 'UOM') diff --git a/rma_sale/wizard/rma_lines_views.xml b/rma_sale/wizard/rma_lines_views.xml new file mode 100644 index 00000000..e6486124 --- /dev/null +++ b/rma_sale/wizard/rma_lines_views.xml @@ -0,0 +1,39 @@ + + + + view.rma.add.lines.form + rma.sale.make.lines + form + + + + + + + + + + + + + + + + + + Add RMA Lines + rma.sale.make.lines + form + form + + new + + \ No newline at end of file
+ +