mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
[MOV] rma: from Hibou Suite Enterprise for 13.0
This commit is contained in:
5
rma/__init__.py
Normal file
5
rma/__init__.py
Normal 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
31
rma/__manifest__.py
Normal 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,
|
||||||
|
}
|
||||||
4
rma/controllers/__init__.py
Normal file
4
rma/controllers/__init__.py
Normal 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
51
rma/controllers/main.py
Normal 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
137
rma/controllers/portal.py
Normal 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
17
rma/data/cron_data.xml
Normal 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>
|
||||||
14
rma/data/ir_sequence_data.xml
Normal file
14
rma/data/ir_sequence_data.xml
Normal 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
45
rma/demo/rma_demo.xml
Normal 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>
|
||||||
11
rma/migrations/13.0.1.2.0/post-migration.py
Normal file
11
rma/migrations/13.0.1.2.0/post-migration.py
Normal 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
5
rma/models/__init__.py
Normal 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
13
rma/models/account.py
Normal 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
582
rma/models/rma.py
Normal 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
|
||||||
26
rma/models/stock_picking.py
Normal file
26
rma/models/stock_picking.py
Normal 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})
|
||||||
10
rma/security/ir.model.access.csv
Normal file
10
rma/security/ir.model.access.csv
Normal 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
|
||||||
|
51
rma/security/rma_security.xml
Normal file
51
rma/security/rma_security.xml
Normal 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
3
rma/tests/__init__.py
Normal 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
246
rma/tests/test_rma.py
Normal 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()
|
||||||
17
rma/views/account_views.xml
Normal file
17
rma/views/account_views.xml
Normal 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>
|
||||||
267
rma/views/portal_templates.xml
Normal file
267
rma/views/portal_templates.xml
Normal 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">&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 < 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
286
rma/views/rma_views.xml
Normal 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', '<=', 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 < 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', '<', 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>
|
||||||
13
rma/views/stock_picking_views.xml
Normal file
13
rma/views/stock_picking_views.xml
Normal 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
3
rma/wizard/__init__.py
Normal 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
59
rma/wizard/rma_lines.py
Normal 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')
|
||||||
40
rma/wizard/rma_lines_views.xml
Normal file
40
rma/wizard/rma_lines_views.xml
Normal 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>
|
||||||
Reference in New Issue
Block a user