mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Initial commit of rma and rma_sale for Odoo 11.0
Big changes to tests (because of the ways stock has changed) and allow activities to be set on RMAs. Other refactors to reduce code duplication between picking and so returns.
This commit is contained in:
2
rma_sale/__init__.py
Normal file
2
rma_sale/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
24
rma_sale/__manifest__.py
Normal file
24
rma_sale/__manifest__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# © 2018 Hibou Corp.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
'name': 'Hibou RMAs for Sale Orders',
|
||||
'version': '11.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',
|
||||
'data/rma_demo.xml',
|
||||
'views/rma_views.xml',
|
||||
'wizard/rma_lines_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
15
rma_sale/data/rma_demo.xml
Normal file
15
rma_sale/data/rma_demo.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="template_sale_return" model="rma.template">
|
||||
<field name="name">Sale Return</field>
|
||||
<field name="usage">sale_order</field>
|
||||
<field name="valid_days" eval="10"/>
|
||||
<field name="create_in_picking" eval="True"/>
|
||||
<field name="in_type_id" ref="stock.picking_type_in"/>
|
||||
<field name="in_location_id" ref="stock.stock_location_customers"/>
|
||||
<field name="in_location_dest_id" ref="stock.stock_location_stock"/>
|
||||
<field name="in_procure_method">make_to_stock</field>
|
||||
<field name="in_to_refund" eval="True"/>
|
||||
<field name="in_require_return" eval="True"/>
|
||||
</record>
|
||||
</odoo>
|
||||
1
rma_sale/models/__init__.py
Normal file
1
rma_sale/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import rma
|
||||
118
rma_sale/models/rma.py
Normal file
118
rma_sale/models/rma.py
Normal file
@@ -0,0 +1,118 @@
|
||||
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
|
||||
values = self.template_id._values_for_in_picking(self)
|
||||
update = {'sale_id': sale_id, 'group_id': group_id}
|
||||
update_lines = {'group_id': group_id}
|
||||
return self._picking_from_values(values, update, update_lines)
|
||||
|
||||
lines = self.lines.filtered(lambda l: l.product_uom_qty >= 1)
|
||||
if not lines:
|
||||
raise UserError(_('You have no lines with positive quantity.'))
|
||||
product_ids = lines.mapped('product_id.id')
|
||||
|
||||
old_picking = self._find_candidate_return_picking(product_ids, self.sale_order_id.picking_ids, self.template_id.in_location_id.id)
|
||||
if not old_picking:
|
||||
raise UserError('No eligible pickings were found to return (you can only return products from the same initial picking).')
|
||||
|
||||
new_picking = self._new_in_picking(old_picking)
|
||||
self._new_in_moves(old_picking, new_picking, {})
|
||||
return new_picking
|
||||
|
||||
def _create_out_picking_sale_order(self):
|
||||
if not self.sale_order_id:
|
||||
raise UserError(_('You must have a sale order for this RMA.'))
|
||||
if not self.template_id.out_require_return:
|
||||
group_id = self.sale_order_id.procurement_group_id.id if self.sale_order_id.procurement_group_id else 0
|
||||
sale_id = self.sale_order_id
|
||||
values = self.template_id._values_for_out_picking(self)
|
||||
update = {'sale_id': sale_id, 'group_id': group_id}
|
||||
update_lines = {'to_refund_so': self.template_id.in_to_refund_so, 'group_id': group_id}
|
||||
return self._picking_from_values(values, update, update_lines)
|
||||
|
||||
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 = self._new_out_picking(old_picking)
|
||||
self._new_out_moves(old_picking, new_picking, {})
|
||||
return new_picking
|
||||
|
||||
|
||||
7
rma_sale/security/ir.model.access.csv
Normal file
7
rma_sale/security/ir.model.access.csv
Normal file
@@ -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
|
||||
|
1
rma_sale/tests/__init__.py
Normal file
1
rma_sale/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_rma
|
||||
113
rma_sale/tests/test_rma.py
Normal file
113
rma_sale/tests/test_rma.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from odoo.addons.rma.tests.test_rma import TestRMA
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
class TestRMASale(TestRMA):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRMASale, self).setUp()
|
||||
self.template_sale_return = self.env.ref('rma_sale.template_sale_return')
|
||||
|
||||
def test_20_sale_return(self):
|
||||
self.product1.tracking = 'serial'
|
||||
order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner1.id,
|
||||
'partner_invoice_id': self.partner1.id,
|
||||
'partner_shipping_id': self.partner1.id,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.product1.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom': self.product1.uom_id.id,
|
||||
'price_unit': 10.0,
|
||||
})]
|
||||
})
|
||||
order.action_confirm()
|
||||
self.assertTrue(order.state in ('sale', 'done'))
|
||||
self.assertEqual(len(order.picking_ids), 1, 'Tests only run with single stage delivery.')
|
||||
|
||||
# Try to RMA item not delivered yet
|
||||
rma = self.env['rma.rma'].create({
|
||||
'template_id': self.template_sale_return.id,
|
||||
'partner_id': self.partner1.id,
|
||||
'partner_shipping_id': self.partner1.id,
|
||||
'sale_order_id': order.id,
|
||||
})
|
||||
self.assertEqual(rma.state, 'draft')
|
||||
wizard = self.env['rma.sale.make.lines'].create({
|
||||
'rma_id': rma.id,
|
||||
})
|
||||
self.assertEqual(wizard.line_ids.qty_delivered, 0.0)
|
||||
wizard.line_ids.product_uom_qty = 1.0
|
||||
wizard.add_lines()
|
||||
self.assertEqual(len(rma.lines), 1)
|
||||
with self.assertRaises(UserError):
|
||||
rma.action_confirm()
|
||||
|
||||
order.picking_ids.force_assign()
|
||||
pack_opt = order.picking_ids.move_line_ids[0]
|
||||
lot = self.env['stock.production.lot'].create({
|
||||
'product_id': self.product1.id,
|
||||
'name': 'X100',
|
||||
'product_uom_id': self.product1.uom_id.id,
|
||||
})
|
||||
pack_opt.qty_done = 1.0
|
||||
pack_opt.lot_id = lot
|
||||
order.picking_ids.do_transfer()
|
||||
self.assertEqual(order.picking_ids.state, 'done')
|
||||
wizard = self.env['rma.sale.make.lines'].create({
|
||||
'rma_id': rma.id,
|
||||
})
|
||||
self.assertEqual(wizard.line_ids.qty_delivered, 1.0)
|
||||
|
||||
# Confirm RMA
|
||||
rma.action_confirm()
|
||||
self.assertEqual(rma.in_picking_id.state, 'assigned')
|
||||
pack_opt = rma.in_picking_id.move_line_ids[0]
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
rma.action_done()
|
||||
|
||||
pack_opt.lot_id = lot
|
||||
pack_opt.qty_done = 1.0
|
||||
rma.in_picking_id.do_transfer()
|
||||
rma.action_done()
|
||||
|
||||
# Make another RMA for the same sale order
|
||||
rma2 = self.env['rma.rma'].create({
|
||||
'template_id': self.template_sale_return.id,
|
||||
'partner_id': self.partner1.id,
|
||||
'partner_shipping_id': self.partner1.id,
|
||||
'sale_order_id': order.id,
|
||||
})
|
||||
wizard = self.env['rma.sale.make.lines'].create({
|
||||
'rma_id': rma2.id,
|
||||
})
|
||||
# The First completed RMA will have "un-delivered" it for invoicing purposes.
|
||||
self.assertEqual(wizard.line_ids.qty_delivered, 0.0)
|
||||
wizard.line_ids.product_uom_qty = 1.0
|
||||
wizard.add_lines()
|
||||
self.assertEqual(len(rma2.lines), 1)
|
||||
|
||||
rma2.action_confirm()
|
||||
|
||||
# In Odoo 10, this would not have been able to reserve.
|
||||
# In Odoo 11, reservation can still happen, but at least we can't move the same lot twice!
|
||||
# self.assertEqual(rma2.in_picking_id.state, 'confirmed')
|
||||
|
||||
# Requires Lot
|
||||
with self.assertRaises(UserError):
|
||||
rma2.in_picking_id.move_line_ids.write({'qty_done': 1.0})
|
||||
rma2.in_picking_id.do_transfer()
|
||||
|
||||
# Assign existing lot
|
||||
rma2.in_picking_id.move_line_ids.write({
|
||||
'lot_id': lot.id
|
||||
})
|
||||
|
||||
# Existing lot cannot be re-used.
|
||||
with self.assertRaises(ValidationError):
|
||||
rma2.in_picking_id.action_done()
|
||||
|
||||
# RMA cannot be completed because the inbound picking state is confirmed
|
||||
with self.assertRaises(UserError):
|
||||
rma2.action_done()
|
||||
65
rma_sale/views/rma_views.xml
Normal file
65
rma_sale/views/rma_views.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- RMA -->
|
||||
<record id="view_rma_rma_form_sale" model="ir.ui.view">
|
||||
<field name="name">rma.rma.form.sale</field>
|
||||
<field name="model">rma.rma</field>
|
||||
<field name="inherit_id" ref="rma.view_rma_rma_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button class="oe_stat_button" name="open_sale_order_rmas" icon="fa-cubes"
|
||||
type="object" attrs="{'invisible': ['|', ('sale_order_id', '=', False), ('sale_order_rma_count', '<=', 1)]}">
|
||||
<field name="sale_order_rma_count" string="SO RMAs" widget="statinfo" />
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='template_id']" position="after">
|
||||
<field name="sale_order_id" options="{'no_create': True}" attrs="{'invisible': [('template_usage', '!=', 'sale_order')], 'required': [('template_usage', '=', 'sale_order')], 'readonly': [('state', 'in', ('confirmed', 'done', 'cancel'))]}"/>
|
||||
<br/>
|
||||
<button string="Add lines" type="object" name="action_add_so_lines" attrs="{'invisible': ['|', ('sale_order_id', '=', False), ('state', '!=', 'draft')]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_rma_rma_tree_sale" model="ir.ui.view">
|
||||
<field name="name">rma.rma.tree.sale</field>
|
||||
<field name="model">rma.rma</field>
|
||||
<field name="inherit_id" ref="rma.view_rma_rma_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='template_id']" position="after">
|
||||
<field name="sale_order_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_rma_rma_search_sale" model="ir.ui.view">
|
||||
<field name="name">rma.rma.tree.sale</field>
|
||||
<field name="model">rma.rma</field>
|
||||
<field name="inherit_id" ref="rma.view_rma_rma_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='template_id']" position="after">
|
||||
<field name="sale_order_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
action="rma.action_rma_rma"
|
||||
id="menu_action_sales_rma_form"
|
||||
parent="sale.sale_order_menu"
|
||||
sequence="12"
|
||||
/>
|
||||
|
||||
<menuitem
|
||||
action="rma.action_rma_template_form"
|
||||
id="menu_action_sales_rma_template_form"
|
||||
parent="sale.menu_sale_config"
|
||||
sequence="12"
|
||||
/>
|
||||
<menuitem
|
||||
action="rma.action_rma_tag_form"
|
||||
id="menu_action_sales_rma_tag_form"
|
||||
parent="sale.menu_sale_config"
|
||||
sequence="12"
|
||||
/>
|
||||
</odoo>
|
||||
2
rma_sale/wizard/__init__.py
Normal file
2
rma_sale/wizard/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import rma_lines
|
||||
61
rma_sale/wizard/rma_lines.py
Normal file
61
rma_sale/wizard/rma_lines.py
Normal file
@@ -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.sale.make.lines')
|
||||
product_id = fields.Many2one('product.product', string="Product")
|
||||
qty_ordered = fields.Float(string='Ordered')
|
||||
qty_invoiced = fields.Float(string='Invoiced')
|
||||
qty_delivered = fields.Float(string='Delivered')
|
||||
product_uom_qty = fields.Float(string='QTY')
|
||||
product_uom_id = fields.Many2one('product.uom', 'UOM')
|
||||
39
rma_sale/wizard/rma_lines_views.xml
Normal file
39
rma_sale/wizard/rma_lines_views.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_rma_add_lines_form" model="ir.ui.view">
|
||||
<field name="name">view.rma.add.lines.form</field>
|
||||
<field name="model">rma.sale.make.lines</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<field name="line_ids">
|
||||
<tree editable="top">
|
||||
<field name="product_id" readonly="1"/>
|
||||
<field name="qty_ordered" readonly="1"/>
|
||||
<field name="qty_delivered" readonly="1"/>
|
||||
<field name="qty_invoiced" readonly="1"/>
|
||||
<field name="product_uom_qty"/>
|
||||
<field name="product_uom_id" readonly="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
<footer>
|
||||
<button class="oe_highlight"
|
||||
name="add_lines"
|
||||
type="object"
|
||||
string="Add" />
|
||||
<button class="oe_link"
|
||||
special="cancel"
|
||||
string="Cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_rma_add_lines" model="ir.actions.act_window">
|
||||
<field name="name">Add RMA Lines</field>
|
||||
<field name="res_model">rma.sale.make.lines</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_rma_add_lines_form" />
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user