Merge branch 'mig/14.0/rma_sale' into '14.0-test'

mig/14.0/rma_sale into 14.0-test

See merge request hibou-io/hibou-odoo/suite!820
This commit is contained in:
Hibou Bot
2021-03-20 20:31:43 +00:00
23 changed files with 1225 additions and 68 deletions

View File

@@ -2,7 +2,7 @@
{
'name': 'Hibou RMAs',
'version': '14.0.1.0.0',
'version': '14.0.1.0.1',
'category': 'Warehouse',
'author': 'Hibou Corp.',
'license': 'OPL-1',

View File

@@ -1,14 +1,68 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from collections import OrderedDict
from operator import itemgetter
from odoo import http, fields
from odoo.exceptions import AccessError, MissingError, ValidationError
from odoo.exceptions import AccessError, MissingError, UserError, ValidationError
from odoo.http import request
from odoo.tools import groupby as groupbyelem
from odoo.tools.translate import _
from odoo.addons.portal.controllers.portal import pager as portal_pager, CustomerPortal
def rma_portal_searchbar_sortings():
# Override to add more sorting
return {
'date': {'label': _('Newest'), 'order': 'create_date desc, id desc'},
'name': {'label': _('Name'), 'order': 'name asc, id asc'},
}
def rma_portal_searchbar_filters():
# Override to add more filters
return {
'all': {'label': _('All'), 'domain': [('state', 'in', ['draft', 'confirmed', 'done', 'cancel'])]},
'draft': {'label': _('Draft'), 'domain': [('state', '=', 'draft')]},
'confirmed': {'label': _('Confirmed'), 'domain': [('state', '=', 'confirmed')]},
'cancel': {'label': _('Cancelled'), 'domain': [('state', '=', 'cancel')]},
'done': {'label': _('Done'), 'domain': [('state', '=', 'done')]},
}
def rma_portal_searchbar_inputs():
# Override to add more search fields
return {
'name': {'input': 'name', 'label': _('Search in Name')},
'all': {'input': 'all', 'label': _('Search in All')},
}
def rma_portal_searchbar_groupby():
# Override to add more options for grouping
return {
'none': {'input': 'none', 'label': _('None')},
'state': {'input': 'state', 'label': _('State')},
'template': {'input': 'template', 'label': _('Type')},
}
def rma_portal_search_domain(search_in, search):
# Override if you added search inputs
search_domain = []
if search_in in ('name', 'all'):
search_domain.append(('name', 'ilike', search))
return search_domain
def rma_portal_group_rmas(rmas, groupby):
# Override to check groupby and perform a different grouping
if groupby == 'state':
return [request.env['rma.rma'].concat(*g) for k, g in groupbyelem(rmas, itemgetter('state'))]
if groupby == 'template':
return [request.env['rma.rma'].concat(*g) for k, g in groupbyelem(rmas, itemgetter('template_id'))]
return [rmas]
class CustomerPortal(CustomerPortal):
def _prepare_portal_layout_values(self):
@@ -25,72 +79,58 @@ class CustomerPortal(CustomerPortal):
return self._get_page_view_values(rma, access_token, values, 'my_rma_history', True, **kwargs)
@http.route(['/my/rma', '/my/rma/page/<int:page>'], type='http', auth="user", website=True)
def portal_my_rma(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, **kw):
def portal_my_rma(self, page=1, date_begin=None, date_end=None, sortby='date', filterby='all', groupby='none', search_in='all', search=None, **kw):
values = self._prepare_portal_layout_values()
RMA = request.env['rma.rma']
domain = []
fields = ['name', 'create_date']
searchbar_sortings = rma_portal_searchbar_sortings()
searchbar_filters = rma_portal_searchbar_filters()
searchbar_inputs = rma_portal_searchbar_inputs()
searchbar_groupby = rma_portal_searchbar_groupby()
archive_groups = self._get_archive_groups('rma.rma', domain, fields)
if sortby not in searchbar_sortings:
raise UserError(_("Unknown sorting option."))
order = searchbar_sortings[sortby]['order']
if filterby not in searchbar_filters:
raise UserError(_("Unknown filter option."))
domain = searchbar_filters[filterby]['domain']
if date_begin and date_end:
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
searchbar_sortings = {
'date': {'label': _('Newest'), 'order': 'create_date desc, id desc'},
'name': {'label': _('Name'), 'order': 'name asc, id asc'},
}
# default sort by value
if not sortby:
sortby = 'date'
order = searchbar_sortings[sortby]['order']
if search_in and search:
domain += rma_portal_search_domain(search_in, search)
searchbar_filters = {
'all': {'label': _('All'), 'domain': [('state', 'in', ['draft', 'confirmed', 'done', 'cancel'])]},
'draft': {'label': _('Draft'), 'domain': [('state', '=', 'draft')]},
'purchase': {'label': _('Confirmed'), 'domain': [('state', '=', 'confirmed')]},
'cancel': {'label': _('Cancelled'), 'domain': [('state', '=', 'cancel')]},
'done': {'label': _('Done'), 'domain': [('state', '=', 'done')]},
}
# default filter by value
if not filterby:
filterby = 'all'
domain += searchbar_filters[filterby]['domain']
# count for pager
rma_count = RMA.search_count(domain)
# make pager
RMA = request.env['rma.rma']
rma_count = len(RMA.search(domain))
pager = portal_pager(
url="/my/rma",
url_args={'date_begin': date_begin, 'date_end': date_end},
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'search_in': search_in, 'search': search},
total=rma_count,
page=page,
step=self._items_per_page
)
# search the rmas to display, according to the pager data
rmas = RMA.search(
domain,
order=order,
limit=self._items_per_page,
offset=pager['offset']
)
rmas = RMA.search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
request.session['my_rma_history'] = rmas.ids[:100]
rma_templates = request.env['rma.template'].sudo().search([('portal_ok', '=', True)])
grouped_rmas = rma_portal_group_rmas(rmas, groupby)
values.update({
'request': request,
'date': date_begin,
'rma_list': rmas,
'rma_templates': rma_templates,
'date': date_begin,
'grouped_rmas': grouped_rmas,
'page_name': 'rma',
'pager': pager,
'archive_groups': archive_groups,
'searchbar_sortings': searchbar_sortings,
'sortby': sortby,
'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())),
'filterby': filterby,
'default_url': '/my/rma',
'pager': pager,
'searchbar_sortings': searchbar_sortings,
'searchbar_filters': searchbar_filters,
'searchbar_inputs': searchbar_inputs,
'searchbar_groupby': searchbar_groupby,
'sortby': sortby,
'groupby': groupby,
'search_in': search_in,
'search': search,
'filterby': filterby,
})
return request.render("rma.portal_my_rma", values)

View File

@@ -8,3 +8,7 @@
access_rma_portal,rma.rma.portal,rma.model_rma_rma,base.group_portal,1,0,0,0
access_rma_line_portal,rma.line.portal,rma.model_rma_line,base.group_portal,1,0,0,0
access_rma_template_portal,rma.template.portal,rma.model_rma_template,base.group_portal,1,0,0,0
"access_rma_picking_make_lines","access rma.picking.make.lines","rma.model_rma_picking_make_lines","stock.group_stock_user",1,1,1,1
"access_rma_picking_make_lines_line","access rma.picking.make.lines.line","rma.model_rma_picking_make_lines_line","stock.group_stock_user",1,1,1,1
"access_rma_make_rtv","access rma.make.rtv","rma.model_rma_make_rtv","stock.group_stock_user",1,1,1,1
"access_rma_make_rtv_line","access rma.make.rtv.line","rma.model_rma_make_rtv_line","stock.group_stock_user",1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
8 access_rma_portal rma.rma.portal rma.model_rma_rma base.group_portal 1 0 0 0
9 access_rma_line_portal rma.line.portal rma.model_rma_line base.group_portal 1 0 0 0
10 access_rma_template_portal rma.template.portal rma.model_rma_template base.group_portal 1 0 0 0
11 access_rma_picking_make_lines access rma.picking.make.lines rma.model_rma_picking_make_lines stock.group_stock_user 1 1 1 1
12 access_rma_picking_make_lines_line access rma.picking.make.lines.line rma.model_rma_picking_make_lines_line stock.group_stock_user 1 1 1 1
13 access_rma_make_rtv access rma.make.rtv rma.model_rma_make_rtv stock.group_stock_user 1 1 1 1
14 access_rma_make_rtv_line access rma.make.rtv.line rma.model_rma_make_rtv_line stock.group_stock_user 1 1 1 1

View File

@@ -32,34 +32,51 @@
<template id="portal_my_rma" name="Portal: My RMAs">
<t t-call="portal.portal_layout">
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<t t-call="portal.portal_searchbar">
<t t-set="title">RMA</t>
</t>
<t t-if="rma_list" t-call="portal.portal_table">
<thead>
<tr class="active">
<th>RMA #</th>
<th>Submitted Date</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<t t-foreach="rma_list" t-as="rma">
<div t-if="not grouped_rmas" class="alert alert-info">
There are currently no RMAs for your account.
</div>
<t t-else="" t-call="portal.portal_table">
<t t-foreach="grouped_rmas" t-as="rmas">
<thead>
<tr t-attf-class="{{'thead-light' if not groupby == 'none' else ''}}">
<th class="text-left">RMA #</th>
<th>Submitted Date</th>
<th class="rma-template">
<t t-if="groupby == 'template'">
<span t-field="rmas[0].template_id"/>
</t>
<t t-else="">
<span>Type</span>
</t>
</th>
<th class="rma-state">
<t t-if="groupby == 'state'">
<em class="font-weight-normal text-muted">RMAs in state:</em>
<span t-field="rmas[0].state"/>
</t>
<t t-else="">
<span>Status</span>
</t>
</th>
</tr>
</thead>
<t t-foreach="rmas" t-as="rma">
<tr>
<td>
<a t-attf-href="/my/rma/#{rma.id}?#{keep_query()}">
<t t-esc="rma.name"/>
</a>
<td class="text-left"><a t-attf-href="/my/rma/#{rma.id}"><span t-field="rma.name"/></a></td>
<td><span t-field="rma.create_date"/></td>
<td class="rma-template">
<span t-if="groupby != 'template'" t-field="rma.template_id"/>
</td>
<td>
<span t-field="rma.create_date"/>
</td>
<td>
<span t-field="rma.state"/>
<td class="rma-state">
<span t-if="groupby != 'state'" t-attf-class="badge badge-pill #{'badge-' + ('warning' if rma.state == 'draft' else 'success' if rma.state == 'confirmed' else 'info')}" t-field="rma.state"/>
</td>
</tr>
</t>
</tbody>
</t>
</t>
<div t-if="rma_templates" class="row">
<div class="col-12">

5
rma_sale/__init__.py Normal file
View File

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

30
rma_sale/__manifest__.py Normal file
View File

@@ -0,0 +1,30 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
{
'name': 'Hibou RMAs for Sale Orders',
'version': '14.0.1.0.0',
'category': 'Sale',
'author': 'Hibou Corp.',
'license': 'OPL-1',
'website': 'https://hibou.io/',
'depends': [
'hibou_professional',
'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,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import portal

View File

@@ -0,0 +1,53 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from operator import itemgetter
import odoo.addons.rma.controllers.portal as rma_portal
from odoo.http import request
from odoo.tools.translate import _
from odoo.osv.expression import OR
from odoo.tools import groupby as groupbyelem
original_rma_portal_searchbar_filters = rma_portal.rma_portal_searchbar_filters
original_rma_portal_searchbar_inputs = rma_portal.rma_portal_searchbar_inputs
original_rma_portal_search_domain = rma_portal.rma_portal_search_domain
original_rma_portal_searchbar_groupby = rma_portal.rma_portal_searchbar_groupby
original_rma_portal_group_rmas = rma_portal.rma_portal_group_rmas
def rma_portal_searchbar_filters():
res = original_rma_portal_searchbar_filters()
res['sale'] = {'label': _('Sale Order'), 'domain': [('sale_order_id', '!=', False)]}
return res
def rma_portal_searchbar_inputs():
res = original_rma_portal_searchbar_inputs()
res['sale'] = {'input': 'sale', 'label': _('Search Sale Order')}
return res
def rma_portal_search_domain(search_in, search):
search_domain = original_rma_portal_search_domain(search_in, search)
if search_in in ('sale', 'all'):
search_domain = OR([search_domain, [('sale_order_id', 'ilike', search)]])
return search_domain
def rma_portal_searchbar_groupby():
res = original_rma_portal_searchbar_groupby()
res['sale'] = {'input': 'sale', 'label': _('Sale Order')}
return res
def rma_portal_group_rmas(rmas, groupby):
if groupby == 'sale':
return [request.env['rma.rma'].concat(*g) for k, g in groupbyelem(rmas, itemgetter('sale_order_id'))]
return original_rma_portal_group_rmas(rmas, groupby)
rma_portal.rma_portal_searchbar_filters = rma_portal_searchbar_filters
rma_portal.rma_portal_searchbar_inputs = rma_portal_searchbar_inputs
rma_portal.rma_portal_search_domain = rma_portal_search_domain
rma_portal.rma_portal_searchbar_groupby = rma_portal_searchbar_groupby
rma_portal.rma_portal_group_rmas = rma_portal_group_rmas

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,26 @@
# 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.')
rma_sale_warranty_validity = fields.Integer(string='RMA Eligible Days (Sale Warranty)',
help='Determines the number of days from the time '
'of the sale that the product is eligible to '
'be returned for warranty claims. '
'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.')

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

@@ -0,0 +1,265 @@
# 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')])
sale_order_warranty = fields.Boolean(string='Sale Order Warranty',
help='Determines if the regular return validity or '
'Warranty validity is used.')
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):
if self.sale_order_warranty:
validity_days = so_line.product_id.rma_sale_warranty_validity
else:
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 _invoice_values_sale_order(self):
# the RMA invoice API will not be used as invoicing will happen at the SO level
return False
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,9 @@
"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
"access_rma_sale_make_lines","access rma.sale.make.lines","rma_sale.model_rma_sale_make_lines","sales_team.group_sale_salesman",1,1,1,1
"access_rma_sale_make_lines_line","access rma.sale.make.lines.line","rma_sale.model_rma_sale_make_lines_line","sales_team.group_sale_salesman",1,1,1,1
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
8 access_rma_sale_make_lines access rma.sale.make.lines rma_sale.model_rma_sale_make_lines sales_team.group_sale_salesman 1 1 1 1
9 access_rma_sale_make_lines_line access rma.sale.make.lines.line rma_sale.model_rma_sale_make_lines_line sales_team.group_sale_salesman 1 1 1 1

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

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

@@ -0,0 +1,306 @@
# 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.button_validate()
# RMA cannot be completed because the inbound picking state is confirmed
with self.assertRaises(UserError):
rma2.action_done()
def test_30_product_sale_return_warranty(self):
self.template_sale_return.write({
'usage': 'sale_order',
'invoice_done': True,
'sale_order_warranty': True,
'in_to_refund': True,
'so_decrement_order_qty': False, # invoice on decremented delivered not decremented order
'next_rma_template_id': self.template_rtv.id,
})
validity = 100 # eligible for 100 days
warranty_validity = validity + 100 # eligible for 200 days
self.product1.write({
'rma_sale_validity': validity,
'rma_sale_warranty_validity': warranty_validity,
'type': 'product',
'invoice_policy': 'delivery',
'tracking': 'serial',
'standard_price': 1.5,
})
order = self.env['sale.order'].create({
'partner_id': self.partner1.id,
'partner_invoice_id': self.partner1.id,
'partner_shipping_id': self.partner1.id,
'user_id': self.user1.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 warranty return.
self.product1.rma_sale_warranty_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_warranty_validity = warranty_validity
original_date_order = order.date_order
order.write({'date_order': original_date_order - timedelta(days=warranty_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 due to date, due to warranty option
order.write({'date_order': original_date_order - timedelta(days=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
wizard.add_lines()
# finish outbound so that we can invoice.
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')
self.assertEqual(order.order_line.qty_delivered, 1.0)
# Invoice the order so that only the core product is invoiced at the end...
self.assertFalse(order.invoice_ids)
wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=order.ids).create({})
wiz.create_invoices()
order.flush()
self.assertTrue(order.invoice_ids)
order_invoice = order.invoice_ids
self.assertEqual(rma.lines.product_id, self.product1)
rma.action_confirm()
self.assertTrue(rma.in_picking_id)
self.assertEqual(rma.in_picking_id.state, 'assigned')
pack_opt = rma.in_picking_id.move_line_ids[0]
pack_opt.lot_id = lot.id
pack_opt.qty_done = 1.0
rma.in_picking_id.button_validate()
self.assertEqual(rma.in_picking_id.state, 'done')
order.flush()
# self.assertEqual(order.order_line.qty_delivered, 0.0)
rma.action_done()
self.assertEqual(rma.state, 'done')
order.flush()
rma_invoice = rma.invoice_ids
self.assertTrue(rma_invoice)
sale_line = rma_invoice.invoice_line_ids.filtered(lambda l: l.sale_line_ids)
so_line = sale_line.sale_line_ids
self.assertTrue(sale_line)
self.assertEqual(sale_line.price_unit, so_line.price_unit)
# Invoices do not have their anglo-saxon cost lines until they post
order_invoice._post(soft=False)
rma_invoice._post(soft=False)
# Find the return to vendor RMA
rtv_rma = self.env['rma.rma'].search([('parent_id', '=', rma.id)])
self.assertTrue(rtv_rma)
self.assertFalse(rtv_rma.out_picking_id)
wiz = self.env['rma.make.rtv'].with_context(active_model='rma.rma', active_ids=rtv_rma.ids).create({})
self.assertTrue(wiz.rma_line_ids)
wiz.partner_id = self.partner2
wiz.create_batch()
self.assertTrue(rtv_rma.out_picking_id)
self.assertEqual(rtv_rma.out_picking_id.partner_id, self.partner2)

View File

@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="portal_my_rma_inherit" inherit_id="rma.portal_my_rma">
<xpath expr="//th[hasclass('rma-state')]" position="before">
<th class="rma-sale">
<t t-if="groupby == 'sale'">
<em class="font-weight-normal text-muted">RMAs for sale:</em>
<span t-field="rmas[0].sale_order_id"/>
</t>
<t t-else="">
<span>Sale Order</span>
</t>
</th>
</xpath>
<xpath expr="//td[hasclass('rma-state')]" position="before">
<td class="rma-sale">
<span t-if="groupby != 'sale'" t-field="rma.sale_order_id"/>
</td>
</xpath>
</template>
<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 class="mr4 float-left o_portal_product_img" t-if="line.product_id.image_128" t-att-src="image_data_uri(line.product_id.image_128)" alt="Product Image" width="64"/>
<img class="mr4 float-left o_portal_product_img" t-if="not line.product_id.image_128" src="/web/static/src/img/placeholder.png" alt="Product Image" width="64"/>
</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,32 @@
<?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"/>
<field name="rma_sale_warranty_validity" string="Warranty 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"/>
<field name="rma_sale_warranty_validity" string="Warranty Eligible Days"/>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,78 @@
<?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')]}"/>
<field name="sale_order_warranty" string="Warranty" 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>