From 698a2a661726da57c0f3864ded78d79e34b0c22a Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 11 Sep 2020 14:22:02 -0700 Subject: [PATCH] [IMP] rma: Release 13.0.1.3.0! Add wizard and invoicing functionality for RTV, chained 'next RMA' etc. --- rma/__manifest__.py | 2 +- rma/demo/rma_demo.xml | 11 ++++ rma/models/rma.py | 90 +++++++++++++++++++++++++++++++ rma/tests/test_rma.py | 62 +++++++++++++++++++++ rma/views/portal_templates.xml | 8 +-- rma/views/rma_views.xml | 5 +- rma/wizard/__init__.py | 1 + rma/wizard/rma_make_rtv.py | 90 +++++++++++++++++++++++++++++++ rma/wizard/rma_make_rtv_views.xml | 41 ++++++++++++++ 9 files changed, 304 insertions(+), 6 deletions(-) create mode 100644 rma/wizard/rma_make_rtv.py create mode 100644 rma/wizard/rma_make_rtv_views.xml diff --git a/rma/__manifest__.py b/rma/__manifest__.py index a1de4a07..ef12b21a 100644 --- a/rma/__manifest__.py +++ b/rma/__manifest__.py @@ -2,7 +2,7 @@ { 'name': 'Hibou RMAs', - 'version': '13.0.1.2.0', + 'version': '13.0.1.3.0', 'category': 'Warehouse', 'author': 'Hibou Corp.', 'license': 'OPL-1', diff --git a/rma/demo/rma_demo.xml b/rma/demo/rma_demo.xml index 17f405e0..94f78e70 100644 --- a/rma/demo/rma_demo.xml +++ b/rma/demo/rma_demo.xml @@ -42,4 +42,15 @@ make_to_stock + + + Return To Vendor + + + + + make_to_stock + + + \ No newline at end of file diff --git a/rma/models/rma.py b/rma/models/rma.py index d2e9847e..cdce249c 100644 --- a/rma/models/rma.py +++ b/rma/models/rma.py @@ -52,6 +52,7 @@ class RMATemplate(models.Model): company_id = fields.Many2one('res.company', 'Company') responsible_user_ids = fields.Many2many('res.users', string='Responsible Users', help='Users that get activities when creating RMA.') + next_rma_template_id = fields.Many2one('rma.template', string='Next RMA Template') def _portal_try_create(self, request_user, res_id, **kw): if self.usage == 'stock_picking': @@ -201,6 +202,7 @@ class RMA(models.Model): ('cancel', 'Cancelled'), ], string='State', default='draft', copy=False) company_id = fields.Many2one('res.company', 'Company') + parent_id = fields.Many2one('rma.rma') template_id = fields.Many2one('rma.template', string='Type', required=True) template_create_in_picking = fields.Boolean(related='template_id.create_in_picking') template_create_out_picking = fields.Boolean(related='template_id.create_out_picking') @@ -215,6 +217,7 @@ class RMA(models.Model): 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') + claim_number = fields.Char(string='Claim Number') invoice_ids = fields.Many2many('account.move', 'rma_invoice_rel', 'rma_id', @@ -363,6 +366,26 @@ class RMA(models.Model): '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}) + def _next_rma_values(self): + return { + 'template_id': self.template_id.next_rma_template_id.id, + # Partners should be set when confirming or using the RTV wizard + # 'partner_id': self.partner_id.id, + # 'partner_shipping_id': self.partner_shipping_id.id, + 'parent_id': self.id, + 'lines': [(0, 0, { + 'product_id': l.product_id.id, + 'product_uom_id': l.product_uom_id.id, + 'product_uom_qty': l.product_uom_qty, + }) for l in self.lines] + } + + def _next_rma(self): + if self.template_id.next_rma_template_id: + # currently we do not want to automatically confirm them + # this is because we want to mass confirm and set picking to one partner/vendor + _ = self.create(self._next_rma_values()) + def action_done(self): for rma in self: if rma.in_picking_id and rma.in_picking_id.state not in ('done', 'cancel'): @@ -370,6 +393,67 @@ class RMA(models.Model): 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'}) + self._done_invoice() + self._next_rma() + + def _done_invoice(self): + for rma in self.filtered(lambda r: r.template_id.invoice_done): + # If you do NOT want to take part in the default invoicing functionality + # then your usage method (e.g. _invoice_values_sale_order) should be + # defined, and return nothing or extend _invoice_values to do the same + usage = rma.template_usage or '' + if hasattr(rma, '_invoice_values_' + usage): + values = getattr(rma, '_invoice_values_' + usage)() + else: + values = rma._invoice_values() + if values: + if hasattr(rma, '_invoice_' + usage): + getattr(rma, '_invoice_' + usage)(values) + else: + rma._invoice(values) + + def _invoice(self, invoice_values): + self.invoice_ids += self.env['account.move'].with_context(default_type=invoice_values['type']).create( + invoice_values) + + def _invoice_values(self): + self.ensure_one() + # special case for vendor return + supplier = self._context.get('rma_supplier') + if supplier is None and self.out_picking_id and self.out_picking_id.location_dest_id.usage == 'supplier': + supplier = True + + fiscal_position_id = self.env['account.fiscal.position'].get_fiscal_position( + self.partner_id.id, delivery_id=self.partner_shipping_id.id) + + invoice_values = { + 'type': 'in_refund' if supplier else 'out_refund', + 'partner_id': self.partner_id.id, + 'fiscal_position_id': fiscal_position_id, + } + + line_commands = [] + for rma_line in self.lines: + product = rma_line.product_id + accounts = product.product_tmpl_id.get_product_accounts() + account = accounts['expense'] if supplier else accounts['income'] + qty = rma_line.product_uom_qty + uom = rma_line.product_uom_id + price = product.standard_price if supplier else product.lst_price + if uom != product.uom_id: + price = product.uom_id._compute_price(price, uom) + line_commands.append((0, 0, { + 'product_id': product.id, + 'product_uom_id': uom.id, + 'name': product.name, + 'price_unit': price, + 'quantity': qty, + 'account_id': account.id, + 'tax_ids': [(6, 0, product.taxes_id.ids)], + })) + if line_commands: + invoice_values['invoice_line_ids'] = line_commands + return invoice_values def action_cancel(self): for rma in self: @@ -382,12 +466,18 @@ class RMA(models.Model): 'state': 'draft', 'in_picking_id': False, 'out_picking_id': False}) def _create_in_picking(self): + if self._context.get('rma_in_picking_id'): + # allow passing/setting by context to allow many RMA's to include the same pickings + return self.env['stock.picking'].browse(self._context.get('rma_in_picking_id')) 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._context.get('rma_out_picking_id'): + # allow passing/setting by context to allow many RMA's to include the same pickings + return self.env['stock.picking'].browse(self._context.get('rma_out_picking_id')) 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) diff --git a/rma/tests/test_rma.py b/rma/tests/test_rma.py index 588cce36..ec70c3ac 100644 --- a/rma/tests/test_rma.py +++ b/rma/tests/test_rma.py @@ -14,8 +14,11 @@ class TestRMA(common.TransactionCase): self.product1 = self.env.ref('product.product_product_24') self.template_missing = self.env.ref('rma.template_missing_item') self.template_return = self.env.ref('rma.template_picking_return') + self.template_rtv = self.env.ref('rma.template_rtv') self.partner1 = self.env.ref('base.res_partner_2') self.user1 = self.env.ref('base.user_demo') + # Additional partner in tests or vendor in Return To Vendor + self.partner2 = self.env.ref('base.res_partner_12') def test_00_basic_rma(self): self.template_missing.responsible_user_ids += self.user1 @@ -244,3 +247,62 @@ class TestRMA(common.TransactionCase): # RMA cannot be completed because the inbound picking state is confirmed with self.assertRaises(UserError): rma2.action_done() + + def test_30_next_rma_rtv(self): + self.template_return.usage = False + self.template_return.in_require_return = False + self.template_return.next_rma_template_id = self.template_rtv + rma = self.env['rma.rma'].create({ + 'template_id': self.template_return.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 outbound picking + self.assertFalse(rma.out_picking_id) + # Good inbound picking + self.assertTrue(rma.in_picking_id) + self.assertEqual(rma_line.product_id, rma.in_picking_id.move_lines.product_id) + self.assertEqual(rma_line.product_uom_qty, rma.in_picking_id.move_lines.product_uom_qty) + + with self.assertRaises(UserError): + rma.action_done() + + rma.in_picking_id.move_lines.quantity_done = 2.0 + rma.in_picking_id.action_done() + rma.action_done() + self.assertEqual(rma.state, 'done') + + # RTV RMA + rma_rtv = self.env['rma.rma'].search([('parent_id', '=', rma.id)]) + self.assertTrue(rma_rtv) + self.assertEqual(rma_rtv.state, 'draft') + + wiz = self.env['rma.make.rtv'].with_context(active_model='rma.rma', active_ids=rma_rtv.ids).create({}) + self.assertTrue(wiz.rma_line_ids) + wiz.partner_id = self.partner2 + wiz.create_batch() + self.assertTrue(rma_rtv.out_picking_id) + self.assertEqual(rma_rtv.out_picking_id.partner_id, self.partner2) + self.assertEqual(rma_rtv.state, 'confirmed') + + # ship and finish + rma_rtv.out_picking_id.move_lines.quantity_done = 2.0 + rma_rtv.out_picking_id.action_done() + rma_rtv.action_done() + self.assertEqual(rma_rtv.state, 'done') + + # ensure invoice and type + rtv_invoice = rma_rtv.invoice_ids + self.assertTrue(rtv_invoice) + self.assertEqual(rtv_invoice.type, 'in_refund') diff --git a/rma/views/portal_templates.xml b/rma/views/portal_templates.xml index abd1728e..0692be42 100644 --- a/rma/views/portal_templates.xml +++ b/rma/views/portal_templates.xml @@ -138,8 +138,8 @@
- Product image + Product Image + Product Image
@@ -237,8 +237,8 @@
- Product image + Product Image + Product Image
diff --git a/rma/views/rma_views.xml b/rma/views/rma_views.xml index f636ba54..42ee959a 100644 --- a/rma/views/rma_views.xml +++ b/rma/views/rma_views.xml @@ -26,6 +26,7 @@
+ @@ -35,6 +36,7 @@
@@ -172,6 +174,7 @@ + diff --git a/rma/wizard/__init__.py b/rma/wizard/__init__.py index 1cbabc08..38eeb3bd 100644 --- a/rma/wizard/__init__.py +++ b/rma/wizard/__init__.py @@ -1,3 +1,4 @@ # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. from . import rma_lines +from . import rma_make_rtv diff --git a/rma/wizard/rma_make_rtv.py b/rma/wizard/rma_make_rtv.py new file mode 100644 index 00000000..728e3905 --- /dev/null +++ b/rma/wizard/rma_make_rtv.py @@ -0,0 +1,90 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class RMAMakeRTV(models.TransientModel): + _name = 'rma.make.rtv' + _description = 'Make RTV Batch' + + partner_id = fields.Many2one('res.partner', string='Vendor') + partner_shipping_id = fields.Many2one('res.partner', string='Shipping Address') + rma_line_ids = fields.One2many('rma.make.rtv.line', 'rma_make_rtv_id', string='Lines') + + @api.model + def default_get(self, fields): + result = super().default_get(fields) + if 'rma_line_ids' in fields and self._context.get('active_model') == 'rma.rma' and self._context.get('active_ids'): + active_ids = self._context.get('active_ids') + rmas = self.env['rma.rma'].browse(active_ids) + result['rma_line_ids'] = [(0, 0, { + 'rma_id': r.id, + 'rma_state': r.state, + 'rma_claim_number': r.claim_number, + }) for r in rmas] + rma_partner = rmas.mapped('partner_id') + if rma_partner: + result['partner_id'] = rma_partner[0].id + return result + + def create_batch(self): + self.ensure_one() + if self.rma_line_ids.filtered(lambda rl: rl.rma_id.state != 'draft'): + raise UserError('All RMAs must be in the draft state.') + rma_partner = self.rma_line_ids.mapped('rma_id.partner_id') + if rma_partner and len(rma_partner) != 1: + raise UserError('All RMAs must be for the same partner.') + elif not rma_partner and not self.partner_id: + raise UserError('Please select a Vendor') + elif not rma_partner: + rma_partner = self.partner_id + rma_partner_shipping = self.partner_shipping_id or rma_partner + # update all RMA's to the currently selected vendor + self.rma_line_ids.mapped('rma_id').write({ + 'partner_id': rma_partner.id, + 'partner_shipping_id': rma_partner_shipping.id, + }) + if len(self.rma_line_ids.mapped('rma_id.template_id')) != 1: + raise UserError('All RMAs must be of the same template.') + + in_values = None + out_values = None + for rma in self.rma_line_ids.mapped('rma_id'): + if rma.template_id.create_in_picking: + if not in_values: + in_values = rma.template_id._values_for_in_picking(rma) + in_values['origin'] = [in_values['origin']] + else: + other_in_values = rma.template_id._values_for_in_picking(rma) + in_values['move_lines'] += other_in_values['move_lines'] + if rma.template_id.create_out_picking: + if not out_values: + out_values = rma.template_id._values_for_out_picking(rma) + out_values['origin'] = [out_values['origin']] + else: + other_out_values = rma.template_id._values_for_out_picking(rma) + out_values['move_lines'] += other_out_values['move_lines'] + in_picking_id = False + out_picking_id = False + if in_values: + in_values['origin'] = ', '.join(in_values['origin']) + in_picking = self.env['stock.picking'].sudo().create(in_values) + in_picking_id = in_picking.id + if out_values: + out_values['origin'] = ', '.join(out_values['origin']) + out_picking = self.env['stock.picking'].sudo().create(out_values) + out_picking_id = out_picking.id + rmas = self.rma_line_ids.mapped('rma_id').with_context(rma_in_picking_id=in_picking_id, rma_out_picking_id=out_picking_id) + # action_confirm known to be multi-aware and makes only one context + rmas.action_confirm() + + +class RMAMakeRTVLine(models.TransientModel): + _name = 'rma.make.rtv.line' + _description = 'Make RTV Batch RMA' + + rma_make_rtv_id = fields.Many2one('rma.make.rtv') + rma_id = fields.Many2one('rma.rma') + rma_state = fields.Selection(related='rma_id.state') + rma_claim_number = fields.Char(related='rma_id.claim_number', readonly=False) diff --git a/rma/wizard/rma_make_rtv_views.xml b/rma/wizard/rma_make_rtv_views.xml new file mode 100644 index 00000000..b7da2d82 --- /dev/null +++ b/rma/wizard/rma_make_rtv_views.xml @@ -0,0 +1,41 @@ + + + + + Return To Vendor + rma.make.rtv + +
+

+ RMAs will be batched to pick simultaneously. +

+ + + + + + + + + + + +
+
+
+
+
+ + + RMA Make RTV + ir.actions.act_window + rma.make.rtv + form + new + + list + + +