diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 077e8147917..631d7542e0c 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -883,17 +883,7 @@ class StockMove(models.Model): # If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move move.move_line_ids.filtered(lambda x: x.qty_done == 0.0).write({'move_id': new_move}) self.browse(new_move).quantity_done = 0.0 - for packop in move.move_line_ids: - if float_compare(packop.qty_done, 0, precision_rounding=rounding) > 0: - if move.has_tracking != 'none' and (move.picking_type_id.use_create_lots or move.picking_type_id.use_existing_lots): - if packop.lot_name and not packop.lot_id: - lot = self.env['stock.production.lot'].create( - {'name': packop.lot_name, 'product_id': packop.product_id.id}) - packop.write({'lot_id': lot.id}) - if not packop.lot_id: - raise UserError(_('You need to supply a lot/serial number.')) - # execute `action_done` even if nothing was done, to free the reservation - packop.action_done() + move.move_line_ids.action_done() picking = self and self[0].picking_id or False moves_todo.write({'state': 'done', 'date': fields.Datetime.now()}) moves_todo.mapped('move_dest_ids').action_assign() diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index c2768c11a27..cdfb68920de 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -237,10 +237,56 @@ class StockMoveLine(models.Model): return super(StockMoveLine, self).unlink() def action_done(self): - """ This method will finalize the work with a move line by "moving" quants to the - destination location. + """ This method is called during a move's `action_done`. It'll actually move a quant from + the source location to the destination location, and unreserve if needed in the source + location. + + This method is intended to be called on all the move lines of a move. This method is not + intended to be called when editing a `done` move (that's what the override of `write` here + is done. """ + + # First, we loop over all the move lines to do a preliminary check: `qty_done` should not + # be negative and, according to the presence of a picking type or a linked inventory + # adjustment, enforce some rules on the `lot_id` field. If `qty_done` is null, we unlink + # the line. It is mandatory in order to free the reservation and correctly apply + # `action_done` on the next move lines. + ml_to_delete = self.env['stock.move.line'] for ml in self: + qty_done_float_compared = float_compare(ml.qty_done, 0, precision_rounding=ml.product_uom_id.rounding) + if qty_done_float_compared > 0: + if ml.product_id.tracking != 'none': + picking_type_id = ml.move_id.picking_type_id + if picking_type_id: + if picking_type_id.use_create_lots: + # If a picking type is linked, we may have to create a production lot on + # the fly before assigning it to the move line if the user checked both + # `use_create_lots` and `use_existing_lots`. + if ml.lot_name and not ml.lot_id: + lot = self.env['stock.production.lot'].create( + {'name': ml.lot_name, 'product_id': ml.product_id.id} + ) + ml.write({'lot_id': lot.id}) + elif not picking_type_id.use_create_lots and not picking_type_id.use_existing_lots: + # If the user disabled both `use_create_lots` and `use_existing_lots` + # checkboxes on the picking type, he's allowed to enter tracked + # products without a `lot_id`. + continue + elif ml.move_id.inventory_id: + # If an inventory adjustment is linked, the user is allowed to enter + # tracked products without a `lot_id`. + continue + + if not ml.lot_id: + raise UserError(_('You need to supply a lot/serial number.')) + elif qty_done_float_compared < 0: + raise UserError(_('No negative quantities allowed')) + else: + ml_to_delete |= ml + ml_to_delete.unlink() + + # Now, we can actually move the quant. + for ml in self - ml_to_delete: if ml.product_id.type != 'consu': Quant = self.env['stock.quant'] rounding = ml.product_uom_id.rounding diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 29dbc3eabf4..fa4ee655934 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -591,7 +591,6 @@ class Picking(models.Model): @api.multi def button_validate(self): self.ensure_one() - move_line_delete = self.env['stock.move.line'] if not self.move_lines and not self.move_line_ids: raise UserError(_('Please add some lines to move')) # In draft or with no pack operations edited yet, ask if we can just do everything @@ -633,16 +632,6 @@ class Picking(models.Model): 'res_id': wiz.id, 'context': self.env.context, } - for operation in self.move_line_ids: - if operation.qty_done < 0: - raise UserError(_('No negative quantities allowed')) - if operation.qty_done > 0: - pass - #operation.write({'product_qty': operation.qty_done}) - else: - move_line_delete |= operation - if move_line_delete: - move_line_delete.unlink() self.action_done() return diff --git a/addons/stock/models/stock_production_lot.py b/addons/stock/models/stock_production_lot.py index 88602d3523a..d9197ad96ac 100644 --- a/addons/stock/models/stock_production_lot.py +++ b/addons/stock/models/stock_production_lot.py @@ -30,10 +30,10 @@ class ProductionLot(models.Model): @api.model def create(self, vals): - pack_id = self.env.context.get('active_pack_operation', False) - if pack_id: - pack = self.env['stock.move.line'].browse(pack_id) - if pack.picking_id and not pack.picking_id.picking_type_id.use_create_lots: + active_picking_id = self.env.context.get('active_picking_id', False) + if active_picking_id: + picking_id = self.env['stock.picking'].browse(active_picking_id) + if picking_id and not picking_id.picking_type_id.use_create_lots: raise UserError(_("You are not allowed to create a lot for this picking type")) return super(ProductionLot, self).create(vals) diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py index e6773e93fd9..cd6d48f9393 100644 --- a/addons/stock/tests/test_move2.py +++ b/addons/stock/tests/test_move2.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.addons.stock.tests.common import TestStockCommon +from odoo.exceptions import UserError class TestPickShip(TestStockCommon): @@ -746,3 +747,177 @@ class TestSinglePicking(TestStockCommon): self.assertEqual(move2.state, 'assigned') self.assertEqual(delivery_order.state, 'assigned') + + def test_use_create_lot_use_existing_lot_1(self): + """ Check the behavior of a picking when `use_create_lot` and `use_existing_lot` are + set to False and there's a move for a tracked product. + """ + self.env['stock.picking.type']\ + .browse(self.picking_type_out)\ + .write({ + 'use_create_lots': False, + 'use_existing_lots': False, + }) + self.productA.tracking = 'lot' + + delivery_order = self.env['stock.picking'].create({ + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_out, + }) + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': delivery_order.id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + }) + + delivery_order.action_confirm() + delivery_order.force_assign() + delivery_order.move_lines.quantity_done = 2 + # do not set a lot_id or lot_name, it should work + delivery_order.action_done() + + def test_use_create_lot_use_existing_lot_2(self): + """ Check the behavior of a picking when `use_create_lot` and `use_existing_lot` are + set to True and there's a move for a tracked product. + """ + self.env['stock.picking.type']\ + .browse(self.picking_type_out)\ + .write({ + 'use_create_lots': True, + 'use_existing_lots': True, + }) + self.productA.tracking = 'lot' + + delivery_order = self.env['stock.picking'].create({ + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_out, + }) + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': delivery_order.id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + }) + + delivery_order.action_confirm() + delivery_order.force_assign() + delivery_order.move_lines.quantity_done = 2 + move_line = delivery_order.move_lines.move_line_ids + + # not lot_name set, should raise + with self.assertRaises(UserError): + delivery_order.action_done() + + # enter a new lot name, should work + move_line.lot_name = 'newlot' + delivery_order.action_done() + + def test_use_create_lot_use_existing_lot_3(self): + """ Check the behavior of a picking when `use_create_lot` is set to True and + `use_existing_lot` is set to False and there's a move for a tracked product. + """ + self.env['stock.picking.type']\ + .browse(self.picking_type_out)\ + .write({ + 'use_create_lots': True, + 'use_existing_lots': False, + }) + self.productA.tracking = 'lot' + + delivery_order = self.env['stock.picking'].create({ + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_out, + }) + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': delivery_order.id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + }) + + delivery_order.action_confirm() + delivery_order.force_assign() + delivery_order.move_lines.quantity_done = 2 + move_line = delivery_order.move_lines.move_line_ids + + # not lot_name set, should raise + with self.assertRaises(UserError): + delivery_order.action_done() + + # enter a new lot name, should work + move_line.lot_name = 'newlot' + delivery_order.action_done() + + def test_use_create_lot_use_existing_lot_4(self): + """ Check the behavior of a picking when `use_create_lot` is set to False and + `use_existing_lot` is set to True and there's a move for a tracked product. + """ + self.env['stock.picking.type']\ + .browse(self.picking_type_out)\ + .write({ + 'use_create_lots': False, + 'use_existing_lots': True, + }) + self.productA.tracking = 'lot' + + delivery_order = self.env['stock.picking'].create({ + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_out, + }) + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': delivery_order.id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.pack_location, + 'location_dest_id': self.customer_location, + }) + + delivery_order.action_confirm() + delivery_order.force_assign() + delivery_order.move_lines.quantity_done = 2 + move_line = delivery_order.move_lines.move_line_ids + + # not lot_name set, should raise + with self.assertRaises(UserError): + delivery_order.action_done() + + # creating a lot from the view should raise + with self.assertRaises(UserError): + self.env['stock.production.lot']\ + .with_context(active_picking_id=delivery_order.id)\ + .create({ + 'name': 'lot1', + 'product_id': self.productA.id, + }) + + # enter an existing lot_id, should work + lot1 = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': self.productA.id, + }) + move_line.lot_id = lot1 + delivery_order.action_done() diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index 589e43dbd7e..5db94bc9a58 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -1237,7 +1237,6 @@ class TestStockFlow(TestStockCommon): def test_30_check_with_no_incoming_lot(self): """ Picking in without lots and picking out with""" - # Change basic operation type not to get lots # Create product with lot tracking picking_in = self.env['stock.picking.type'].browse(self.picking_type_in) @@ -1254,6 +1253,7 @@ class TestStockFlow(TestStockCommon): 'product_uom_qty': 4, 'product_uom': self.productA.uom_id.id, 'picking_id': picking_in.id, + 'picking_type_id': self.picking_type_in, 'location_id': self.supplier_location, 'location_dest_id': self.stock_location}) diff --git a/addons/stock/views/stock_move_views.xml b/addons/stock/views/stock_move_views.xml index 337a9b287eb..41716054ccc 100644 --- a/addons/stock/views/stock_move_views.xml +++ b/addons/stock/views/stock_move_views.xml @@ -171,7 +171,7 @@ - +