mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
461 lines
22 KiB
Python
461 lines
22 KiB
Python
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')
|
|
company_id = fields.Many2one('res.company', 'Company')
|
|
|
|
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],
|
|
'picking_type_id': self.in_type_id.id,
|
|
'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],
|
|
'picking_type_id': self.out_type_id.id,
|
|
'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'].with_context(force_company=self.company_id.id).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'].with_context(force_company=self.company_id.id).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'].with_context(force_company=self.company_id.id).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('uom.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
|