[MOV] rma: from Hibou Suite Enterprise for 13.0

This commit is contained in:
Jared Kipe
2020-07-03 08:53:24 -07:00
parent 78da77ab55
commit 9afed572a2
24 changed files with 1936 additions and 0 deletions

5
rma/__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

31
rma/__manifest__.py Normal file
View File

@@ -0,0 +1,31 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
{
'name': 'Hibou RMAs',
'version': '13.0.1.2.0',
'category': 'Warehouse',
'author': 'Hibou Corp.',
'license': 'OPL-1',
'website': 'https://hibou.io/',
'depends': [
'stock',
'delivery',
],
'demo': [
'demo/rma_demo.xml',
],
'data': [
'data/cron_data.xml',
'data/ir_sequence_data.xml',
'security/ir.model.access.csv',
'security/rma_security.xml',
'views/account_views.xml',
'views/portal_templates.xml',
'views/rma_views.xml',
'views/stock_picking_views.xml',
'wizard/rma_lines_views.xml',
],
'installable': True,
'application': True,
'auto_install': False,
}

View File

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

51
rma/controllers/main.py Normal file
View File

@@ -0,0 +1,51 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import http, exceptions
from base64 import b64decode
import hmac
from hashlib import sha256
from datetime import datetime
from time import mktime
def create_hmac(secret, a_attchment_id, e_expires):
return hmac.new(secret.encode(), str(str(a_attchment_id) + str(e_expires)).encode(), sha256).hexdigest()
def check_hmac(secret, hash_, a_attachment_id, e_expires):
myh = hmac.new(secret.encode(), str(str(a_attachment_id) + str(e_expires)).encode(), sha256)
return hmac.compare_digest(str(hash_), myh.hexdigest())
class RMAController(http.Controller):
@http.route(['/rma_label'], type='http', auth='public', website=True)
def index(self, *args, **request):
a_attachment_id = request.get('a')
e_expires = request.get('e')
hash = request.get('h')
if not all([a_attachment_id, e_expires, hash]):
return http.Response('Invalid Request', status=400)
now = datetime.utcnow()
now = int(mktime(now.timetuple()))
config = http.request.env['ir.config_parameter'].sudo()
secret = str(config.search([('key', '=', 'database.secret')], limit=1).value)
if not check_hmac(secret, hash, a_attachment_id, e_expires):
return http.Response('Invalid Request', status=400)
if now > int(e_expires):
return http.Response('Expired', status=404)
attachment = http.request.env['ir.attachment'].sudo().search([('id', '=', int(a_attachment_id))], limit=1)
if attachment:
data = attachment.datas
filename = attachment.name
mimetype = attachment.mimetype
return http.request.make_response(b64decode(data), [
('Content-Type', mimetype),
('Content-Disposition', 'attachment; filename="' + filename + '"')])
return http.Response('Invalid Attachment', status=404)

137
rma/controllers/portal.py Normal file
View File

@@ -0,0 +1,137 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from collections import OrderedDict
from odoo import http, fields
from odoo.exceptions import AccessError, MissingError, ValidationError
from odoo.http import request
from odoo.tools.translate import _
from odoo.addons.portal.controllers.portal import pager as portal_pager, CustomerPortal
class CustomerPortal(CustomerPortal):
def _prepare_portal_layout_values(self):
values = super(CustomerPortal, self)._prepare_portal_layout_values()
values['rma_count'] = request.env['rma.rma'].search_count([
])
return values
def _rma_get_page_view_values(self, rma, access_token, **kwargs):
values = {
'rma': rma,
'current_date': fields.Datetime.now(),
}
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):
values = self._prepare_portal_layout_values()
RMA = request.env['rma.rma']
domain = []
fields = ['name', 'create_date']
archive_groups = self._get_archive_groups('rma.rma', domain, fields)
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']
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
pager = portal_pager(
url="/my/rma",
url_args={'date_begin': date_begin, 'date_end': date_end},
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']
)
request.session['my_rma_history'] = rmas.ids[:100]
rma_templates = request.env['rma.template'].sudo().search([('portal_ok', '=', True)])
values.update({
'request': request,
'date': date_begin,
'rma_list': rmas,
'rma_templates': rma_templates,
'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',
})
return request.render("rma.portal_my_rma", values)
@http.route(['/my/rma/<int:rma_id>'], type='http', auth="public", website=True)
def portal_my_rma_rma(self, rma_id=None, access_token=None, **kw):
try:
rma_sudo = self._document_check_access('rma.rma', rma_id, access_token=access_token)
except (AccessError, MissingError):
return request.redirect('/my')
values = self._rma_get_page_view_values(rma_sudo, access_token, **kw)
return request.render("rma.portal_my_rma_rma", values)
@http.route(['/my/rma/new/<int:rma_template_id>',
'/my/rma/new/<int:rma_template_id>/res/<int:res_id>'], type='http', auth='public', website=True)
def portal_rma_new(self, rma_template_id=None, res_id=None, **kw):
if request.env.user.has_group('base.group_public'):
return request.redirect('/my')
rma_template = request.env['rma.template'].sudo().browse(rma_template_id)
if not rma_template.exists() or not rma_template.portal_ok:
return request.redirect('/my')
error = None
try:
if res_id:
# Even if res_id is not important to the RMA type, some sort of number
# should be submitted to indicate that a selection has occurred.
rma = rma_template._portal_try_create(request.env.user, res_id, **kw)
if rma:
return request.redirect('/my/rma/' + str(rma.id))
except ValidationError as e:
error = e.name
template_name = rma_template._portal_template(res_id=res_id)
if not template_name:
return request.redirect('/my')
values = rma_template._portal_values(request.env.user, res_id=res_id)
values.update({
'request': request,
'error': error,
'current_date': fields.Datetime.now(),
})
return request.render(template_name, values)

17
rma/data/cron_data.xml Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data noupdate="1">
<record model="ir.cron" id="rma_expire">
<field name="name">RMA Expiration</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="model_id" ref="model_rma_template"/>
<field name="state">code</field>
<field name="code">model._rma_expire()</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="seq_rma" model="ir.sequence">
<field name="name">RMA</field>
<field name="code">rma.rma</field>
<field name="prefix">RMA</field>
<field name="padding">3</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>

45
rma/demo/rma_demo.xml Normal file
View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="template_missing_item" model="rma.template">
<field name="name">Missing Item</field>
<field name="valid_days" eval="10"/>
<field name="create_out_picking" eval="True"/>
<field name="out_type_id" ref="stock.picking_type_out"/>
<field name="out_location_id" ref="stock.stock_location_stock"/>
<field name="out_location_dest_id" ref="stock.stock_location_customers"/>
<field name="out_procure_method">make_to_stock</field>
</record>
<record id="rma_return_sequence" model="ir.sequence">
<field name="name">RMA Returns</field>
<field name="implementation">standard</field>
<field name="prefix">WH/RMA/</field>
<field name="padding" eval="5"/>
<field name="number_increment" eval="1"/>
</record>
<record id="picking_type_rma_return" model="stock.picking.type">
<field name="name">RMA Receipts</field>
<field name="sequence_code">WH/RMA</field>
<field name="sequence_id" ref="rma_return_sequence"/>
<field name="warehouse_id" ref="stock.warehouse0"/>
<field name="code">incoming</field>
<field name="show_operations" eval="False"/>
<field name="show_reserved" eval="True"/>
<field name="use_create_lots" eval="False"/>
<field name="use_existing_lots" eval="True"/>
<field name="default_location_dest_id" ref="stock.stock_location_stock"/>
</record>
<record id="template_picking_return" model="rma.template">
<field name="name">Picking Return</field>
<field name="usage">stock_picking</field>
<field name="valid_days" eval="10"/>
<field name="create_in_picking" eval="True"/>
<field name="in_type_id" ref="picking_type_rma_return"/>
<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_require_return" eval="True"/>
</record>
</odoo>

View File

@@ -0,0 +1,11 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
def migrate(cr, installed_version):
# Provide defaults for RMAs that were created before these fields existed.
cr.execute('''
UPDATE rma_rma as r
SET initial_in_picking_carrier_id = t.in_carrier_id ,
initial_out_picking_carrier_id = t.out_carrier_id
FROM rma_template as t
WHERE r.template_id = t.id
''')

5
rma/models/__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 account
from . import rma
from . import stock_picking

13
rma/models/account.py Normal file
View File

@@ -0,0 +1,13 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import fields, models
class AccountMove(models.Model):
_inherit = 'account.move'
rma_ids = fields.Many2many('rma.rma',
'rma_invoice_rel',
'invoice_id',
'rma_id',
string='RMAs')

582
rma/models/rma.py Normal file
View File

@@ -0,0 +1,582 @@
# 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 ..controllers.main import create_hmac
from datetime import timedelta, datetime
from time import mktime
class RMATemplate(models.Model):
_name = 'rma.template'
_description = 'RMA Template'
name = fields.Char(string='Name')
usage = fields.Selection([
('stock_picking', 'Stock Picking'),
], string='Applies To')
description = fields.Html(string='Internal Instructions')
customer_description = fields.Html(string='Customer Instructions')
valid_days = fields.Integer(string='Expire in Days')
automatic_expire = fields.Boolean('Automatic Expire',
help='RMAs with this template will automatically '
'expire when past their expiration date.')
invoice_done = fields.Boolean(string='Invoice on Completion')
create_in_picking = fields.Boolean(string='Create Inbound Picking')
create_out_picking = fields.Boolean(string='Create Outbound Picking')
in_type_id = fields.Many2one('stock.picking.type', string='Inbound Picking Type')
out_type_id = fields.Many2one('stock.picking.type', string='Outbound Picking Type')
in_location_id = fields.Many2one('stock.location', string='Inbound Source Location')
in_location_dest_id = fields.Many2one('stock.location', string='Inbound Destination Location')
in_carrier_id = fields.Many2one('delivery.carrier', string='Inbound Carrier')
in_require_return = fields.Boolean(string='Inbound Require return of picking')
in_procure_method = fields.Selection([
('make_to_stock', 'Take from Stock'),
('make_to_order', 'Apply Procurements')
], string="Inbound Procurement Method", default='make_to_stock')
in_to_refund = fields.Boolean(string='Inbound Mark Refund')
out_location_id = fields.Many2one('stock.location', string='Outbound Source Location')
out_location_dest_id = fields.Many2one('stock.location', string='Outbound Destination Location')
out_carrier_id = fields.Many2one('delivery.carrier', string='Outbound Carrier')
out_require_return = fields.Boolean(string='Outbound Require picking to duplicate')
out_procure_method = fields.Selection([
('make_to_stock', 'Take from Stock'),
('make_to_order', 'Apply Procurements')
], string="Outbound Procurement Method", default='make_to_stock')
out_to_refund = fields.Boolean(string='Outbound Mark Refund')
portal_ok = fields.Boolean(string='Allow on Portal')
company_id = fields.Many2one('res.company', 'Company')
responsible_user_ids = fields.Many2many('res.users', string='Responsible Users',
help='Users that get activities when creating RMA.')
def _portal_try_create(self, request_user, res_id, **kw):
if self.usage == 'stock_picking':
prefix = 'move_'
move_map = {int(key[len(prefix):]): float(kw[key]) for key in kw if key.find(prefix) == 0 and kw[key]}
if move_map:
picking = self.env['stock.picking'].browse(res_id)
if picking.partner_id != request_user.partner_id:
raise ValidationError('Invalid user for picking.')
lines = []
for move_id, qty in move_map.items():
move = picking.move_lines.filtered(lambda l: l.id == move_id)
if move:
if not qty:
continue
if qty < 0.0 or move.quantity_done < qty:
raise ValidationError('Invalid quantity.')
lines.append((0, 0, {
'product_id': move.product_id.id,
'product_uom_id': move.product_uom.id,
'product_uom_qty': qty,
}))
if not lines:
raise ValidationError('Missing product quantity.')
rma = self.env['rma.rma'].create({
'name': _('New'),
'stock_picking_id': picking.id,
'template_id': self.id,
'partner_id': request_user.partner_id.id,
'partner_shipping_id': request_user.partner_id.id,
'lines': lines,
})
return rma
def _portal_template(self, res_id=None):
if self.usage == 'stock_picking':
return 'rma.portal_new_stock_picking'
def _portal_values(self, request_user, res_id=None):
if self.usage == 'stock_picking':
picking = None
pickings = None
if res_id:
picking = self.env['stock.picking'].browse(res_id)
if picking.partner_id != request_user.partner_id:
picking = None
else:
pickings = self.env['stock.picking'].search([('partner_id', '=', request_user.partner_id.id)], limit=100)
return {
'rma_template': self,
'pickings': pickings,
'picking': picking,
}
def _values_for_in_picking(self, rma):
return {
'origin': rma.name,
'partner_id': rma.partner_shipping_id.id,
'picking_type_id': self.in_type_id.id,
'location_id': self.in_location_id.id,
'location_dest_id': self.in_location_dest_id.id,
'carrier_id': rma.initial_in_picking_carrier_id.id,
'move_lines': [(0, None, {
'name': rma.name + ' IN: ' + l.product_id.name_get()[0][1],
'product_id': l.product_id.id,
'product_uom_qty': l.product_uom_qty,
'product_uom': l.product_uom_id.id,
'procure_method': self.in_procure_method,
'to_refund': self.in_to_refund,
}) for l in rma.lines.filtered(lambda l: l.product_id.type != 'service')],
}
def _values_for_out_picking(self, rma):
return {
'origin': rma.name,
'partner_id': rma.partner_shipping_id.id,
'picking_type_id': self.out_type_id.id,
'location_id': self.out_location_id.id,
'location_dest_id': self.out_location_dest_id.id,
'carrier_id': rma.initial_out_picking_carrier_id.id,
'move_lines': [(0, None, {
'name': rma.name + ' OUT: ' + l.product_id.name_get()[0][1],
'product_id': l.product_id.id,
'product_uom_qty': l.product_uom_qty,
'product_uom': l.product_uom_id.id,
'procure_method': self.out_procure_method,
'to_refund': self.out_to_refund,
}) for l in rma.lines.filtered(lambda l: l.product_id.type != 'service')],
}
def _schedule_responsible_activities(self, rma):
model_id = self.env['ir.model']._get(rma._name).id
activity_to_write = []
for user in self.responsible_user_ids:
if rma.with_user(user).check_access_rights('read', raise_exception=False):
activity_to_write.append((0, 0, {
'res_id': rma.id,
'res_model_id': model_id,
'summary': 'Review New RMA',
'activity_type_id': False,
'user_id': user.id,
}))
if activity_to_write:
rma.write({
'activity_ids': activity_to_write,
})
def _rma_expire(self):
templates = self.sudo().search([('automatic_expire', '=', True)])
if not templates:
return True
rmas = self.env['rma.rma'].sudo().search([
('template_id', 'in', templates.ids),
('state', 'in', ('draft', 'confirmed',)),
('validity_date', '<', fields.Datetime.now())
])
if rmas:
return rmas._action_expire()
return True
class RMATag(models.Model):
_name = "rma.tag"
_description = "RMA Tag"
name = fields.Char('Tag Name', required=True)
color = fields.Integer('Color Index')
_sql_constraints = [
('name_uniq', 'unique (name)', "Tag name already exists !"),
]
class RMA(models.Model):
_name = 'rma.rma'
_inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin']
_description = 'RMA'
_order = 'id desc'
_mail_post_access = 'read'
name = fields.Char(string='Number', copy=False)
state = fields.Selection([
('draft', 'New'),
('confirmed', 'Confirmed'),
('done', 'Done'),
('expired', 'Expired'),
('cancel', 'Cancelled'),
], string='State', default='draft', copy=False)
company_id = fields.Many2one('res.company', 'Company')
template_id = fields.Many2one('rma.template', string='Type', required=True)
template_create_in_picking = fields.Boolean(related='template_id.create_in_picking')
template_create_out_picking = fields.Boolean(related='template_id.create_out_picking')
stock_picking_id = fields.Many2one('stock.picking', string='Stock Picking')
stock_picking_rma_count = fields.Integer('Number of RMAs for this Picking', compute='_compute_stock_picking_rma_count')
partner_id = fields.Many2one('res.partner', string='Partner')
partner_shipping_id = fields.Many2one('res.partner', string='Shipping')
lines = fields.One2many('rma.line', 'rma_id', string='Lines')
tag_ids = fields.Many2many('rma.tag', 'rma_tags_rel', 'rma_id', 'tag_id', string='Tags')
description = fields.Html(string='Internal Instructions', related='template_id.description')
customer_description = fields.Html(string='Customer Instructions', related='template_id.customer_description')
template_usage = fields.Selection(string='Template Usage', related='template_id.usage')
validity_date = fields.Datetime(string='Expiration Date')
invoice_ids = fields.Many2many('account.move',
'rma_invoice_rel',
'rma_id',
'invoice_id',
string='Invoices')
initial_in_picking_carrier_id = fields.Many2one('delivery.carrier', string='In Delivery Method')
initial_out_picking_carrier_id = fields.Many2one('delivery.carrier', string='Out Delivery Method')
in_picking_id = fields.Many2one('stock.picking', string='Inbound Picking', copy=False)
out_picking_id = fields.Many2one('stock.picking', string='Outbound Picking', copy=False)
in_picking_state = fields.Selection(string='In Picking State', related='in_picking_id.state')
out_picking_state = fields.Selection(string='Out Picking State', related='out_picking_id.state')
in_picking_carrier_id = fields.Many2one('delivery.carrier', related='in_picking_id.carrier_id', readonly=False)
out_picking_carrier_id = fields.Many2one('delivery.carrier', related='out_picking_id.carrier_id', readonly=False)
in_carrier_tracking_ref = fields.Char(related='in_picking_id.carrier_tracking_ref')
in_label_url = fields.Char(compute='_compute_in_label_url')
out_carrier_tracking_ref = fields.Char(related='out_picking_id.carrier_tracking_ref')
def _compute_access_url(self):
super(RMA, self)._compute_access_url()
for rma in self:
rma.access_url = '/my/rma/%s' % (rma.id)
@api.onchange('template_id')
def _onchange_template_id(self):
for rma in self:
rma.initial_in_picking_carrier_id = rma.template_id.in_carrier_id
rma.initial_out_picking_carrier_id = rma.template_id.out_carrier_id
@api.onchange('template_usage')
def _onchange_template_usage(self):
now = datetime.now()
for rma in self:
if rma.template_id.valid_days:
rma.validity_date = now + timedelta(days=rma.template_id.valid_days)
if rma.template_usage != 'stock_picking':
rma.stock_picking_id = False
@api.onchange('stock_picking_id')
def _onchange_stock_picking_id(self):
for rma in self.filtered(lambda rma: rma.stock_picking_id):
rma.partner_id = rma.stock_picking_id.partner_id
rma.partner_shipping_id = rma.stock_picking_id.partner_id
@api.onchange('in_carrier_tracking_ref', 'validity_date')
def _compute_in_label_url(self):
config = self.env['ir.config_parameter'].sudo()
secret = config.search([('key', '=', 'database.secret')], limit=1)
secret = str(secret.value) if secret else ''
base_url = config.search([('key', '=', 'web.base.url')], limit=1)
base_url = str(base_url.value) if base_url else ''
for rma in self:
if not rma.in_picking_id:
rma.in_label_url = ''
continue
if rma.validity_date:
e_expires = int(mktime(fields.Datetime.from_string(rma.validity_date).timetuple()))
else:
year = datetime.now() + timedelta(days=365)
e_expires = int(mktime(year.timetuple()))
attachment = self.env['ir.attachment'].search([
('res_model', '=', 'stock.picking'),
('res_id', '=', rma.in_picking_id.id),
('name', 'like', 'Label%')], limit=1)
if not attachment:
rma.in_label_url = ''
continue
rma.in_label_url = base_url + '/rma_label?a=' + \
str(attachment.id) + '&e=' + str(e_expires) + \
'&h=' + create_hmac(secret, attachment.id, e_expires)
@api.depends('stock_picking_id')
def _compute_stock_picking_rma_count(self):
for rma in self:
if rma.stock_picking_id:
rma_data = self.read_group([('stock_picking_id', '=', rma.stock_picking_id.id), ('state', '!=', 'cancel')],
['stock_picking_id'], ['stock_picking_id'])
if rma_data:
rma.stock_picking_rma_count = rma_data[0]['stock_picking_id_count']
else:
rma.stock_picking_rma_count = 0.0
def open_stock_picking_rmas(self):
return {
'type': 'ir.actions.act_window',
'name': _('Picking RMAs'),
'res_model': 'rma.rma',
'view_mode': 'tree,form',
'context': {'search_default_stock_picking_id': self[0].stock_picking_id.id}
}
def _action_expire(self):
pickings_to_cancel = self.env['stock.picking']
rmas = self.filtered(lambda rma: rma.in_picking_state != 'done' and rma.out_picking_state != 'done')
pickings_to_cancel += rmas.filtered(lambda r: r.in_picking_id).mapped('in_picking_id')
pickings_to_cancel += rmas.filtered(lambda r: r.out_picking_id).mapped('out_picking_id')
pickings_to_cancel.action_cancel()
rmas.write({'state': 'expired'})
return True
@api.model
def create(self, vals):
if vals.get('name', _('New')) == _('New'):
if 'company_id' in vals:
vals['name'] = self.env['ir.sequence'].with_context(force_company=vals['company_id']).next_by_code('rma.rma') or _('New')
else:
vals['name'] = self.env['ir.sequence'].next_by_code('rma.rma') or _('New')
# Provide defaults on create (e.g. from portal)
if vals.get('template_id'):
template = self.env['rma.template'].browse(vals.get('template_id'))
if 'initial_in_picking_carrier_id' not in vals:
vals['initial_in_picking_carrier_id'] = template.in_carrier_id.id
if 'initial_out_picking_carrier_id' not in vals:
vals['initial_out_picking_carrier_id'] = template.out_carrier_id.id
if template.valid_days and 'validity_date' not in vals:
now = datetime.now()
vals['validity_date'] = now + timedelta(days=template.valid_days)
result = super(RMA, self).create(vals)
result.template_id._schedule_responsible_activities(result)
return result
def action_confirm(self):
for rma in self:
in_picking_id = False
out_picking_id = False
if any((not rma.template_id, not rma.lines, not rma.partner_id, not rma.partner_shipping_id)):
raise UserError(_('You can only confirm RMAs with lines, and partner information.'))
if rma.template_id.create_in_picking:
in_picking_id = rma._create_in_picking()
if in_picking_id:
in_picking_id.action_confirm()
in_picking_id.action_assign()
if rma.template_id.create_out_picking:
out_picking_id = rma._create_out_picking()
if out_picking_id:
out_picking_id.action_confirm()
out_picking_id.action_assign()
rma.write({'state': 'confirmed',
'in_picking_id': in_picking_id.id if in_picking_id else False,
'out_picking_id': out_picking_id.id if out_picking_id else False})
def action_done(self):
for rma in self:
if rma.in_picking_id and rma.in_picking_id.state not in ('done', 'cancel'):
raise UserError(_('Inbound picking not complete or cancelled.'))
if rma.out_picking_id and rma.out_picking_id.state not in ('done', 'cancel'):
raise UserError(_('Outbound picking not complete or cancelled.'))
self.write({'state': 'done'})
def action_cancel(self):
for rma in self:
rma.in_picking_id.action_cancel()
rma.out_picking_id.action_cancel()
self.write({'state': 'cancel'})
def action_draft(self):
self.filtered(lambda l: l.state in ('cancel', 'expired')).write({
'state': 'draft', 'in_picking_id': False, 'out_picking_id': False})
def _create_in_picking(self):
if self.template_usage and hasattr(self, '_create_in_picking_' + self.template_usage):
return getattr(self, '_create_in_picking_' + self.template_usage)()
values = self.template_id._values_for_in_picking(self)
return self.env['stock.picking'].sudo().create(values)
def _create_out_picking(self):
if self.template_usage and hasattr(self, '_create_out_picking_' + self.template_usage):
return getattr(self, '_create_out_picking_' + self.template_usage)()
values = self.template_id._values_for_out_picking(self)
return self.env['stock.picking'].sudo().create(values)
def _find_candidate_return_picking(self, product_ids, pickings, location_id):
done_pickings = pickings.filtered(lambda p: p.state == 'done' and p.location_dest_id.id == location_id)
for p in done_pickings:
p_product_ids = p.move_lines.filtered(lambda l: l.state == 'done').mapped('product_id.id')
if set(product_ids) & set(p_product_ids) == set(product_ids):
return p
return None
def action_in_picking_send_to_shipper(self):
for rma in self:
if rma.in_picking_id and rma.in_picking_carrier_id:
rma.in_picking_id.send_to_shipper()
def action_add_picking_lines(self):
make_line_obj = self.env['rma.picking.make.lines']
for rma in self:
lines = make_line_obj.create({
'rma_id': rma.id,
})
action = self.env.ref('rma.action_rma_add_lines').read()[0]
action['res_id'] = lines.id
return action
def unlink(self):
for rma in self:
if rma.state not in ('draft'):
raise UserError(_('You can not delete a non-draft RMA.'))
return super(RMA, self).unlink()
def _picking_from_values(self, values, values_update, move_line_values_update):
values.update(values_update)
move_lines = []
for l1, l2, vals in values['move_lines']:
vals.update(move_line_values_update)
move_lines.append((l1, l2, vals))
values['move_lines'] = move_lines
return self.env['stock.picking'].sudo().create(values)
def _new_in_picking(self, old_picking):
new_picking = old_picking.copy({
'move_lines': [],
'picking_type_id': self.template_id.in_type_id.id,
'state': 'draft',
'origin': old_picking.name + ' ' + self.name,
'location_id': self.template_id.in_location_id.id,
'location_dest_id': self.template_id.in_location_dest_id.id,
'carrier_id': self.initial_in_picking_carrier_id.id,
'carrier_tracking_ref': False,
'carrier_price': False
})
new_picking.message_post_with_view('mail.message_origin_link',
values={'self': new_picking, 'origin': self},
subtype_id=self.env.ref('mail.mt_note').id)
return new_picking
def _new_in_move_vals(self, rma_line, new_picking, old_move):
return {
'name': self.name + ' IN: ' + rma_line.product_id.name_get()[0][1],
'product_id': rma_line.product_id.id,
'product_uom_qty': rma_line.product_uom_qty,
'product_uom': rma_line.product_uom_id.id,
'picking_id': new_picking.id,
'state': 'draft',
'location_id': old_move.location_dest_id.id,
'location_dest_id': self.template_id.in_location_dest_id.id,
'picking_type_id': new_picking.picking_type_id.id,
'warehouse_id': new_picking.picking_type_id.warehouse_id.id,
'origin_returned_move_id': old_move.id,
'procure_method': self.template_id.in_procure_method,
'to_refund': self.template_id.in_to_refund,
}
def _new_in_moves(self, old_picking, new_picking, move_update):
lines = self.lines.filtered(lambda l: l.product_uom_qty >= 1)
if not lines:
raise UserError(_('You have no lines with positive quantity.'))
moves = self.env['stock.move']
for l in lines:
return_move = old_picking.move_lines.filtered(lambda ol: ol.state == 'done' and ol.product_id.id == l.product_id.id)[0]
copy_vals = self._new_in_move_vals(l, new_picking, return_move)
copy_vals.update(move_update)
r = return_move.copy(copy_vals)
vals = {}
# +--------------------------------------------------------------------------------------------------------+
# | picking_pick <--Move Orig-- picking_pack --Move Dest--> picking_ship
# | | returned_move_ids ↑ | returned_move_ids
# | ↓ | return_line.move_id ↓
# | return pick(Add as dest) return toLink return ship(Add as orig)
# +--------------------------------------------------------------------------------------------------------+
move_orig_to_link = return_move.move_dest_ids.mapped('returned_move_ids')
move_dest_to_link = return_move.move_orig_ids.mapped('returned_move_ids')
vals['move_orig_ids'] = [(4, m.id) for m in move_orig_to_link | return_move]
vals['move_dest_ids'] = [(4, m.id) for m in move_dest_to_link]
r.write(vals)
moves += r
return moves
def _new_out_picking(self, old_picking):
new_picking = old_picking.copy({
'move_lines': [],
'picking_type_id': self.template_id.out_type_id.id,
'state': 'draft',
'origin': old_picking.name + ' ' + self.name,
'location_id': self.template_id.out_location_id.id,
'location_dest_id': self.template_id.out_location_dest_id.id,
'carrier_id': self.initial_out_picking_carrier_id.id,
'carrier_tracking_ref': False,
'carrier_price': False
})
new_picking.message_post_with_view('mail.message_origin_link',
values={'self': new_picking, 'origin': self},
subtype_id=self.env.ref('mail.mt_note').id)
return new_picking
def _new_out_move_vals(self, rma_line, new_picking, old_move):
return {
'name': self.name + ' OUT: ' + rma_line.product_id.name_get()[0][1],
'product_id': rma_line.product_id.id,
'product_uom_qty': rma_line.product_uom_qty,
'picking_id': new_picking.id,
'state': 'draft',
'location_id': self.template_id.out_location_id.id,
'location_dest_id': self.template_id.out_location_dest_id.id,
'picking_type_id': new_picking.picking_type_id.id,
'warehouse_id': new_picking.picking_type_id.warehouse_id.id,
'origin_returned_move_id': False,
'procure_method': self.template_id.out_procure_method,
'to_refund': self.template_id.out_to_refund,
}
def _new_out_moves(self, old_picking, new_picking, move_update):
lines = self.lines.filtered(lambda l: l.product_uom_qty >= 1)
if not lines:
raise UserError(_('You have no lines with positive quantity.'))
moves = self.env['stock.move']
for l in lines:
return_move = old_picking.move_lines.filtered(lambda ol: ol.state == 'done' and ol.product_id.id == l.product_id.id)[0]
copy_vals = self._new_out_move_vals(l, new_picking, return_move)
copy_vals.update(move_update)
moves += return_move.copy(copy_vals)
return moves
def _create_in_picking_stock_picking(self):
if not self.stock_picking_id or self.stock_picking_id.state != 'done':
raise UserError(_('You must have a completed stock picking for this RMA.'))
if not self.template_id.in_require_return:
group_id = self.stock_picking_id.group_id.id if self.stock_picking_id.group_id else 0
values = self.template_id._values_for_in_picking(self)
update = {'group_id': group_id}
return self._picking_from_values(values, update, update)
old_picking = self.stock_picking_id
new_picking = self._new_in_picking(old_picking)
self._new_in_moves(old_picking, new_picking, {})
return new_picking
def _create_out_picking_stock_picking(self):
if not self.stock_picking_id or self.stock_picking_id.state != 'done':
raise UserError(_('You must have a completed stock picking for this RMA.'))
if not self.template_id.out_require_return:
group_id = self.stock_picking_id.group_id.id if self.stock_picking_id.group_id else 0
values = self.template_id._values_for_out_picking(self)
update = {'group_id': group_id}
return self._picking_from_values(values, update, update)
old_picking = self.stock_picking_id
new_picking = self._new_out_picking(old_picking)
self._new_out_moves(old_picking, new_picking, {})
return new_picking
class RMALine(models.Model):
_name = 'rma.line'
_description = 'RMA Line'
rma_id = fields.Many2one('rma.rma', string='RMA')
product_id = fields.Many2one('product.product', 'Product')
product_uom_id = fields.Many2one('uom.uom', 'UOM')
product_uom_qty = fields.Float(string='QTY')
rma_template_usage = fields.Selection(related='rma_id.template_usage')
@api.onchange('product_id')
def _onchange_product_id(self):
for line in self:
line.product_uom_id = line.product_id.uom_id

View File

@@ -0,0 +1,26 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, models, _
class StockPicking(models.Model):
_inherit = 'stock.picking'
def send_to_shipper(self):
res = False
for pick in self.filtered(lambda p: not p.carrier_tracking_ref):
# deliver full order if no items are done.
pick_has_no_done = sum(pick.move_line_ids.mapped('qty_done')) == 0
if pick_has_no_done:
pick._rma_complete()
res = super(StockPicking, pick).send_to_shipper()
if pick_has_no_done:
pick._rma_complete_reverse()
return res
def _rma_complete(self):
for line in self.move_line_ids:
line.qty_done = line.product_uom_qty
def _rma_complete_reverse(self):
self.move_line_ids.write({'qty_done': 0.0})

View File

@@ -0,0 +1,10 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"manage_rma stock","manage rma","model_rma_rma","stock.group_stock_user",1,1,1,1
"manage_rma_line stock","manage rma line","model_rma_line","stock.group_stock_user",1,1,1,1
"manage_rma_template stock","manage rma template","model_rma_template","stock.group_stock_manager",1,1,1,1
"manage_rma_tag stock","manage rma tag","model_rma_tag","stock.group_stock_manager",1,1,1,1
"access_rma_template stock","access rma template","model_rma_template","stock.group_stock_user",1,1,0,0
"access_rma_tag stock","access rma tag","model_rma_tag","stock.group_stock_user",1,0,0,0
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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 manage_rma stock manage rma model_rma_rma stock.group_stock_user 1 1 1 1
3 manage_rma_line stock manage rma line model_rma_line stock.group_stock_user 1 1 1 1
4 manage_rma_template stock manage rma template model_rma_template stock.group_stock_manager 1 1 1 1
5 manage_rma_tag stock manage rma tag model_rma_tag stock.group_stock_manager 1 1 1 1
6 access_rma_template stock access rma template model_rma_template stock.group_stock_user 1 1 0 0
7 access_rma_tag stock access rma tag model_rma_tag stock.group_stock_user 1 0 0 0
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

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Multi Company -->
<record id="rma_rma_company_rule" model="ir.rule">
<field name="name">RMA: RMA</field>
<field name="model_id" ref="model_rma_rma"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',
('company_id', '=', False),
('company_id', 'child_of', [user.company_id.id]),
]</field>
</record>
<record id="rma_template_company_rule" model="ir.rule">
<field name="name">RMA: RMA Template</field>
<field name="model_id" ref="model_rma_template"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',
('company_id', '=', False),
('company_id', 'child_of', [user.company_id.id]),
]</field>
</record>
<!-- Portal Access Rules -->
<record id="rma_rule_portal" model="ir.rule">
<field name="name">Portal Personal RMAs</field>
<field name="model_id" ref="rma.model_rma_rma"/>
<field name="domain_force">[('partner_id','child_of',[user.commercial_partner_id.id])]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="perm_unlink" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_read" eval="True"/>
<field name="perm_create" eval="False"/>
</record>
<record id="rma_line_rule_portal" model="ir.rule">
<field name="name">Portal RMA Line</field>
<field name="model_id" ref="rma.model_rma_line"/>
<field name="domain_force">[('rma_id.partner_id','child_of',[user.commercial_partner_id.id])]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="perm_unlink" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_read" eval="True"/>
<field name="perm_create" eval="False"/>
</record>
</data>
</odoo>

3
rma/tests/__init__.py Normal file
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

246
rma/tests/test_rma.py Normal file
View File

@@ -0,0 +1,246 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo.tests import common
from odoo.exceptions import UserError, ValidationError
import logging
_logger = logging.getLogger(__name__)
class TestRMA(common.TransactionCase):
def setUp(self):
super(TestRMA, self).setUp()
self.product1 = self.env.ref('product.product_product_24')
self.template_missing = self.env.ref('rma.template_missing_item')
self.template_return = self.env.ref('rma.template_picking_return')
self.partner1 = self.env.ref('base.res_partner_2')
self.user1 = self.env.ref('base.user_demo')
def test_00_basic_rma(self):
self.template_missing.responsible_user_ids += self.user1
self.template_missing.usage = False
rma = self.env['rma.rma'].create({
'template_id': self.template_missing.id,
'partner_id': self.partner1.id,
'partner_shipping_id': self.partner1.id,
})
self.assertEqual(rma.state, 'draft')
self.assertTrue(rma.activity_ids)
self.assertEqual(rma.activity_ids.user_id, self.user1)
rma_line = self.env['rma.line'].create({
'rma_id': rma.id,
'product_id': self.product1.id,
'product_uom_id': self.product1.uom_id.id,
'product_uom_qty': 2.0,
})
rma.action_confirm()
# Should have made pickings
self.assertEqual(rma.state, 'confirmed')
# No inbound picking
self.assertFalse(rma.in_picking_id)
# Good outbound picking
self.assertTrue(rma.out_picking_id)
self.assertEqual(rma_line.product_id, rma.out_picking_id.move_lines.product_id)
self.assertEqual(rma_line.product_uom_qty, rma.out_picking_id.move_lines.product_uom_qty)
with self.assertRaises(UserError):
rma.action_done()
rma.out_picking_id.move_lines.quantity_done = 2.0
rma.out_picking_id.action_done()
rma.action_done()
self.assertEqual(rma.state, 'done')
def test_10_rma_cancel(self):
self.template_missing.usage = False
rma = self.env['rma.rma'].create({
'template_id': self.template_missing.id,
'partner_id': self.partner1.id,
'partner_shipping_id': self.partner1.id,
})
self.assertEqual(rma.state, 'draft')
rma_line = self.env['rma.line'].create({
'rma_id': rma.id,
'product_id': self.product1.id,
'product_uom_id': self.product1.uom_id.id,
'product_uom_qty': 2.0,
})
rma.action_confirm()
# Good outbound picking
self.assertEqual(rma.out_picking_id.move_lines.state, 'assigned')
rma.action_cancel()
self.assertEqual(rma.out_picking_id.move_lines.state, 'cancel')
def test_20_picking_rma(self):
type_out = self.env.ref('stock.picking_type_out')
location = self.env.ref('stock.stock_location_stock')
location_customer = self.env.ref('stock.stock_location_customers')
adj = self.env['stock.inventory'].create({
'name': 'Adjust Out',
'product_ids': [(4, self.product1.id)],
})
adj.action_start()
adj.line_ids.write({
'product_qty': 0.0,
})
adj.action_validate()
# Adjust in a single serial
self.product1.tracking = 'serial'
# Need to ensure this is the only quant that can be reserved for this move.
lot = self.env['stock.production.lot'].create({
'product_id': self.product1.id,
'name': 'X1000',
'product_uom_id': self.product1.uom_id.id,
'company_id': self.env.user.company_id.id,
})
adj = self.env['stock.inventory'].create({
'name': 'Initial',
'product_ids': [(4, self.product1.id)],
})
adj.action_start()
if not adj.line_ids:
_ = self.env['stock.inventory.line'].create({
'inventory_id': adj.id,
'product_id': self.product1.id,
'location_id': self.env.ref('stock.warehouse0').lot_stock_id.id,
})
adj.line_ids.write({
'product_qty': 1.0,
'prod_lot_id': lot.id,
})
adj.action_validate()
self.assertEqual(self.product1.qty_available, 1.0)
self.assertTrue(lot.quant_ids)
# Test some internals in Odoo 12.0
lot_internal_quants = lot.quant_ids.filtered(lambda q: q.location_id.usage in ['internal', 'transit'])
self.assertEqual(len(lot_internal_quants), 1)
self.assertEqual(lot_internal_quants.mapped('quantity'), [1.0])
# Re-compute qty as it does not depend on anything.
lot._product_qty()
self.assertEqual(lot.product_qty, 1.0)
# Create initial picking that will be returned by RMA
picking_out = self.env['stock.picking'].create({
'partner_id': self.partner1.id,
'name': 'testpicking',
'picking_type_id': type_out.id,
'location_id': location.id,
'location_dest_id': location_customer.id,
})
self.env['stock.move'].create({
'name': self.product1.name,
'product_id': self.product1.id,
'product_uom_qty': 1.0,
'product_uom': self.product1.uom_id.id,
'picking_id': picking_out.id,
'location_id': location.id,
'location_dest_id': location_customer.id,
})
picking_out.with_context(planned_picking=True).action_confirm()
# Try to RMA item not delivered yet
rma = self.env['rma.rma'].create({
'template_id': self.template_return.id,
'partner_id': self.partner1.id,
'partner_shipping_id': self.partner1.id,
'stock_picking_id': picking_out.id,
})
self.assertEqual(rma.state, 'draft')
wizard = self.env['rma.picking.make.lines'].create({
'rma_id': rma.id,
})
wizard.line_ids.product_uom_qty = 1.0
wizard.add_lines()
self.assertEqual(len(rma.lines), 1)
# Make sure that we cannot 'return' if we cannot 'reverse' a stock move
# (this is what `in_require_return` and `out_require_return` do on `rma.template`)
with self.assertRaises(UserError):
rma.action_confirm()
# Finish our original picking
picking_out.action_assign()
self.assertEqual(picking_out.state, 'assigned')
# The only lot should be reserved, so we shouldn't get an exception finishing the transfer.
picking_out.move_line_ids.write({
'qty_done': 1.0,
})
picking_out.button_validate()
self.assertEqual(picking_out.state, 'done')
# Now we can 'return' that picking
rma.action_confirm()
self.assertEqual(rma.in_picking_id.state, 'assigned')
pack_opt = rma.in_picking_id.move_line_ids[0]
self.assertTrue(pack_opt)
# We cannot check this directly anymore. Instead just try to return the same lot and make sure you can.
# self.assertEqual(pack_opt.lot_id, lot)
with self.assertRaises(UserError):
rma.action_done()
pack_opt.qty_done = 1.0
with self.assertRaises(UserError):
# require a lot
rma.in_picking_id.button_validate()
pack_opt.lot_id = lot
rma.in_picking_id.button_validate()
rma.action_done()
# Ensure that the same lot was in fact returned into our destination inventory
quant = self.env['stock.quant'].search([('product_id', '=', self.product1.id), ('location_id', '=', location.id)])
self.assertEqual(len(quant), 1)
self.assertEqual(quant.lot_id, lot)
# Make another RMA for the same picking
rma2 = self.env['rma.rma'].create({
'template_id': self.template_return.id,
'partner_id': self.partner1.id,
'partner_shipping_id': self.partner1.id,
'stock_picking_id': picking_out.id,
})
wizard = self.env['rma.picking.make.lines'].create({
'rma_id': rma2.id,
})
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
# TODO: Investigate
# rma2.in_picking_id.move_line_ids.write({
# 'lot_id': lot.id
# })
# Existing lot cannot be re-used.
# TODO: Investigate
# It appears that in Odoo 13 You can move the lot again...
# 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,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_move_form_rma" model="ir.ui.view">
<field name="name">account.move.form.rma</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='payments_info_group']" position="after">
<group string="RMAs" name="rma" attrs="{'invisible': [('type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))]}">
<field name="rma_ids"/>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,267 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="portal_my_home_menu_rma" name="Portal layout : RMA menu entries"
inherit_id="portal.portal_breadcrumbs" priority="20">
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
<li t-if="page_name == 'rma' or rma or rma_template" t-attf-class="breadcrumb-item #{'active ' if not rma else ''}">
<a t-if="rma" t-attf-href="/my/rma?{{ keep_query() }}">RMA</a>
<t t-else="">RMA</t>
</li>
<li t-if="rma" class="breadcrumb-item active">
<t t-esc="rma.name"/>
</li>
<li t-if="rma_template" class="breadcrumb-item active">
<a t-attf-href="/my/rma/new/#{rma_template.id}">
New "<t t-esc="rma_template.name"/>"
</a>
</li>
</xpath>
</template>
<template id="portal_my_home_rma" name="Portal My Home : RMA entry" inherit_id="portal.portal_my_home"
priority="20">
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
<t t-call="portal.portal_docs_entry">
<t t-set="title">RMA</t>
<t t-set="url" t-value="'/my/rma'"/>
<t t-set="count" t-value="rma_count"/>
</t>
</xpath>
</template>
<template id="portal_my_rma" name="Portal: My RMAs">
<t t-call="portal.portal_layout">
<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">
<tr>
<td>
<a t-attf-href="/my/rma/#{rma.id}?#{keep_query()}">
<t t-esc="rma.name"/>
</a>
</td>
<td>
<span t-field="rma.create_date"/>
</td>
<td>
<span t-field="rma.state"/>
</td>
</tr>
</t>
</tbody>
</t>
<div t-if="rma_templates" class="row">
<div class="col-12">
<button class="create-rma btn btn-primary mt8" data-toggle="modal" data-target="#create-rma">Create New RMA</button>
</div>
</div>
<div t-if="rma_templates" role="dialog" class="modal fade" id="create-rma">
<div class="modal-dialog">
<form id="create" class="modal-content">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<header class="modal-header">
<h4 class="modal-title">Create RMA</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&amp;times;</button>
</header>
<main class="modal-body">
<ul class="list-group">
<li class="list-group-item" t-foreach="rma_templates" t-as="template">
<a t-attf-href="/my/rma/new/#{template.id}">
<span t-esc="template.name"/>
</a>
</li>
</ul>
</main>
<footer class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Cancel</button>
</footer>
</form>
</div>
</div>
</t>
</template>
<template id="portal_my_rma_rma" name="Portal: My RMA">
<t t-call="portal.portal_layout">
<div id="optional_placeholder"></div>
<div class="container">
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-lg-12">
<h4>
<t t-if="rma.state == 'draft'">
Submitted -
</t>
<span t-esc="rma.name"/> - <span t-field="rma.state"/>
</h4>
</div>
</div>
</div>
<div class="card-body">
<div class="rma-details">
<div class="mb8">
<strong>Submitted Date:</strong>
<span t-field="rma.create_date" t-options='{"widget": "date"}'/>
</div>
<div t-if="rma.validity_date" class="mb8">
<strong>Validity Date:</strong>
<span t-attf-class="#{'text-danger' if rma.validity_date &lt; current_date else 'text-warning'}"
t-field="rma.validity_date" t-options='{"widget": "date"}'/>
</div>
<div t-if="rma.stock_picking_id" class="mb8">
<strong>Transfer:</strong>
<span t-esc="rma.stock_picking_id.name"/>
</div>
</div>
<div class="row">
<div class="col-lg-10">
<strong>Product</strong>
</div>
<div class="col-lg-2 text-right">
<strong>Quantity</strong>
</div>
</div>
<t t-foreach="rma.lines" t-as="line">
<div class="row purchases_vertical_align">
<div class="col-lg-3 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-7">
<span t-esc="line.product_id.name"/>
</div>
<div class="col-lg-2 text-right">
<span t-esc="line.product_uom_qty"/>
</div>
</div>
</t>
<!-- Return Label -->
<t t-if="rma.in_label_url">
<hr/>
<a t-attf-href="#{rma.in_label_url}">Download Your Return Label</a>
</t>
<!-- Customer Instructions -->
<t t-if="rma.customer_description">
<hr/>
<div class="row">
<div class="col-12">
<h3>Customer Instructions</h3>
<div t-raw="rma.customer_description"/>
</div>
</div>
</t>
<hr/>
<!-- chatter -->
<div id="rma_communication" class="mt-4">
<h2>Communication</h2>
<t t-call="portal.message_thread">
<t t-set="object" t-value="rma"/>
</t>
</div>
</div>
</div>
</div>
<div class="oe_structure mb32"/>
</t>
</template>
<template id="portal_rma_error" name="RMA Error">
<t t-if="error">
<div class="alert alert-danger text-left mt16" role="alert">
<t t-esc="error"/>
</div>
</t>
</template>
<!-- New -->
<template id="portal_new_stock_picking" name="New Transfer 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="pickings" class="list-group">
<li class="list-group-item" t-foreach="pickings" t-as="p">
<a t-attf-href="/my/rma/new/#{rma_template.id}/res/#{p.id}">
<span t-esc="p.name"/>
</a>
</li>
</ul>
<p t-if="not pickings and not picking">No Transfers to choose from.</p>
<form t-if="picking" method="post" t-attf-action="/my/rma/new/#{rma_template.id}/res/#{picking.id}">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="row">
<div class="col-lg-2">
<strong>Product</strong>
</div>
<div class="col-lg-4">
<strong>Description</strong>
</div>
<div class="col-lg-2 text-right">
<strong>Quantity Ordered</strong>
</div>
<div class="col-lg-2 text-right">
<strong>Quantity Delivered</strong>
</div>
<div class="col-lg-2 text-right">
<strong>Quantity to Return</strong>
</div>
</div>
<t t-foreach="picking.move_lines" t-as="line">
<div class="row purchases_vertical_align">
<div class="col-lg-2 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-4">
<span t-esc="line.product_id.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.product_qty"/>
</div>
<div class="col-lg-2 text-right">
<input type="text" t-attf-name="move_#{line.id}" class="form-control"/>
</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>

286
rma/views/rma_views.xml Normal file
View File

@@ -0,0 +1,286 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_rma_rma_form" model="ir.ui.view">
<field name="name">rma.rma.form</field>
<field name="model">rma.rma</field>
<field name="arch" type="xml">
<form string="RMA" class="oe_form_nomargin">
<header>
<button name="action_confirm" string="Confirm" class="btn-primary" type="object" attrs="{'invisible': [('state', '!=', 'draft')]}"/>
<button name="action_done" string="Done" class="btn-primary" type="object" attrs="{'invisible': [('state', '!=', 'confirmed')]}"/>
<button name="action_draft" string="Set Draft" class="btn-default" type="object" attrs="{'invisible': [('state', 'not in', ('cancel', 'expired'))]}"/>
<button name="action_cancel" string="Cancel" class="btn-default" type="object" attrs="{'invisible': [('state', 'in', ('draft', 'done'))]}"/>
<field name="state" widget="statusbar" on_change="1" modifiers="{'readonly': true}"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="open_stock_picking_rmas" icon="fa-cubes"
type="object" attrs="{'invisible': ['|', ('stock_picking_id', '=', False), ('stock_picking_rma_count', '&lt;=', 1)]}">
<field name="stock_picking_rma_count" string="Pick RMAs" widget="statinfo" />
</button>
</div>
<div class="oe_title">
<h1>
<field name="name" readonly="1" modifiers="{'readonly': true, 'required': true}"/>
</h1>
</div>
<group>
<group>
<field name="template_usage" invisible="1"/>
<field name="template_create_in_picking" invisible="1"/>
<field name="template_create_out_picking" invisible="1"/>
<field name="template_id" options="{'no_create': True}" attrs="{'readonly': [('state', 'in', ('confirmed', 'done', 'cancel'))]}"/>
<field name="stock_picking_id" options="{'no_create': True}" attrs="{'invisible': [('template_usage', '!=', 'stock_picking')], 'required': [('template_usage', '=', 'stock_picking')], 'readonly': [('state', 'in', ('confirmed', 'done', 'cancel'))]}"/>
<br/>
<button string="Add lines" type="object" name="action_add_picking_lines" attrs="{'invisible': ['|', ('stock_picking_id', '=', False), ('state', '!=', 'draft')]}"/>
</group>
<group>
<field name="validity_date"/>
<field name="tag_ids" widget="many2many_tags" placeholder="Tags" options="{'no_create': True}"/>
<field name="partner_id" options="{'no_create_edit': True}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="partner_shipping_id" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="initial_in_picking_carrier_id" attrs="{'invisible': [('template_create_in_picking', '=', False)]}"/>
<field name="initial_out_picking_carrier_id" attrs="{'invisible': [('template_create_out_picking', '=', False)]}"/>
<field name="company_id" options="{'no_create': True}"/>
</group>
<group name="invoices" string="Invoices" colspan="4" attrs="{'invisible': [('invoice_ids', '=', False), ('state', '!=', 'done')]}">
<field name="invoice_ids" nolabel="1"/>
</group>
</group>
<notebook>
<page string="Internal Instructions">
<field name="description" readonly="1"/>
</page>
<page string="Customer Instructions">
<field name="customer_description" readonly="1"/>
</page>
</notebook>
<notebook>
<page string="RMA Lines">
<field name="lines" attrs="{'readonly': [('state', '!=', 'draft')]}">
<tree editable="bottom">
<field name="rma_template_usage" invisible="1"/>
<field name="product_id" attrs="{'readonly': [('rma_template_usage', '!=', False)]}"/>
<field name="product_uom_qty" attrs="{'readonly': [('rma_template_usage', '!=', False)]}"/>
<field name="product_uom_id" attrs="{'readonly': [('rma_template_usage', '!=', False)]}"/>
</tree>
</field>
</page>
</notebook>
</sheet>
<sheet attrs="{'invisible': [('in_picking_id', '=', False)]}">
<header>
<field name="in_picking_state" widget="statusbar" on_change="1" modifiers="{'readonly': true}"/>
</header>
<div class="oe_title">
<h2>Inbound Picking:</h2>
<h1>
<field name="in_picking_id" readonly="1" modifiers="{'readonly': true}"/>
</h1>
<p>
<field name="in_label_url" attrs="{'invisible': [('in_label_url', '=', False)]}"/>
</p>
</div>
<group>
<group>
<field name="in_picking_carrier_id" string="Carrier"/>
<field name="in_carrier_tracking_ref" string="Tracking"/>
<button string="Generate Label" type="object" name="action_in_picking_send_to_shipper" attrs="{'invisible': ['|', ('in_carrier_tracking_ref', '!=', False), ('in_picking_carrier_id', '=', False)]}"/>
</group>
</group>
</sheet>
<sheet attrs="{'invisible': [('out_picking_id', '=', False)]}">
<header>
<field name="out_picking_state" widget="statusbar" on_change="1" modifiers="{'readonly': true}"/>
</header>
<div class="oe_title">
<h2>Outbound Picking:</h2>
<h1>
<field name="out_picking_id" readonly="1" modifiers="{'readonly': true}"/>
</h1>
</div>
<group>
<group>
<field name="out_picking_carrier_id" string="Carrier"/>
<field name="out_carrier_tracking_ref" string="Tracking"/>
</group>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="activity_ids" widget="mail_activity"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="view_rma_rma_tree" model="ir.ui.view">
<field name="name">rma.rma.tree</field>
<field name="model">rma.rma</field>
<field name="arch" type="xml">
<tree decoration-info="state == 'draft'" decoration-muted="state in ('cancel', 'done')" decoration-warning="validity_date and validity_date &lt; current_date">
<field name="name"/>
<field name="template_id"/>
<field name="stock_picking_id"/>
<field name="partner_id"/>
<field name="create_date"/>
<field name="validity_date"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="view_rma_rma_search" model="ir.ui.view">
<field name="name">rma.rma.search</field>
<field name="model">rma.rma</field>
<field name="arch" type="xml">
<search string="Search RMA">
<field name="name"/>
<field name="partner_id"/>
<field name="template_id"/>
<field name="stock_picking_id"/>
<separator/>
<filter string="New" name="new" domain="[('state', '=', 'draft')]"/>
<filter string="Confirmed" name="confirmed" domain="[('state', '=', 'confirmed')]"/>
<filter string="Expired" name="expired" domain="[('validity_date', '!=', False),('validity_date', '&lt;', datetime.datetime.now())]"/>
<group expand="0" name="group_by" string="Group By">
<filter name="group_state" string="State" domain="[]" context="{'group_by': 'state'}"/>
<filter name="group_template" string="Template" domain="[]" context="{'group_by': 'template_id'}"/>
</group>
</search>
</field>
</record>
<record id="view_rma_template_form" model="ir.ui.view">
<field name="name">rma.template.form</field>
<field name="model">rma.template</field>
<field name="arch" type="xml">
<form string="RMA Template" class="oe_form_nomargin">
<sheet>
<div class="oe_title">
<h1>
<field name="name"/>
</h1>
</div>
<group>
<group>
<field name="usage"/>
<field name="valid_days"/>
<field name="automatic_expire"/>
<field name="company_id" options="{'no_create': True}"/>
<field name="portal_ok"/>
<field name="invoice_done" help="This feature is implemented in specific RMA types automatically when enabled."/>
</group>
<group>
<field name="responsible_user_ids" domain="[('share', '=', False)]" widget="many2many_tags"/>
</group>
</group>
<group>
<group>
<field name="create_in_picking"/>
<field name="in_type_id" attrs="{'invisible': [('create_in_picking', '=', False)]}"/>
<field name="in_location_id" attrs="{'invisible': [('create_in_picking', '=', False)]}"/>
<field name="in_location_dest_id" attrs="{'invisible': [('create_in_picking', '=', False)]}"/>
<field name="in_carrier_id" attrs="{'invisible': [('create_in_picking', '=', False)]}"/>
<field name="in_require_return" attrs="{'invisible': [('create_in_picking', '=', False)]}"/>
<field name="in_procure_method" attrs="{'invisible': [('create_in_picking', '=', False)]}"/>
<field name="in_to_refund" attrs="{'invisible': [('create_in_picking', '=', False)]}"/>
</group>
<group>
<field name="create_out_picking"/>
<field name="out_type_id" attrs="{'invisible': [('create_out_picking', '=', False)]}"/>
<field name="out_location_id" attrs="{'invisible': [('create_out_picking', '=', False)]}"/>
<field name="out_location_dest_id" attrs="{'invisible': [('create_out_picking', '=', False)]}"/>
<field name="out_carrier_id" attrs="{'invisible': [('create_out_picking', '=', False)]}"/>
<field name="out_require_return" attrs="{'invisible': [('create_out_picking', '=', False)]}"/>
<field name="out_procure_method" attrs="{'invisible': [('create_out_picking', '=', False)]}"/>
<field name="out_to_refund" attrs="{'invisible': [('create_out_picking', '=', False)]}"/>
</group>
</group>
<notebook>
<page string="Internal Instructions">
<field name="description"/>
</page>
<page string="Customer Instructions">
<field name="customer_description"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_rma_template_tree" model="ir.ui.view">
<field name="name">rma.template.tree</field>
<field name="model">rma.template</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="usage"/>
<field name="create_in_picking"/>
<field name="create_out_picking"/>
</tree>
</field>
</record>
<record id="action_rma_rma" model="ir.actions.act_window">
<field name="name">RMA</field>
<field name="res_model">rma.rma</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem
action="action_rma_rma"
name="RMA"
id="menu_rma"
web_icon="fa fa-cubes,#FFFFFF,#EB5A46"
sequence="10"
/>
<menuitem
action="action_rma_rma"
id="menu_rma_rmas"
parent="menu_rma"
sequence="10"
/>
<menuitem
id="menu_rma_configuration"
name="Configuration"
parent="menu_rma"
sequence="100"
/>
<record id="action_rma_tag_form" model="ir.actions.act_window">
<field name="name">RMA Tag</field>
<field name="res_model">rma.tag</field>
<field name="view_mode">tree,form</field>
</record>
<record id="action_rma_template_form" model="ir.actions.act_window">
<field name="name">RMA Templates</field>
<field name="res_model">rma.template</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem
id="menu_rma_configuation_rma_template_form"
name="Templates"
action="action_rma_template_form"
parent="menu_rma_configuration"
sequence="21"
/>
<menuitem
id="menu_rma_configuation_rma_tag_form"
name="Tags"
action="action_rma_tag_form"
parent="menu_rma_configuration"
sequence="25"
/>
</odoo>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_picking_withcarrier_out_form" model="ir.ui.view">
<field name="name">delivery.stock.picking_withcarrier.form.view</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="delivery.view_picking_withcarrier_out_form" />
<field name="arch" type="xml">
<xpath expr="//button[@name='send_to_shipper']" position="attributes">
<attribute name="attrs">{'invisible':['|','|','|',('carrier_tracking_ref','!=',False),('delivery_type','in', ['fixed', 'base_on_rule']),('delivery_type','=',False)]}</attribute>
</xpath>
</field>
</record>
</odoo>

3
rma/wizard/__init__.py Normal file
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

59
rma/wizard/rma_lines.py Normal file
View File

@@ -0,0 +1,59 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models
class RMAPickingMakeLines(models.TransientModel):
_name = 'rma.picking.make.lines'
_description = 'Add Picking Lines'
rma_id = fields.Many2one('rma.rma', string='RMA')
line_ids = fields.One2many('rma.picking.make.lines.line', 'rma_make_lines_id', string='Lines')
@api.model
def create(self, vals):
maker = super(RMAPickingMakeLines, self).create(vals)
maker._create_lines()
return maker
def _line_values(self, move):
return {
'rma_make_lines_id': self.id,
'product_id': move.product_id.id,
'qty_ordered': move.product_uom_qty,
'qty_delivered': move.product_qty,
'product_uom_qty': 0.0,
'product_uom_id': move.product_uom.id,
}
def _create_lines(self):
make_lines_obj = self.env['rma.picking.make.lines.line']
if self.rma_id.template_usage == 'stock_picking' and self.rma_id.stock_picking_id:
for l in self.rma_id.stock_picking_id.move_lines:
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)
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 RMAPickingMakeLinesLine(models.TransientModel):
_name = 'rma.picking.make.lines.line'
_description = 'RMA Picking Make Lines Line'
rma_make_lines_id = fields.Many2one('rma.picking.make.lines')
product_id = fields.Many2one('product.product', string="Product")
qty_ordered = fields.Float(string='Ordered')
qty_delivered = fields.Float(string='Delivered')
product_uom_qty = fields.Float(string='QTY')
product_uom_id = fields.Many2one('uom.uom', 'UOM')

View File

@@ -0,0 +1,40 @@
<?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.picking.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">
<field name="product_id" readonly="1"/>
<field name="qty_ordered" readonly="1"/>
<field name="qty_delivered" 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.picking.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>