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
+
-
@@ -20,22 +99,31 @@ + - - - - - - - - - -