[MOV] rma_sale: from Hibou Suite Enterprise for 13.0

This commit is contained in:
Jared Kipe
2020-07-03 09:04:44 -07:00
parent 11ee6a567d
commit 4c941b3661
17 changed files with 860 additions and 0 deletions

4
rma_sale/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import models
from . import wizard

29
rma_sale/__manifest__.py Normal file
View File

@@ -0,0 +1,29 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
{
'name': 'Hibou RMAs for Sale Orders',
'version': '12.0.1.1.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/portal_templates.xml',
'views/product_views.xml',
'views/rma_views.xml',
'views/sale_views.xml',
'wizard/rma_lines_views.xml',
],
'demo': [
'data/rma_demo.xml',
],
'installable': True,
'auto_install': True,
'application': False,
}

View File

@@ -0,0 +1,18 @@
<?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"/>
<field name="so_decrement_order_qty" eval="True"/>
</record>
</odoo>

View File

@@ -0,0 +1,5 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import product
from . import rma
from . import sale

View File

@@ -0,0 +1,16 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import fields, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
rma_sale_validity = fields.Integer(string='RMA Eligible Days (Sale)',
help='Determines the number of days from the time '
'of the sale that the product is eligible to '
'be returned. 0 (default) will allow the product '
'to be returned for an indefinite period of time. '
'A positive number will allow the product to be '
'returned up to that number of days. A negative '
'number prevents the return of the product.')

255
rma_sale/models/rma.py Normal file
View File

@@ -0,0 +1,255 @@
# 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, ValidationError
from datetime import timedelta
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
def _get_protected_fields(self):
res = super(SaleOrderLine, self)._get_protected_fields()
context = self._context or {}
if context.get('rma_done') and 'product_uom_qty' in res:
res.remove('product_uom_qty')
return res
class RMATemplate(models.Model):
_inherit = 'rma.template'
usage = fields.Selection(selection_add=[('sale_order', 'Sale Order')])
so_decrement_order_qty = fields.Boolean(string='SO Decrement Ordered Qty.',
help='When completing the RMA, the Ordered Quantity will be decremented by '
'the RMA qty.')
def _portal_try_create(self, request_user, res_id, **kw):
if self.usage == 'sale_order':
prefix = 'line_'
line_map = {int(key[len(prefix):]): float(kw[key]) for key in kw if key.find(prefix) == 0 and kw[key]}
if line_map:
sale_order = self.env['sale.order'].with_user(request_user).browse(res_id)
if not sale_order.exists():
raise ValidationError('Invalid user for sale order.')
lines = []
sale_order_sudo = sale_order.sudo()
for line_id, qty in line_map.items():
line = sale_order_sudo.order_line.filtered(lambda l: l.id == line_id)
if line:
if not qty:
continue
if qty < 0.0 or line.qty_delivered < qty:
raise ValidationError('Invalid quantity.')
validity = self._rma_sale_line_validity(line)
if not validity:
raise ValidationError('Product is not eligible for return.')
if validity == 'expired':
raise ValidationError('Product is past the return period.')
lines.append((0, 0, {
'product_id': line.product_id.id,
'product_uom_id': line.product_uom.id,
'product_uom_qty': qty,
}))
if not lines:
raise ValidationError('Missing product quantity.')
rma = self.env['rma.rma'].create({
'name': _('New'),
'sale_order_id': sale_order.id,
'template_id': self.id,
'partner_id': sale_order.partner_id.id,
'partner_shipping_id': sale_order.partner_shipping_id.id,
'lines': lines,
})
return rma
return super(RMATemplate, self)._portal_try_create(request_user, res_id, **kw)
def _portal_template(self, res_id=None):
if self.usage == 'sale_order':
return 'rma_sale.portal_new_sale_order'
return super(RMATemplate, self)._portal_template(res_id=res_id)
def _portal_values(self, request_user, res_id=None):
if self.usage == 'sale_order':
sale_orders = None
sale_order = None
if res_id:
sale_order = self.env['sale.order'].with_user(request_user).browse(res_id)
if sale_order:
sale_order = sale_order.sudo()
else:
sale_orders = self.env['sale.order'].with_user(request_user).search([], limit=100)
return {
'rma_template': self,
'rma_sale_orders': sale_orders,
'rma_sale_order': sale_order,
}
return super(RMATemplate, self)._portal_values(request_user, res_id=res_id)
def _rma_sale_line_validity(self, so_line):
validity_days = so_line.product_id.rma_sale_validity
if validity_days < 0:
return ''
elif validity_days > 0:
sale_date = so_line.order_id.date_order
now = fields.Datetime.now()
if sale_date < (now - timedelta(days=validity_days)):
return 'expired'
return 'valid'
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.company)
@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
else:
rma.sale_order_rma_count = 0.0
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')
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')
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
def action_done(self):
res = super(RMA, self).action_done()
res2 = self._so_action_done()
if isinstance(res, dict) and isinstance(res2, dict):
if 'warning' in res and 'warning' in res2:
res['warning'] = '\n'.join([res['warning'], res2['warning']])
return res
if 'warning' in res2:
res['warning'] = res2['warning']
return res
elif isinstance(res2, dict):
return res2
return res
def _so_action_done(self):
warnings = []
for rma in self:
sale_orders = rma.sale_order_id
if rma.template_id.so_decrement_order_qty:
sale_orders = self.env['sale.order'].browse()
for rma_line in rma.lines:
so_lines = rma.sale_order_id.order_line.filtered(lambda l: l.product_id == rma_line.product_id)
qty_remaining = rma_line.product_uom_qty
for sale_line in so_lines:
if qty_remaining == 0:
continue
sale_line_qty = sale_line.product_uom_qty
sale_line_qty = sale_line_qty - qty_remaining
if sale_line_qty < 0:
qty_remaining = abs(sale_line_qty)
sale_line_qty = 0
else:
qty_remaining = 0
sale_line.with_context(rma_done=True).write({'product_uom_qty': sale_line_qty})
sale_orders += sale_line.order_id
if qty_remaining:
warnings.append((rma, rma.sale_order_id, rma_line, qty_remaining))
# Try to invoice if we don't already have an invoice (e.g. from resetting to draft)
if sale_orders and rma.template_id.invoice_done and not rma.invoice_ids:
rma.invoice_ids += rma._sale_invoice_done(sale_orders)
if warnings:
return {'warning': _('Could not reduce all ordered qty:\n %s' % '\n'.join(
['%s %s %s : %s' % (w[0].name, w[1].name, w[2].product_id.display_name, w[3]) for w in warnings]))}
return True
def _sale_invoice_done(self, sale_orders):
original_invoices = sale_orders.mapped('invoice_ids')
try:
wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=sale_orders.ids).create({})
wiz.create_invoices()
except UserError:
pass
return sale_orders.mapped('invoice_ids') - original_invoices
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
values = self.template_id._values_for_in_picking(self)
update = {'sale_id': sale_id, 'group_id': group_id}
update_lines = {'to_refund': self.template_id.in_to_refund, 'group_id': group_id}
return self._picking_from_values(values, update, update_lines)
lines = self.lines.filtered(lambda l: l.product_uom_qty >= 1 and l.product_id.type != 'service')
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.id
values = self.template_id._values_for_out_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 and l.product_id.type != 'service')
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

15
rma_sale/models/sale.py Normal file
View File

@@ -0,0 +1,15 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
rma_count = fields.Integer(string='RMA Count', compute='_compute_rma_count', compute_sudo=True)
rma_ids = fields.One2many('rma.rma', 'sale_order_id', string='RMAs')
@api.depends('rma_ids')
def _compute_rma_count(self):
for so in self:
so.rma_count = len(so.rma_ids)

View 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 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 manage_rma sale manage rma rma.model_rma_rma sales_team.group_sale_salesman 1 1 1 1
3 manage_rma_line sale manage rma line rma.model_rma_line sales_team.group_sale_salesman 1 1 1 1
4 manage_rma_template sale manage rma template rma.model_rma_template sales_team.group_sale_manager 1 1 1 1
5 manage_rma_tag sale manage rma tag rma.model_rma_tag sales_team.group_sale_manager 1 1 1 1
6 access_rma_template sale access rma template rma.model_rma_template sales_team.group_sale_salesman 1 1 0 0
7 access_rma_tag sale access rma tag rma.model_rma_tag sales_team.group_sale_salesman 1 0 0 0

View File

@@ -0,0 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import test_rma

173
rma_sale/tests/test_rma.py Normal file
View File

@@ -0,0 +1,173 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo.addons.rma.tests.test_rma import TestRMA
from odoo.exceptions import UserError, ValidationError
from datetime import timedelta
class TestRMASale(TestRMA):
def setUp(self):
super(TestRMASale, self).setUp()
self.template_sale_return = self.env.ref('rma_sale.template_sale_return')
# Make it possible to "see all sale orders", but not be a manager (as managers can RMA ineligible lines)
self.user1.groups_id += self.env.ref('sales_team.group_sale_salesman_all_leads')
def test_20_sale_return(self):
self.template_sale_return.write({
'usage': 'sale_order',
'invoice_done': True,
})
self.product1.write({
'type': 'product',
'invoice_policy': 'delivery',
'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')
# Do not allow return.
self.product1.rma_sale_validity = -1
wizard = self.env['rma.sale.make.lines'].with_user(self.user1).create({'rma_id': rma.id})
self.assertEqual(wizard.line_ids.qty_delivered, 0.0)
wizard.line_ids.product_uom_qty = 1.0
with self.assertRaises(UserError):
wizard.add_lines()
# Allows returns, but not forever
self.product1.rma_sale_validity = 5
original_date_order = order.date_order
order.write({'date_order': original_date_order - timedelta(days=6)})
wizard = self.env['rma.sale.make.lines'].with_user(self.user1).create({'rma_id': rma.id})
self.assertEqual(wizard.line_ids.qty_delivered, 0.0)
wizard.line_ids.product_uom_qty = 1.0
with self.assertRaises(UserError):
wizard.add_lines()
# Allows returns due to date
order.write({'date_order': original_date_order})
wizard = self.env['rma.sale.make.lines'].with_user(self.user1).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.action_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,
'company_id': self.env.user.company_id.id,
})
pack_opt.qty_done = 1.0
pack_opt.lot_id = lot
order.picking_ids.button_validate()
self.assertEqual(order.picking_ids.state, 'done')
# Invoice order so that the return is invoicable
wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=order.ids).create({})
wiz.create_invoices()
# Odoo 13 Not flushing the order here will cause delivered_qty to be incorrect later
order.flush()
self.assertTrue(order.invoice_ids, 'Order did not create an invoice.')
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.button_validate()
self.assertEqual(rma.in_picking_id.move_lines.sale_line_id, order.order_line)
self.assertEqual(rma.in_picking_id.state, 'done')
for move in order.order_line.mapped('move_ids'):
# Additional testing like this may not be needed in the future. Was added troubleshooting new 13 ORM
self.assertEqual(move.state, 'done', 'Move not done ' + str(move.name))
self.assertEqual(order.order_line.qty_delivered, 0.0)
rma.action_done()
self.assertEqual(order.order_line.qty_delivered, 0.0)
# Finishing the RMA should have made an invoice
self.assertTrue(rma.invoice_ids, 'Finishing RMA did not create an invoice(s).')
# Test Ordered Qty was decremented.
self.assertEqual(order.order_line.product_uom_qty, 0.0)
self.assertEqual(order.order_line.qty_delivered, 0.0)
# 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.button_validate()
self.assertTrue(rma2.in_picking_id.move_line_ids)
self.assertFalse(rma2.in_picking_id.move_line_ids.lot_id.name)
# Assign existing lot
rma2.in_picking_id.move_line_ids.write({
'lot_id': lot.id,
'qty_done': 1.0,
})
# 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()

View File

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="portal_my_rma_rma_inherit" inherit_id="rma.portal_my_rma_rma">
<xpath expr="//div[hasclass('rma-details')]" position="inside">
<div t-if="rma.sale_order_id" class="mb8">
<strong>Sale Order:</strong>
<span t-esc="rma.sale_order_id.name"/>
</div>
</xpath>
</template>
<!-- New -->
<template id="portal_new_sale_order" name="New Sale Order RMA">
<t t-call="portal.portal_layout">
<div id="optional_placeholder"></div>
<div class="container">
<t t-call="rma.portal_rma_error"/>
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-lg-12">
<h4>
<span t-esc="rma_template.name"/>
</h4>
</div>
</div>
</div>
<div class="card-body">
<ul t-if="rma_sale_orders" class="list-group">
<li class="list-group-item" t-foreach="rma_sale_orders" t-as="s">
<a t-attf-href="/my/rma/new/#{rma_template.id}/res/#{s.id}">
<span t-esc="s.name"/> - Order Date: <span t-field="s.date_order"/>
</a>
</li>
</ul>
<p t-if="not rma_sale_orders and not rma_sale_order">No Sale Orders to choose from.</p>
<form t-if="rma_sale_order" method="post" t-attf-action="/my/rma/new/#{rma_template.id}/res/#{rma_sale_order.id}">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="row">
<div class="col-lg-4">
<strong>Product</strong>
</div>
<div class="col-lg-3">
<strong>Description</strong>
</div>
<div class="col-lg-2 text-right">
<strong>Qty. Ordered</strong>
</div>
<div class="col-lg-2 text-right">
<strong>Qty. Delivered</strong>
</div>
<div class="col-lg-1 text-right">
<strong>Qty. to Return</strong>
</div>
</div>
<t t-foreach="rma_sale_order.order_line" t-as="line">
<t t-set="validity" t-value="rma_template._rma_sale_line_validity(line)"/>
<div class="row" t-attf-class="row #{'' if validity == 'valid' else 'text-muted'}">
<div class="col-lg-1 text-center">
<img t-attf-src="/web/image/product.product/#{line.product_id.id}/image_64"
width="64" alt="Product image"></img>
</div>
<div class="col-lg-3">
<span t-esc="line.product_id.name"/>
</div>
<div class="col-lg-3">
<span t-esc="line.name"/>
</div>
<div class="col-lg-2 text-right">
<span t-esc="line.product_uom_qty"/>
</div>
<div class="col-lg-2 text-right">
<span t-esc="line.qty_delivered"/>
</div>
<div class="col-lg-1 text-right" t-if="validity == 'valid'">
<input type="text" t-attf-name="line_#{line.id}" class="form-control"/>
</div>
<div class="col-lg-1 text-left" t-else="">
<span t-if="validity == 'expired'">Expired</span>
<span t-else="">Not Eligible</span>
</div>
</div>
</t>
<input type="submit" class="btn btn-primary mt16 float-right" name="submit"/>
</form>
</div>
</div>
</div>
<div class="oe_structure mb32"/>
</t>
</template>
</odoo>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="product_template_only_form_view_inherit" model="ir.ui.view">
<field name="name">product.template.product.form.inherit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='sales']/group[@name='sale']" position="inside">
<group name="rma_sale" string="RMA Sales">
<field name="rma_sale_validity" string="Eligible Days"/>
</group>
</xpath>
</field>
</record>
<record id="product_normal_form_view_inherit" model="ir.ui.view">
<field name="name">product.product.form.inherit</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_normal_form_view"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='sales']/group[@name='sale']" position="inside">
<group name="rma_sale" string="RMA Sales">
<field name="rma_sale_validity" string="Eligible Days"/>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- RMA Template -->
<record id="view_rma_template_form_sale" model="ir.ui.view">
<field name="name">rma.template.form.sale</field>
<field name="model">rma.template</field>
<field name="inherit_id" ref="rma.view_rma_template_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='usage']" position="after">
<field name="so_decrement_order_qty" string="Decrement Ordered Qty" attrs="{'invisible': [('usage', '!=', 'sale_order')]}"/>
</xpath>
</field>
</record>
<!-- 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', '&lt;=', 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>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_order_form_inherit" model="ir.ui.view">
<field name="name">sale.order.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="%(rma.action_rma_rma)d" type="action" class="oe_stat_button" icon="fa-cubes"
attrs="{'invisible': [('rma_count', '=', 0)]}"
context="{'search_default_sale_order_id': active_id}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="rma_count"/></span> RMA
</div>
</button>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import rma_lines

View File

@@ -0,0 +1,73 @@
# 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 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,
'validity': self.rma_id.template_id._rma_sale_line_validity(so_line),
}
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))
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)
if not self.env.user.has_group('sales_team.group_sale_manager'):
if lines.filtered(lambda l: not l.validity):
raise UserError('One or more items are not eligible for return.')
if lines.filtered(lambda l: l.validity == 'expired'):
raise UserError('One or more items are past their return period.')
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'
_description = '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('uom.uom', 'UOM')
validity = fields.Selection([
('', 'Not Eligible'),
('valid', 'Eligible'),
('expired', 'Expired'),
], string='Validity')

View 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" create="false" delete="false" decoration-muted="validity != 'valid'">
<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"/>
<field name="validity" 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_mode">form</field>
<field name="view_id" ref="view_rma_add_lines_form" />
<field name="target">new</field>
</record>
</odoo>