Initial commit of rma and rma_sale fro 10.0

This commit is contained in:
Jared Kipe
2018-08-14 11:05:30 -07:00
parent 53bfbc2e6b
commit 1ce48af5e4
23 changed files with 1129 additions and 0 deletions

3
rma/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models

27
rma/__manifest__.py Normal file
View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# © 2018 Hibou Corp.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Hibou RMAs',
'version': '10.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',
],
'demo': [
'data/rma_demo.xml',
],
'installable': True,
'application': True,
}

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
import main

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

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
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, str(str(a_attchment_id) + str(e_expires)), sha256).hexdigest()
def check_hmac(secret, hash_, a_attachment_id, e_expires):
myh = hmac.new(secret, str(str(a_attachment_id) + str(e_expires)), 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)

View File

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

12
rma/data/rma_demo.xml Normal file
View File

@@ -0,0 +1,12 @@
<?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>
</odoo>

3
rma/models/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import rma
from . import stock_picking

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

@@ -0,0 +1,266 @@
# -*- coding: utf-8 -*-
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([], 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_so = fields.Boolean(string='Inbound mark 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')
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,
}) 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,
}) 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']
_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)
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.filtered(lambda r: r.template_id.valid_days):
rma.validity_date = now + timedelta(days=rma.template_id.valid_days)
@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.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 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()
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

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from odoo import api, models, _
from odoo.exceptions import UserError
class StockPicking(models.Model):
_inherit = 'stock.picking'
@api.multi
def send_to_shipper(self):
if self.filtered(lambda p: p.carrier_tracking_ref):
raise UserError(_('Unable to send to shipper with existing tracking numbers.'))
return super(StockPicking, self).send_to_shipper()

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

2
rma/tests/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import test_rma

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

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
from odoo.tests import common
from odoo.exceptions import UserError
class TestRMA(common.TransactionCase):
def setUp(self):
super(TestRMA, self).setUp()
self.product1 = self.env.ref('product.product_product_24')
self.template1 = self.env.ref('rma.template_missing_item')
self.partner1 = self.env.ref('base.res_partner_2')
def test_00_basic_rma(self):
rma = self.env['rma.rma'].create({
'template_id': self.template1.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.action_done()
rma.action_done()
self.assertEqual(rma.state, 'done')
def test_10_rma_cancel(self):
rma = self.env['rma.rma'].create({
'template_id': self.template1.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')

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

@@ -0,0 +1,263 @@
<?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"/>
<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'))]}"/>
</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="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 &lt; current_date;">
<field name="name"/>
<field name="template_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"/>
<separator/>
<filter string="New" name="new" domain="[('state', '=', 'draft')]"/>
<filter string="Confirmed" name="confirmed" domain="[('state', '=', 'confirmed')]"/>
<filter string="Expired" name="expired" domain="[('validity_date', '!=', False),('validity_date', '&lt;', datetime.datetime.now())]"/>
<group expand="0" name="group_by" string="Group By">
<filter 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_so" 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)]}"/>
</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>

View File

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

3
rma_sale/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizard

24
rma_sale/__manifest__.py Normal file
View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# © 2018 Hibou Corp.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Hibou RMAs for Sale Orders',
'version': '10.0.1.0.0',
'category': 'Sale',
'author': "Hibou Corp.",
'license': 'AGPL-3',
'website': 'https://hibou.io/',
'depends': [
'rma',
'sale',
'sales_team',
],
'data': [
'security/ir.model.access.csv',
'views/rma_views.xml',
'wizard/rma_lines_views.xml',
],
'installable': True,
'application': False,
}

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import rma

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

@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class RMATemplate(models.Model):
_inherit = 'rma.template'
usage = fields.Selection(selection_add=[('sale_order', 'Sale Order')])
class RMA(models.Model):
_inherit = 'rma.rma'
sale_order_id = fields.Many2one('sale.order', string='Sale Order')
sale_order_rma_count = fields.Integer('Number of RMAs for this Sale Order', compute='_compute_sale_order_rma_count')
company_id = fields.Many2one('res.company', 'Company',
default=lambda self: self.env['res.company']._company_default_get('sale.order'))
@api.multi
@api.depends('sale_order_id')
def _compute_sale_order_rma_count(self):
for rma in self:
if rma.sale_order_id:
rma_data = self.read_group([('sale_order_id', '=', rma.sale_order_id.id), ('state', '!=', 'cancel')],
['sale_order_id'], ['sale_order_id'])
if rma_data:
rma.sale_order_rma_count = rma_data[0]['sale_order_id_count']
else:
rma.sale_order_rma_count = 0.0
@api.multi
def open_sale_order_rmas(self):
return {
'type': 'ir.actions.act_window',
'name': _('Sale Order RMAs'),
'res_model': 'rma.rma',
'view_mode': 'tree,form',
'context': {'search_default_sale_order_id': self[0].sale_order_id.id}
}
@api.onchange('template_usage')
@api.multi
def _onchange_template_usage(self):
res = super(RMA, self)._onchange_template_usage()
for rma in self.filtered(lambda rma: rma.template_usage != 'sale_order'):
rma.sale_order_id = False
return res
@api.onchange('sale_order_id')
@api.multi
def _onchange_sale_order_id(self):
for rma in self.filtered(lambda rma: rma.sale_order_id):
rma.partner_id = rma.sale_order_id.partner_id
rma.partner_shipping_id = rma.sale_order_id.partner_shipping_id
@api.multi
def action_add_so_lines(self):
make_line_obj = self.env['rma.sale.make.lines']
for rma in self:
lines = make_line_obj.create({
'rma_id': rma.id,
})
action = self.env.ref('rma_sale.action_rma_add_lines').read()[0]
action['res_id'] = lines.id
return action
def _create_in_picking_sale_order(self):
if not self.sale_order_id:
raise UserError(_('You must have a sale order for this RMA.'))
if not self.template_id.in_require_return:
group_id = self.sale_order_id.procurement_group_id.id if self.sale_order_id.procurement_group_id else 0
sale_id = self.sale_order_id.id if self.sale_order_id else 0
values = self.template_id._values_for_in_picking(self)
values.update({'sale_id': sale_id, 'group_id': group_id})
move_lines = []
for l1, l2, vals in values['move_lines']:
vals.update({'to_refund_so': self.template_id.in_to_refund_so, 'group_id': group_id})
move_lines.append((l1, l2, vals))
values['move_lines'] = move_lines
return self.env['stock.picking'].sudo().create(values)
lines = self.lines.filtered(lambda l: l.product_uom_qty >= 1)
if not lines:
raise UserError(_('You have no lines with positive quantity.'))
product_ids = lines.mapped('product_id.id')
old_picking = self._find_candidate_return_picking(product_ids, self.sale_order_id.picking_ids, self.template_id.in_location_id.id)
if not old_picking:
raise UserError('No eligible pickings were found to return (you can only return products from the same initial picking).')
new_picking = 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)
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]
return_move.copy({
'name': self.name + ' IN: ' + l.product_id.name_get()[0][1],
'product_id': l.product_id.id,
'product_uom_qty': l.product_uom_qty,
'picking_id': new_picking.id,
'state': 'draft',
'location_id': return_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': return_move.id,
'procure_method': self.template_id.in_procure_method,
'move_dest_id': False,
'to_refund_so': self.template_id.in_to_refund_so,
})
return new_picking
def _create_out_picking_sale_order(self):
if not self.sale_order_id:
raise UserError(_('You must have a sale order for this RMA.'))
if not self.template_id.out_require_return:
group_id = self.sale_order_id.procurement_group_id.id if self.sale_order_id.procurement_group_id else 0
sale_id = self.sale_order_id.id if self.sale_order_id else 0
values = self.template_id._values_for_out_picking(self)
values.update({'sale_id': sale_id, 'group_id': group_id})
move_lines = []
for l1, l2, vals in values['move_lines']:
vals.update({'group_id': group_id})
move_lines.append((l1, l2, vals))
values['move_lines'] = move_lines
return self.env['stock.picking'].sudo().create(values)
lines = self.lines.filtered(lambda l: l.product_uom_qty >= 1)
if not lines:
raise UserError(_('You have no lines with positive quantity.'))
product_ids = lines.mapped('product_id.id')
old_picking = self._find_candidate_return_picking(product_ids, self.sale_order_id.picking_ids, self.template_id.out_location_dest_id.id)
if not old_picking:
raise UserError(
'No eligible pickings were found to duplicate (you can only return products from the same initial picking).')
new_picking = 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)
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]
return_move.copy({
'name': self.name + ' OUT: ' + l.product_id.name_get()[0][1],
'product_id': l.product_id.id,
'product_uom_qty': l.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,
'move_dest_id': False,
})
return new_picking

View File

@@ -0,0 +1,7 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"manage_rma sale","manage rma","rma.model_rma_rma","sales_team.group_sale_salesman",1,1,1,1
"manage_rma_line sale","manage rma line","rma.model_rma_line","sales_team.group_sale_salesman",1,1,1,1
"manage_rma_template sale","manage rma template","rma.model_rma_template","sales_team.group_sale_manager",1,1,1,1
"manage_rma_tag sale","manage rma tag","rma.model_rma_tag","sales_team.group_sale_manager",1,1,1,1
"access_rma_template sale","access rma template","rma.model_rma_template","sales_team.group_sale_salesman",1,1,0,0
"access_rma_tag sale","access rma tag","rma.model_rma_tag","sales_team.group_sale_salesman",1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 manage_rma sale manage rma rma.model_rma_rma sales_team.group_sale_salesman 1 1 1 1
3 manage_rma_line sale manage rma line rma.model_rma_line sales_team.group_sale_salesman 1 1 1 1
4 manage_rma_template sale manage rma template rma.model_rma_template sales_team.group_sale_manager 1 1 1 1
5 manage_rma_tag sale manage rma tag rma.model_rma_tag sales_team.group_sale_manager 1 1 1 1
6 access_rma_template sale access rma template rma.model_rma_template sales_team.group_sale_salesman 1 1 0 0
7 access_rma_tag sale access rma tag rma.model_rma_tag sales_team.group_sale_salesman 1 0 0 0

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_rma_rma_form_sale" model="ir.ui.view">
<field name="name">rma.rma.form.sale</field>
<field name="model">rma.rma</field>
<field name="inherit_id" ref="rma.view_rma_rma_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button class="oe_stat_button" name="open_sale_order_rmas" icon="fa-cubes"
type="object" attrs="{'invisible': ['|', ('sale_order_id', '=', False), ('sale_order_rma_count', '&lt;=', 1)]}">
<field name="sale_order_rma_count" string="SO RMAs" widget="statinfo" />
</button>
</xpath>
<xpath expr="//field[@name='template_id']" position="after">
<field name="sale_order_id" options="{'no_create': True}" attrs="{'invisible': [('template_usage', '!=', 'sale_order')], 'required': [('template_usage', '=', 'sale_order')], 'readonly': [('state', 'in', ('confirmed', 'done', 'cancel'))]}"/>
<br/>
<button string="Add lines" type="object" name="action_add_so_lines" attrs="{'invisible': ['|', ('sale_order_id', '=', False), ('state', '!=', 'draft')]}"/>
</xpath>
</field>
</record>
<record id="view_rma_rma_tree_sale" model="ir.ui.view">
<field name="name">rma.rma.tree.sale</field>
<field name="model">rma.rma</field>
<field name="inherit_id" ref="rma.view_rma_rma_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='template_id']" position="after">
<field name="sale_order_id"/>
</xpath>
</field>
</record>
<record id="view_rma_rma_search_sale" model="ir.ui.view">
<field name="name">rma.rma.tree.sale</field>
<field name="model">rma.rma</field>
<field name="inherit_id" ref="rma.view_rma_rma_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='template_id']" position="after">
<field name="sale_order_id"/>
</xpath>
</field>
</record>
<menuitem
action="rma.action_rma_rma"
id="menu_action_sales_rma_form"
parent="sales_team.menu_sales"
sequence="12"
/>
<menuitem
action="rma.action_rma_template_form"
id="menu_action_sales_rma_template_form"
parent="sale.menu_sales_config"
sequence="12"
/>
<menuitem
action="rma.action_rma_tag_form"
id="menu_action_sales_rma_tag_form"
parent="sale.menu_sales_config"
sequence="12"
/>
</odoo>

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import rma_lines

View File

@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class RMASaleMakeLines(models.TransientModel):
_name = 'rma.sale.make.lines'
_description = 'Add SO Lines'
rma_id = fields.Many2one('rma.rma', string='RMA')
line_ids = fields.One2many('rma.sale.make.lines.line', 'rma_make_lines_id', string='Lines')
@api.model
def create(self, vals):
maker = super(RMASaleMakeLines, self).create(vals)
maker._create_lines()
return maker
def _line_values(self, so_line):
return {
'rma_make_lines_id': self.id,
'product_id': so_line.product_id.id,
'qty_ordered': so_line.product_uom_qty,
'qty_delivered': so_line.qty_delivered,
'qty_invoiced': so_line.qty_invoiced,
'product_uom_qty': 0.0,
'product_uom_id': so_line.product_uom.id,
}
def _create_lines(self):
make_lines_obj = self.env['rma.sale.make.lines.line']
if self.rma_id.template_usage == 'sale_order' and self.rma_id.sale_order_id:
for l in self.rma_id.sale_order_id.order_line:
self.line_ids |= make_lines_obj.create(self._line_values(l))
@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 RMASOMakeLinesLine(models.TransientModel):
_name = 'rma.sale.make.lines.line'
rma_make_lines_id = fields.Many2one('rma.make.lines')
product_id = fields.Many2one('product.product', string="Product")
qty_ordered = fields.Float(string='Ordered')
qty_invoiced = fields.Float(string='Invoiced')
qty_delivered = fields.Float(string='Delivered')
product_uom_qty = fields.Float(string='QTY')
product_uom_id = fields.Many2one('product.uom', 'UOM')

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_rma_add_lines_form" model="ir.ui.view">
<field name="name">view.rma.add.lines.form</field>
<field name="model">rma.sale.make.lines</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form>
<field name="line_ids">
<tree editable="top">
<field name="product_id" readonly="1"/>
<field name="qty_ordered" readonly="1"/>
<field name="qty_delivered" readonly="1"/>
<field name="qty_invoiced" readonly="1"/>
<field name="product_uom_qty"/>
<field name="product_uom_id" readonly="1"/>
</tree>
</field>
<footer>
<button class="oe_highlight"
name="add_lines"
type="object"
string="Add" />
<button class="oe_link"
special="cancel"
string="Cancel" />
</footer>
</form>
</field>
</record>
<record id="action_rma_add_lines" model="ir.actions.act_window">
<field name="name">Add RMA Lines</field>
<field name="res_model">rma.sale.make.lines</field>
<field name="view_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>