mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Initial commit of rma and rma_sale for Odoo 11.0
Big changes to tests (because of the ways stock has changed) and allow activities to be set on RMAs. Other refactors to reduce code duplication between picking and so returns.
This commit is contained in:
3
rma/__init__.py
Normal file
3
rma/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from . import controllers
|
||||||
|
from . import models
|
||||||
|
from . import wizard
|
||||||
27
rma/__manifest__.py
Normal file
27
rma/__manifest__.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# © 2018 Hibou Corp.
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'Hibou RMAs',
|
||||||
|
'version': '11.0.1.0.0',
|
||||||
|
'category': 'Warehouse',
|
||||||
|
'author': "Hibou Corp.",
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
'website': 'https://hibou.io/',
|
||||||
|
'depends': [
|
||||||
|
'stock',
|
||||||
|
'delivery',
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'data/ir_sequence_data.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/rma_views.xml',
|
||||||
|
'views/stock_picking_views.xml',
|
||||||
|
'wizard/rma_lines_views.xml',
|
||||||
|
],
|
||||||
|
'demo': [
|
||||||
|
'data/rma_demo.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
}
|
||||||
1
rma/controllers/__init__.py
Normal file
1
rma/controllers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import main
|
||||||
49
rma/controllers/main.py
Normal file
49
rma/controllers/main.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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)
|
||||||
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>
|
||||||
44
rma/data/rma_demo.xml
Normal file
44
rma/data/rma_demo.xml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?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_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>
|
||||||
2
rma/models/__init__.py
Normal file
2
rma/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import rma
|
||||||
|
from . import stock_picking
|
||||||
457
rma/models/rma.py
Normal file
457
rma/models/rma.py
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from ..controllers.main import create_hmac
|
||||||
|
from datetime import timedelta, datetime
|
||||||
|
from time import mktime
|
||||||
|
|
||||||
|
|
||||||
|
class RMATemplate(models.Model):
|
||||||
|
_name = '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')
|
||||||
|
|
||||||
|
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', oldname='in_to_refund_so')
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
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': self.in_carrier_id.id if self.in_carrier_id else False,
|
||||||
|
'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],
|
||||||
|
}
|
||||||
|
|
||||||
|
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': self.out_carrier_id.id if self.out_carrier_id else False,
|
||||||
|
'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],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 = ['mail.thread', 'mail.activity.mixin']
|
||||||
|
_description = 'RMA'
|
||||||
|
_order = 'id desc'
|
||||||
|
|
||||||
|
name = fields.Char(string='Number', copy=False)
|
||||||
|
state = fields.Selection([
|
||||||
|
('draft', 'New'),
|
||||||
|
('confirmed', 'Confirmed'),
|
||||||
|
('done', 'Done'),
|
||||||
|
('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)
|
||||||
|
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')
|
||||||
|
|
||||||
|
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')
|
||||||
|
out_picking_carrier_id = fields.Many2one('delivery.carrier', related='out_picking_id.carrier_id')
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
@api.onchange('template_usage')
|
||||||
|
@api.multi
|
||||||
|
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')
|
||||||
|
@api.multi
|
||||||
|
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')
|
||||||
|
@api.multi
|
||||||
|
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.multi
|
||||||
|
@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
|
||||||
|
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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')
|
||||||
|
|
||||||
|
result = super(RMA, self).create(vals)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
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})
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
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'})
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def action_cancel(self):
|
||||||
|
for rma in self:
|
||||||
|
rma.in_picking_id.action_cancel()
|
||||||
|
rma.out_picking_id.action_cancel()
|
||||||
|
self.write({'state': 'cancel'})
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def action_draft(self):
|
||||||
|
self.filtered(lambda l: l.state == 'cancel').write({
|
||||||
|
'state': 'draft', 'in_picking_id': False, 'out_picking_id': False})
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
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
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
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()
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
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
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
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.template_id.in_carrier_id.id if self.template_id.in_carrier_id else 0,
|
||||||
|
'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.template_id.out_carrier_id.id if self.template_id.out_carrier_id else 0,
|
||||||
|
'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'
|
||||||
|
|
||||||
|
rma_id = fields.Many2one('rma.rma', string='RMA')
|
||||||
|
product_id = fields.Many2one('product.product', 'Product')
|
||||||
|
product_uom_id = fields.Many2one('product.uom', 'UOM')
|
||||||
|
product_uom_qty = fields.Float(string='QTY')
|
||||||
|
rma_template_usage = fields.Selection(related='rma_id.template_usage')
|
||||||
|
|
||||||
|
@api.onchange('product_id')
|
||||||
|
@api.multi
|
||||||
|
def _onchange_product_id(self):
|
||||||
|
for line in self:
|
||||||
|
line.product_uom_id = line.product_id.uom_id
|
||||||
13
rma/models/stock_picking.py
Normal file
13
rma/models/stock_picking.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from odoo import api, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class StockPicking(models.Model):
|
||||||
|
_inherit = 'stock.picking'
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def send_to_shipper(self):
|
||||||
|
res = False
|
||||||
|
for pick in self.filtered(lambda p: not p.carrier_tracking_ref):
|
||||||
|
res = super(StockPicking, pick).send_to_shipper()
|
||||||
|
return res
|
||||||
7
rma/security/ir.model.access.csv
Normal file
7
rma/security/ir.model.access.csv
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||||
|
"manage_rma 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
|
||||||
|
1
rma/tests/__init__.py
Normal file
1
rma/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_rma
|
||||||
222
rma/tests/test_rma.py
Normal file
222
rma/tests/test_rma.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
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')
|
||||||
|
|
||||||
|
def test_00_basic_rma(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()
|
||||||
|
# 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')
|
||||||
|
self.product1.tracking = 'serial'
|
||||||
|
|
||||||
|
# Need to ensure this is the only quant that can be reserved for this move.
|
||||||
|
adj = self.env['stock.inventory'].create({
|
||||||
|
'name': 'Test',
|
||||||
|
'location_id': location.id,
|
||||||
|
'filter': 'product',
|
||||||
|
'product_id': self.product1.id,
|
||||||
|
})
|
||||||
|
lot = self.env['stock.production.lot'].create({
|
||||||
|
'product_id': self.product1.id,
|
||||||
|
'name': 'X100',
|
||||||
|
'product_uom_id': self.product1.uom_id.id,
|
||||||
|
})
|
||||||
|
self.assertFalse(lot.quant_ids)
|
||||||
|
self.assertEqual(lot.product_qty, 0.0)
|
||||||
|
|
||||||
|
adj.action_start()
|
||||||
|
self.assertTrue(adj.line_ids)
|
||||||
|
adj.line_ids.write({
|
||||||
|
'product_qty': 0.0,
|
||||||
|
})
|
||||||
|
|
||||||
|
self.env['stock.inventory.line'].create({
|
||||||
|
'inventory_id': adj.id,
|
||||||
|
'location_id': location.id,
|
||||||
|
'product_id': self.product1.id,
|
||||||
|
'product_uom_id': self.product1.uom_id.id,
|
||||||
|
'prod_lot_id': lot.id,
|
||||||
|
'product_qty': 1.0,
|
||||||
|
})
|
||||||
|
|
||||||
|
adj.action_done()
|
||||||
|
self.assertEqual(self.product1.qty_available, 1.0)
|
||||||
|
self.assertTrue(lot.quant_ids)
|
||||||
|
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.do_transfer()
|
||||||
|
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.do_transfer()
|
||||||
|
|
||||||
|
pack_opt.lot_id = lot
|
||||||
|
rma.in_picking_id.do_transfer()
|
||||||
|
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.do_transfer()
|
||||||
|
|
||||||
|
# Assign existing lot
|
||||||
|
rma2.in_picking_id.move_line_ids.write({
|
||||||
|
'lot_id': lot.id
|
||||||
|
})
|
||||||
|
|
||||||
|
# Existing lot cannot be re-used.
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
rma2.in_picking_id.action_done()
|
||||||
|
|
||||||
|
# RMA cannot be completed because the inbound picking state is confirmed
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
rma2.action_done()
|
||||||
275
rma/views/rma_views.xml
Normal file
275
rma/views/rma_views.xml
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
<?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', '!=', 'cancel')]}"/>
|
||||||
|
<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_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="company_id" invisible="1" options="{'no_create': True}" can_create="true" can_write="true" modifiers="{}"/>
|
||||||
|
</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 colors="blue:state == 'draft';gray:state in ('cancel', 'done');orange: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.tree</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 string="State" domain="[]" context="{'group_by': 'state'}"/>
|
||||||
|
<filter string="Template" domain="[]" context="{'group_by': 'Template'}"/>
|
||||||
|
</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"/>
|
||||||
|
</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_type">form</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_type">form</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_type">form</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>
|
||||||
1
rma/wizard/__init__.py
Normal file
1
rma/wizard/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import rma_lines
|
||||||
57
rma/wizard/rma_lines.py
Normal file
57
rma/wizard/rma_lines.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
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.ordered_qty,
|
||||||
|
'qty_delivered': move.product_uom_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))
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def add_lines(self):
|
||||||
|
rma_line_obj = self.env['rma.line']
|
||||||
|
for o in self:
|
||||||
|
lines = o.line_ids.filtered(lambda l: l.product_uom_qty > 0.0)
|
||||||
|
for l in lines:
|
||||||
|
rma_line_obj.create({
|
||||||
|
'rma_id': o.rma_id.id,
|
||||||
|
'product_id': l.product_id.id,
|
||||||
|
'product_uom_id': l.product_uom_id.id,
|
||||||
|
'product_uom_qty': l.product_uom_qty,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class RMAPickingMakeLinesLine(models.TransientModel):
|
||||||
|
_name = '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('product.uom', 'UOM')
|
||||||
38
rma/wizard/rma_lines_views.xml
Normal file
38
rma/wizard/rma_lines_views.xml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?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">
|
||||||
|
<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_type">form</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="view_id" ref="view_rma_add_lines_form" />
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user