305 lines
14 KiB
Python
305 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from markupsafe import Markup
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import UserError
|
|
from odoo.osv.expression import AND
|
|
from odoo.tools.float_utils import float_compare, float_is_zero, float_round
|
|
|
|
class StockPickingBatch(models.Model):
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
_name = "stock.picking.batch"
|
|
_description = "Batch Transfer"
|
|
_order = "name desc"
|
|
|
|
name = fields.Char(
|
|
string='Batch Transfer', default='New',
|
|
copy=False, required=True, readonly=True)
|
|
user_id = fields.Many2one(
|
|
'res.users', string='Responsible', tracking=True, check_company=True)
|
|
company_id = fields.Many2one(
|
|
'res.company', string="Company", required=True, readonly=True,
|
|
index=True, default=lambda self: self.env.company)
|
|
picking_ids = fields.One2many(
|
|
'stock.picking', 'batch_id', string='Transfers',
|
|
domain="[('id', 'in', allowed_picking_ids)]", check_company=True,
|
|
help='List of transfers associated to this batch')
|
|
show_check_availability = fields.Boolean(
|
|
compute='_compute_move_ids',
|
|
string='Show Check Availability')
|
|
show_allocation = fields.Boolean(
|
|
compute='_compute_show_allocation',
|
|
string='Show Allocation Button')
|
|
allowed_picking_ids = fields.One2many('stock.picking', compute='_compute_allowed_picking_ids')
|
|
move_ids = fields.One2many(
|
|
'stock.move', string="Stock moves", compute='_compute_move_ids')
|
|
move_line_ids = fields.One2many(
|
|
'stock.move.line', string='Stock move lines',
|
|
compute='_compute_move_ids', inverse='_set_move_line_ids')
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('in_progress', 'In progress'),
|
|
('done', 'Done'),
|
|
('cancel', 'Cancelled')], default='draft',
|
|
store=True, compute='_compute_state',
|
|
copy=False, tracking=True, required=True, readonly=True, index=True)
|
|
picking_type_id = fields.Many2one(
|
|
'stock.picking.type', 'Operation Type', check_company=True, copy=False,
|
|
index=True)
|
|
picking_type_code = fields.Selection(
|
|
related='picking_type_id.code')
|
|
scheduled_date = fields.Datetime(
|
|
'Scheduled Date', copy=False, store=True, readonly=False, compute="_compute_scheduled_date",
|
|
help="""Scheduled date for the transfers to be processed.
|
|
- If manually set then scheduled date for all transfers in batch will automatically update to this date.
|
|
- If not manually changed and transfers are added/removed/updated then this will be their earliest scheduled date
|
|
but this scheduled date will not be set for all transfers in batch.""")
|
|
is_wave = fields.Boolean('This batch is a wave')
|
|
# To remove in master
|
|
show_set_qty_button = fields.Boolean(compute='_compute_show_qty_button')
|
|
show_clear_qty_button = fields.Boolean(compute='_compute_show_qty_button')
|
|
show_lots_text = fields.Boolean(compute='_compute_show_lots_text')
|
|
|
|
@api.depends()
|
|
def _compute_show_qty_button(self):
|
|
self.show_set_qty_button = False
|
|
self.show_clear_qty_button = False
|
|
|
|
@api.depends('picking_type_id')
|
|
def _compute_show_lots_text(self):
|
|
for batch in self:
|
|
batch.show_lots_text = batch.picking_ids and batch.picking_ids[0].show_lots_text
|
|
|
|
@api.depends('company_id', 'picking_type_id', 'state')
|
|
def _compute_allowed_picking_ids(self):
|
|
allowed_picking_states = ['waiting', 'confirmed', 'assigned']
|
|
|
|
for batch in self:
|
|
domain_states = list(allowed_picking_states)
|
|
# Allows to add draft pickings only if batch is in draft as well.
|
|
if batch.state == 'draft':
|
|
domain_states.append('draft')
|
|
domain = [
|
|
('company_id', '=', batch.company_id.id),
|
|
('state', 'in', domain_states),
|
|
]
|
|
if batch.picking_type_id:
|
|
domain += [('picking_type_id', '=', batch.picking_type_id.id)]
|
|
batch.allowed_picking_ids = self.env['stock.picking'].search(domain)
|
|
|
|
@api.depends('picking_ids', 'picking_ids.move_line_ids', 'picking_ids.move_ids', 'picking_ids.move_ids.state')
|
|
def _compute_move_ids(self):
|
|
for batch in self:
|
|
batch.move_ids = batch.picking_ids.move_ids
|
|
batch.move_line_ids = batch.picking_ids.move_line_ids
|
|
batch.show_check_availability = any(m.state not in ['assigned', 'done'] for m in batch.move_ids)
|
|
|
|
@api.depends('state', 'move_ids', 'picking_type_id')
|
|
def _compute_show_allocation(self):
|
|
self.show_allocation = False
|
|
if not self.user_has_groups('stock.group_reception_report'):
|
|
return
|
|
for batch in self:
|
|
batch.show_allocation = batch.picking_ids._get_show_allocation(batch.picking_type_id)
|
|
|
|
@api.depends('picking_ids', 'picking_ids.state')
|
|
def _compute_state(self):
|
|
batchs = self.filtered(lambda batch: batch.state not in ['cancel', 'done'])
|
|
for batch in batchs:
|
|
if not batch.picking_ids:
|
|
continue
|
|
# Cancels automatically the batch picking if all its transfers are cancelled.
|
|
if all(picking.state == 'cancel' for picking in batch.picking_ids):
|
|
batch.state = 'cancel'
|
|
# Batch picking is marked as done if all its not canceled transfers are done.
|
|
elif all(picking.state in ['cancel', 'done'] for picking in batch.picking_ids):
|
|
batch.state = 'done'
|
|
|
|
@api.depends('picking_ids', 'picking_ids.scheduled_date')
|
|
def _compute_scheduled_date(self):
|
|
for rec in self:
|
|
rec.scheduled_date = min(rec.picking_ids.filtered('scheduled_date').mapped('scheduled_date'), default=False)
|
|
|
|
@api.onchange('scheduled_date')
|
|
def onchange_scheduled_date(self):
|
|
if self.scheduled_date:
|
|
self.picking_ids.scheduled_date = self.scheduled_date
|
|
|
|
def _set_move_line_ids(self):
|
|
new_move_lines = self[0].move_line_ids
|
|
for picking in self.picking_ids:
|
|
old_move_lines = picking.move_line_ids
|
|
picking.move_line_ids = new_move_lines.filtered(lambda ml: ml.picking_id.id == picking.id)
|
|
move_lines_to_unlink = old_move_lines - new_move_lines
|
|
if move_lines_to_unlink:
|
|
move_lines_to_unlink.unlink()
|
|
|
|
# -------------------------------------------------------------------------
|
|
# CRUD
|
|
# -------------------------------------------------------------------------
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
for vals in vals_list:
|
|
if vals.get('name', '/') == '/':
|
|
company_id = vals.get('company_id', self.env.company.id)
|
|
if vals.get('is_wave'):
|
|
vals['name'] = self.env['ir.sequence'].with_company(company_id).next_by_code('picking.wave') or '/'
|
|
else:
|
|
vals['name'] = self.env['ir.sequence'].with_company(company_id).next_by_code('picking.batch') or '/'
|
|
return super().create(vals_list)
|
|
|
|
def write(self, vals):
|
|
res = super().write(vals)
|
|
if not self.picking_ids:
|
|
self.filtered(lambda b: b.state == 'in_progress').action_cancel()
|
|
if vals.get('picking_type_id'):
|
|
self._sanity_check()
|
|
if vals.get('picking_ids'):
|
|
batch_without_picking_type = self.filtered(lambda batch: not batch.picking_type_id)
|
|
if batch_without_picking_type:
|
|
picking = self.picking_ids and self.picking_ids[0]
|
|
batch_without_picking_type.picking_type_id = picking.picking_type_id.id
|
|
if vals.get('user_id'):
|
|
self.picking_ids.assign_batch_user(vals['user_id'])
|
|
return res
|
|
|
|
@api.ondelete(at_uninstall=False)
|
|
def _unlink_if_not_done(self):
|
|
if any(batch.state == 'done' for batch in self):
|
|
raise UserError(_("You cannot delete Done batch transfers."))
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Action methods
|
|
# -------------------------------------------------------------------------
|
|
def action_confirm(self):
|
|
"""Sanity checks, confirm the pickings and mark the batch as confirmed."""
|
|
self.ensure_one()
|
|
if not self.picking_ids:
|
|
raise UserError(_("You have to set some pickings to batch."))
|
|
self.picking_ids.action_confirm()
|
|
self._check_company()
|
|
self.state = 'in_progress'
|
|
return True
|
|
|
|
def action_cancel(self):
|
|
self.state = 'cancel'
|
|
self.picking_ids = False
|
|
return True
|
|
|
|
def action_print(self):
|
|
self.ensure_one()
|
|
return self.env.ref('stock_picking_batch.action_report_picking_batch').report_action(self)
|
|
|
|
def action_done(self):
|
|
def has_no_quantity(picking):
|
|
return all(not m.picked or float_is_zero(m.quantity, precision_rounding=m.product_uom.rounding) for m in picking.move_ids if m.state not in ('done', 'cancel'))
|
|
|
|
self.ensure_one()
|
|
self._check_company()
|
|
# Empty 'waiting for another operation' pickings will be removed from the batch when it is validated.
|
|
pickings = self.mapped('picking_ids').filtered(lambda picking: picking.state not in ('cancel', 'done'))
|
|
empty_waiting_pickings = self.mapped('picking_ids').filtered(lambda p: p.state == 'waiting' and has_no_quantity(p))
|
|
pickings = pickings - empty_waiting_pickings
|
|
|
|
empty_pickings = set()
|
|
for picking in pickings:
|
|
if has_no_quantity(picking):
|
|
empty_pickings.add(picking.id)
|
|
picking.message_post(
|
|
body=Markup("<b>%s:</b> %s <a href=#id=%s&view_type=form&model=stock.picking.batch>%s</a>") % (
|
|
_("Transferred by"),
|
|
_("Batch Transfer"),
|
|
picking.batch_id.id,
|
|
picking.batch_id.name))
|
|
|
|
# Run sanity_check as a batch and ignore the one in button_validate() since it is done here.
|
|
pickings._sanity_check(separate_pickings=False)
|
|
# Skip sanity_check in pickings button_validate() & remove 'waiting' pickings from the batch
|
|
context = {'skip_sanity_check': True, 'pickings_to_detach': empty_waiting_pickings.ids}
|
|
if len(empty_pickings) == len(pickings):
|
|
return pickings.with_context(**context).button_validate()
|
|
else:
|
|
# If some pickings are at least partially done, other pickings (empty & waiting) will be removed from batch without being cancelled in case of no backorder
|
|
pickings = pickings - self.env['stock.picking'].browse(empty_pickings)
|
|
context['pickings_to_detach'] = context['pickings_to_detach'] + list(empty_pickings)
|
|
return pickings.with_context(skip_immediate=True, **context).button_validate()
|
|
|
|
def action_assign(self):
|
|
self.ensure_one()
|
|
self.picking_ids.action_assign()
|
|
|
|
def action_put_in_pack(self):
|
|
""" Action to put move lines with 'Done' quantities into a new pack
|
|
This method follows same logic to stock.picking.
|
|
"""
|
|
self.ensure_one()
|
|
if self.state not in ('done', 'cancel'):
|
|
move_line_ids = self.picking_ids[0]._package_move_lines()
|
|
if move_line_ids:
|
|
res = move_line_ids.picking_id[0]._pre_put_in_pack_hook(move_line_ids)
|
|
if res:
|
|
return res
|
|
package = move_line_ids.picking_id._put_in_pack(move_line_ids)
|
|
return move_line_ids.picking_id[0]._post_put_in_pack_hook(package)
|
|
raise UserError(_("Please add 'Done' quantities to the batch picking to create a new pack."))
|
|
|
|
def action_view_reception_report(self):
|
|
action = self.picking_ids[0].action_view_reception_report()
|
|
action['context'] = {'default_picking_ids': self.picking_ids.ids}
|
|
return action
|
|
|
|
def action_open_label_layout(self):
|
|
if self.user_has_groups('stock.group_production_lot') and self.move_line_ids.lot_id:
|
|
view = self.env.ref('stock.picking_label_type_form')
|
|
return {
|
|
'name': _('Choose Type of Labels To Print'),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'picking.label.type',
|
|
'views': [(view.id, 'form')],
|
|
'target': 'new',
|
|
'context': {'default_picking_ids': self.picking_ids.ids},
|
|
}
|
|
view = self.env.ref('stock.product_label_layout_form_picking')
|
|
return {
|
|
'name': _('Choose Labels Layout'),
|
|
'type': 'ir.actions.act_window',
|
|
'view_mode': 'form',
|
|
'res_model': 'product.label.layout',
|
|
'views': [(view.id, 'form')],
|
|
'view_id': view.id,
|
|
'target': 'new',
|
|
'context': {
|
|
'default_product_ids': self.move_line_ids.product_id.ids,
|
|
'default_move_ids': self.move_ids.ids,
|
|
'default_move_quantity': 'move'},
|
|
}
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Miscellaneous
|
|
# -------------------------------------------------------------------------
|
|
def _sanity_check(self):
|
|
for batch in self:
|
|
if not batch.picking_ids <= batch.allowed_picking_ids:
|
|
erroneous_pickings = batch.picking_ids - batch.allowed_picking_ids
|
|
raise UserError(_(
|
|
"The following transfers cannot be added to batch transfer %s. "
|
|
"Please check their states and operation types.\n\n"
|
|
"Incompatibilities: %s", batch.name, ', '.join(erroneous_pickings.mapped('name'))))
|
|
|
|
def _track_subtype(self, init_values):
|
|
if 'state' in init_values:
|
|
return self.env.ref('stock_picking_batch.mt_batch_state')
|
|
return super()._track_subtype(init_values)
|
|
|
|
def _is_picking_auto_mergeable(self, picking):
|
|
""" Verifies if a picking can be safely inserted into the batch without violating auto_batch_constrains.
|
|
"""
|
|
res = True
|
|
if self.picking_type_id.batch_max_lines:
|
|
res = res and (len(self.move_ids) + len(picking.move_ids) <= self.picking_type_id.batch_max_lines)
|
|
if self.picking_type_id.batch_max_pickings:
|
|
res = res and (len(self.picking_ids) + 1 <= self.picking_type_id.batch_max_pickings)
|
|
return res
|