diff --git a/addons/stock_picking_batch/data/stock_picking_batch_demo.xml b/addons/stock_picking_batch/data/stock_picking_batch_demo.xml
index b4f00288029..69c8edf0e76 100644
--- a/addons/stock_picking_batch/data/stock_picking_batch_demo.xml
+++ b/addons/stock_picking_batch/data/stock_picking_batch_demo.xml
@@ -4,10 +4,12 @@
in_progress
+
in_progress
+
diff --git a/addons/stock_picking_batch/models/stock_picking.py b/addons/stock_picking_batch/models/stock_picking.py
index b7501f41de0..dd8470bee70 100644
--- a/addons/stock_picking_batch/models/stock_picking.py
+++ b/addons/stock_picking_batch/models/stock_picking.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import fields, models
+from odoo import api, fields, models
class StockPicking(models.Model):
@@ -13,6 +13,21 @@ class StockPicking(models.Model):
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
help='Batch associated to this transfer', copy=False)
+ @api.model
+ def create(self, vals):
+ res = super().create(vals)
+ if vals.get('batch_id'):
+ self.batch_id._sanity_check()
+ return res
+
+ def write(self, vals):
+ res = super().write(vals)
+ if vals.get('batch_id'):
+ if not self.batch_id.picking_type_id:
+ self.batch_id.picking_type_id = self.picking_type_id[0]
+ self.batch_id._sanity_check()
+ return res
+
def _should_show_transfers(self):
if len(self.batch_id) == 1 and self == self.batch_id.picking_ids:
return False
diff --git a/addons/stock_picking_batch/models/stock_picking_batch.py b/addons/stock_picking_batch/models/stock_picking_batch.py
index 70e4c1b17f2..5e13a4649a9 100644
--- a/addons/stock_picking_batch/models/stock_picking_batch.py
+++ b/addons/stock_picking_batch/models/stock_picking_batch.py
@@ -17,48 +17,134 @@ class StockPickingBatch(models.Model):
help='Name of the batch transfer')
user_id = fields.Many2one(
'res.users', string='Responsible', tracking=True, check_company=True,
+ readonly=True, states={'draft': [('readonly', False)], 'in_progress': [('readonly', False)]},
help='Person responsible for this batch transfer')
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="[('company_id', '=', company_id), ('state', 'not in', ('done', 'cancel'))]",
+ 'stock.picking', 'batch_id', string='Transfers', readonly=True,
+ domain="[('id', 'in', allowed_picking_ids)]", check_company=True,
+ states={'draft': [('readonly', False)], 'in_progress': [('readonly', False)]},
help='List of transfers associated to this batch')
+ show_check_availability = fields.Boolean(
+ compute='_compute_move_ids',
+ help='Technical field used to compute whether the check availability button should be shown.')
+ 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', readonly=True,
+ states={'draft': [('readonly', False)], 'in_progress': [('readonly', False)]})
state = fields.Selection([
('draft', 'Draft'),
('in_progress', 'In progress'),
('done', 'Done'),
('cancel', 'Cancelled')], default='draft',
copy=False, tracking=True, required=True, readonly=True)
+ picking_type_id = fields.Many2one(
+ 'stock.picking.type', 'Operation Type', check_company=True, copy=False,
+ readonly=True, states={'draft': [('readonly', False)]})
+ @api.depends('company_id', 'picking_type_id', 'state')
+ def _compute_allowed_picking_ids(self):
+ allowed_picking_states = ['waiting', 'confirmed', 'assigned']
+ cancelled_batchs = self.env['stock.picking.batch'].search_read(
+ [('state', '=', 'cancel')], ['id']
+ )
+ cancelled_batch_ids = [batch['id'] for batch in cancelled_batchs]
+
+ 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),
+ ('immediate_transfer', '=', False),
+ ('state', 'in', domain_states),
+ '|',
+ '|',
+ ('batch_id', '=', False),
+ ('batch_id', '=', batch.id),
+ ('batch_id', 'in', cancelled_batch_ids),
+ ]
+ 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_lines', 'picking_ids.move_lines.state')
+ def _compute_move_ids(self):
+ for batch in self:
+ batch.move_ids = batch.picking_ids.move_lines
+ batch.move_line_ids = batch.picking_ids.move_line_ids
+ batch.show_check_availability = any(m.state != 'assigned' for m in batch.move_ids)
+
+
+ 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
def create(self, vals):
if vals.get('name', '/') == '/':
vals['name'] = self.env['ir.sequence'].next_by_code('picking.batch') or '/'
- return super(StockPickingBatch, self).create(vals)
+ return super().create(vals)
- def confirm_picking(self):
+ def write(self, vals):
+ res = super().write(vals)
+ 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
+ return res
+
+ def unlink(self):
+ if any(batch.state != 'draft' for batch in self):
+ raise UserError(_("You can only delete draft batch transfers."))
+ return super().unlink()
+
+ # -------------------------------------------------------------------------
+ # 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()
- pickings_todo = self.mapped('picking_ids')
- self.write({'state': 'in_progress'})
- return pickings_todo.action_assign()
+ self.state = 'in_progress'
+ return True
- def cancel_picking(self):
- self.mapped('picking_ids').action_cancel()
- return self.write({'state': 'cancel'})
+ def action_cancel(self):
+ self.ensure_one()
+ self.state = 'cancel'
+ return True
- def print_picking(self):
- pickings = self.mapped('picking_ids')
- if not pickings:
- raise UserError(_('Nothing to print.'))
+ def action_print(self):
+ self.ensure_one()
return self.env.ref('stock_picking_batch.action_report_picking_batch').report_action(self)
- def done(self):
+ def action_done(self):
+ self.ensure_one()
self._check_company()
pickings = self.mapped('picking_ids').filtered(lambda picking: picking.state not in ('cancel', 'done'))
if any(picking.state not in ('assigned') for picking in pickings):
raise UserError(_('Some transfers are still waiting for goods. Please check or force their availability before setting this batch to done.'))
+
for picking in pickings:
picking.message_post(
body="%s: %s %s" % (
@@ -70,8 +156,25 @@ class StockPickingBatch(models.Model):
self.write({'state': 'done'})
return self.picking_ids.button_validate()
+ def action_assign(self):
+ self.ensure_one()
+ self.picking_ids.action_assign()
+
+ # -------------------------------------------------------------------------
+ # 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, if they aren't immediate " +
+ "transfers or if they're not already part of another batch transfer.\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(StockPickingBatch, self)._track_subtype(init_values)
+ return super()._track_subtype(init_values)
diff --git a/addons/stock_picking_batch/tests/test_batch_picking.py b/addons/stock_picking_batch/tests/test_batch_picking.py
index 1a41622bb99..f23270e003c 100644
--- a/addons/stock_picking_batch/tests/test_batch_picking.py
+++ b/addons/stock_picking_batch/tests/test_batch_picking.py
@@ -71,14 +71,18 @@ class TestBatchPicking(TransactionCase):
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 10.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
- # confirm batch, picking should be assigned
- self.batch.confirm_picking()
- self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be reserved')
- self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be reserved')
+ # Confirm batch, pickings should not be automatically assigned.
+ self.batch.action_confirm()
+ self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed')
+ self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed')
+ # Ask to assign, so pickings should be assigned now.
+ self.batch.action_assign()
+ self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready')
+ self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready')
self.picking_client_1.move_lines.quantity_done = 10
self.picking_client_2.move_lines.quantity_done = 10
- self.batch.done()
+ self.batch.action_done()
self.assertEqual(self.picking_client_1.state, 'done', 'Picking 1 should be done')
self.assertEqual(self.picking_client_2.state, 'done', 'Picking 2 should be done')
@@ -98,13 +102,17 @@ class TestBatchPicking(TransactionCase):
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 10.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
- # confirm batch, picking should be assigned
- self.batch.confirm_picking()
- self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be reserved')
- self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be reserved')
+ # Confirm batch, pickings should not be automatically assigned.
+ self.batch.action_confirm()
+ self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed')
+ self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed')
+ # Ask to assign, so pickings should be assigned now.
+ self.batch.action_assign()
+ self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready')
+ self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready')
# There should be a wizard asking to process picking without quantity done
- immediate_transfer_wizard_dict = self.batch.done()
+ immediate_transfer_wizard_dict = self.batch.action_done()
self.assertTrue(immediate_transfer_wizard_dict)
immediate_transfer_wizard = Form(self.env[(immediate_transfer_wizard_dict.get('res_model'))].with_context(immediate_transfer_wizard_dict['context'])).save()
self.assertEqual(len(immediate_transfer_wizard.pick_ids), 2)
@@ -128,8 +136,12 @@ class TestBatchPicking(TransactionCase):
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 5.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
- # confirm batch, picking should be assigned
- self.batch.confirm_picking()
+ # Confirm batch, pickings should not be automatically assigned.
+ self.batch.action_confirm()
+ self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed')
+ self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed')
+ # Ask to assign, so pickings should be assigned now.
+ self.batch.action_assign()
self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready')
self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready')
@@ -137,7 +149,7 @@ class TestBatchPicking(TransactionCase):
self.picking_client_2.move_lines.quantity_done = 10
# There should be a wizard asking to process picking without quantity done
- back_order_wizard_dict = self.batch.done()
+ back_order_wizard_dict = self.batch.action_done()
self.assertTrue(back_order_wizard_dict)
back_order_wizard = Form(self.env[(back_order_wizard_dict.get('res_model'))].with_context(back_order_wizard_dict['context'])).save()
self.assertEqual(len(back_order_wizard.pick_ids), 1)
@@ -164,13 +176,17 @@ class TestBatchPicking(TransactionCase):
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 5.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
- # confirm batch, picking should be assigned
- self.batch.confirm_picking()
+ # Confirm batch, pickings should not be automatically assigned.
+ self.batch.action_confirm()
+ self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed')
+ self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed')
+ # Ask to assign, so pickings should be assigned now.
+ self.batch.action_assign()
self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready')
self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready')
# There should be a wizard asking to process picking without quantity done
- immediate_transfer_wizard_dict = self.batch.done()
+ immediate_transfer_wizard_dict = self.batch.action_done()
self.assertTrue(immediate_transfer_wizard_dict)
immediate_transfer_wizard = Form(self.env[(immediate_transfer_wizard_dict.get('res_model'))].with_context(immediate_transfer_wizard_dict['context'])).save()
self.assertEqual(len(immediate_transfer_wizard.pick_ids), 2)
@@ -200,14 +216,18 @@ class TestBatchPicking(TransactionCase):
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 5.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
- # confirm batch, picking should be assigned
- self.batch.confirm_picking()
+ # Confirm batch, pickings should not be automatically assigned.
+ self.batch.action_confirm()
+ self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed')
+ self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed')
+ # Ask to assign, so pickings should be assigned now.
+ self.batch.action_assign()
self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready')
self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready')
self.picking_client_1.move_lines.quantity_done = 5
# There should be a wizard asking to process picking without quantity done
- immediate_transfer_wizard_dict = self.batch.done()
+ immediate_transfer_wizard_dict = self.batch.action_done()
self.assertTrue(immediate_transfer_wizard_dict)
immediate_transfer_wizard = Form(self.env[(immediate_transfer_wizard_dict.get('res_model'))].with_context(immediate_transfer_wizard_dict['context'])).save()
self.assertEqual(len(immediate_transfer_wizard.pick_ids), 1)
diff --git a/addons/stock_picking_batch/views/stock_picking_batch_views.xml b/addons/stock_picking_batch/views/stock_picking_batch_views.xml
index a53e19c8c06..b37fd782c9f 100644
--- a/addons/stock_picking_batch/views/stock_picking_batch_views.xml
+++ b/addons/stock_picking_batch/views/stock_picking_batch_views.xml
@@ -1,17 +1,96 @@
+
+ stock_picking_batch.picking.form
+ stock.picking
+
+ primary
+
+
+
+
+
+
+
+
+
+
+ stock_picking_batch.picking.move.tree
+ stock.move
+
+ primary
+
+
+
+ 0
+
+
+
+
+
+ 1
+
+
+
+
+
+ stock_picking_batch.move.line.tree
+ stock.move.line
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
stock.picking.batch.form
stock.picking.batch