diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index 8f0eeb113ef..fa373e022f3 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -98,8 +98,9 @@ class StockMoveLine(models.Model): if ml.state == 'done': Quant = self.env['stock.quant'] quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id,rounding_method='HALF-UP') + in_date = None if ml.location_id.should_impact_quants() and ml.product_id.type == 'product': - available_qty = Quant._update_available_quantity(ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) + available_qty, in_date = Quant._update_available_quantity(ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) if available_qty < 0 and ml.lot_id: # see if we can compensate the negative quants with some untracked quants untracked_qty = Quant._get_available_quantity(ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) @@ -108,7 +109,7 @@ class StockMoveLine(models.Model): Quant._update_available_quantity(ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id) Quant._update_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) if ml.location_dest_id.should_impact_quants() and ml.product_id.type == 'product': - Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id) + Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date) next_moves = ml.move_id.move_dest_ids.filtered(lambda move: move.state not in ('done', 'cancel')) next_moves.do_unreserve() next_moves.action_assign() @@ -187,14 +188,16 @@ class StockMoveLine(models.Model): if updates or 'qty_done' in vals: for ml in self.filtered(lambda ml: ml.move_id.state == 'done'): # undo the original move line + in_date = None if ml.location_dest_id.should_impact_quants() and ml.product_id.type == 'product': - Quant._update_available_quantity(ml.product_id, ml.location_dest_id, -ml.qty_done, lot_id=ml.lot_id, - package_id=ml.package_id, owner_id=ml.owner_id) + in_date = Quant._update_available_quantity(ml.product_id, ml.location_dest_id, -ml.qty_done, lot_id=ml.lot_id, + package_id=ml.package_id, owner_id=ml.owner_id)[1] if ml.location_id.should_impact_quants() and ml.product_id.type == 'product': Quant._update_available_quantity(ml.product_id, ml.location_id, ml.qty_done, lot_id=ml.lot_id, - package_id=ml.package_id, owner_id=ml.owner_id) + package_id=ml.package_id, owner_id=ml.owner_id, in_date=in_date) # move what's been actually done + in_date = None product_id = ml.product_id location_id = updates.get('location_id', ml.location_id) location_dest_id = updates.get('location_dest_id', ml.location_dest_id) @@ -206,7 +209,7 @@ class StockMoveLine(models.Model): quantity = ml.move_id.product_uom._compute_quantity(qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') if location_id.should_impact_quants() and ml.product_id.type == 'product': ml._free_reservation(product_id, location_id, quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id) - available_qty = Quant._update_available_quantity(product_id, location_id, -quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id) + available_qty, in_date = Quant._update_available_quantity(product_id, location_id, -quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id) if available_qty < 0 and lot_id: # see if we can compensate the negative quants with some untracked quants untracked_qty = Quant._get_available_quantity(product_id, location_id, lot_id=False, package_id=package_id, owner_id=owner_id, strict=True) @@ -216,7 +219,7 @@ class StockMoveLine(models.Model): Quant._update_available_quantity(product_id, location_id, taken_from_untracked_qty, lot_id=lot_id, package_id=package_id, owner_id=owner_id) ml._free_reservation(ml.product_id, location_id, untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id) if location_dest_id.should_impact_quants() and ml.product_id.type == 'product' and qty_done: - Quant._update_available_quantity(product_id, location_dest_id, quantity, lot_id=lot_id, package_id=result_package_id, owner_id=owner_id) + Quant._update_available_quantity(product_id, location_dest_id, quantity, lot_id=lot_id, package_id=result_package_id, owner_id=owner_id, in_date=in_date) # Unreserve and reserve following move in order to have the real reserved quantity on move_line. next_moves |= ml.move_id.move_dest_ids.filtered(lambda move: move.state not in ('done', 'cancel')) res = super(StockMoveLine, self).write(vals) @@ -304,8 +307,9 @@ class StockMoveLine(models.Model): # move what's been actually done quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') + in_date = None if ml.location_id.should_impact_quants() and ml.product_id.type == 'product': - available_qty = Quant._update_available_quantity(ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) + available_qty, in_date = Quant._update_available_quantity(ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) if available_qty < 0 and ml.lot_id: # see if we can compensate the negative quants with some untracked quants untracked_qty = Quant._get_available_quantity(ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) @@ -314,7 +318,7 @@ class StockMoveLine(models.Model): Quant._update_available_quantity(ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id) Quant._update_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) if ml.location_dest_id.should_impact_quants() and ml.product_id.type == 'product': - Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id) + Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date) def _free_reservation(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None): """ When editing a done move line or validating one with some forced quantities, it is diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index 641e3e3f5b4..5609f5be6a6 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -57,6 +57,13 @@ class StockQuant(models.Model): if quant.quantity > 1 and quant.lot_id and quant.product_id.tracking == 'serial': raise ValidationError(_('A serial number should only be linked to a single product.')) + @api.multi + @api.constrains('in_date', 'lot_id') + def check_in_date(self): + for quant in self: + if quant.in_date and not quant.lot_id: + raise ValidationError(_('An incoming date cannot be set to an untracked product.')) + @api.one def _compute_name(self): self.name = '%s: %s%s' % (self.lot_id.name or self.product_id.code or '', self.quantity, self.product_id.uom_id.name) @@ -126,14 +133,44 @@ class StockQuant(models.Model): return sum(quants.mapped('quantity')) - sum(quants.mapped('reserved_quantity')) @api.model - def _update_available_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None): + def _update_available_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, in_date=None): + """ Increase or decrease `reserved_quantity` of a set of quants for a given set of + product_id/location_id/lot_id/package_id/owner_id. + + :param product_id: + :param location_id: + :param quantity: + :param lot_id: + :param package_id: + :param owner_id: + :param datetime in_date: Should only be passed when calls to this method are done in + order to move a quant. When creating a tracked quant, the + current datetime will be used. + :return: tuple (available_quantity, in_date as a datetime) + """ self = self.sudo() quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True) + + if lot_id: + incoming_dates = quants.mapped('in_date') # `mapped` already filtered out falsy items + incoming_dates = [fields.Datetime.from_string(incoming_date) for incoming_date in incoming_dates] + if in_date: + incoming_dates += [in_date] + # If multiple incoming dates are available for a given lot_id/package_id/owner_id, we + # consider only the oldest one as being relevant. + if incoming_dates: + in_date = fields.Datetime.to_string(min(incoming_dates)) + else: + in_date = fields.Datetime.now() + for quant in quants: try: with self._cr.savepoint(): self._cr.execute("SELECT 1 FROM stock_quant WHERE id = %s FOR UPDATE NOWAIT", [quant.id], log_exceptions=False) - quant.quantity += quantity + quant.write({ + 'quantity': quant.quantity + quantity, + 'in_date': in_date, + }) # cleanup empty quants if quant.quantity == 0 and quant.reserved_quantity == 0: quant.unlink() @@ -151,8 +188,9 @@ class StockQuant(models.Model): 'lot_id': lot_id and lot_id.id, 'package_id': package_id and package_id.id, 'owner_id': owner_id and owner_id.id, + 'in_date': in_date, }) - return self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=False) + return self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=False), fields.Datetime.from_string(in_date) @api.model def _update_reserved_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, strict=False): diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index c08a63a2f8c..a0902c455ce 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -114,7 +114,10 @@ class StockMove(TransactionCase): self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.supplier_location), 0.0) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.stock_location), 5.0) self.assertEqual(len(self.env['stock.quant']._gather(self.product3, self.supplier_location)), 0.0) - self.assertEqual(len(self.env['stock.quant']._gather(self.product3, self.stock_location)), 1.0) + quants = self.env['stock.quant']._gather(self.product3, self.stock_location) + self.assertEqual(len(quants), 1.0) + for quant in quants: + self.assertNotEqual(quant.in_date, False) def test_in_3(self): """ Receive 5 serial-tracked products from a supplier. The system should create 5 differents @@ -162,7 +165,10 @@ class StockMove(TransactionCase): self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location), 5.0) self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.supplier_location)), 0.0) - self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.stock_location)), 5.0) + quants = self.env['stock.quant']._gather(self.product2, self.stock_location) + self.assertEqual(len(quants), 5.0) + for quant in quants: + self.assertNotEqual(quant.in_date, False) def test_out_1(self): """ Send products to a client. Check that a move line is created reserving products in @@ -2371,3 +2377,241 @@ class StockMove(TransactionCase): self.assertEqual(move1.state, 'confirmed') self.assertEqual(len(move1.move_line_ids), 0) + + def test_in_date_1(self): + """ Check that moving a tracked quant keeps the incoming date. + """ + move1 = self.env['stock.move'].create({ + 'name': 'test_in_date_1', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product3.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + 'picking_type_id': self.env.ref('stock.picking_type_in').id, + }) + move1.action_confirm() + move1.action_assign() + move1.move_line_ids.lot_name = 'lot1' + move1.move_line_ids.qty_done = 1 + move1.action_done() + + quant = self.env['stock.quant']._gather(self.product3, self.stock_location) + self.assertEqual(len(quant), 1.0) + self.assertNotEqual(quant.in_date, False) + + # Keep a reference to the initial incoming date in order to compare it later. + initial_incoming_date = quant.in_date + + move2 = self.env['stock.move'].create({ + 'name': 'test_in_date_1', + 'location_id': self.stock_location.id, + 'location_dest_id': self.pack_location.id, + 'product_id': self.product3.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move2.action_confirm() + move2.action_assign() + move2.move_line_ids.qty_done = 1 + move2.action_done() + + quant = self.env['stock.quant']._gather(self.product3, self.pack_location) + self.assertEqual(len(quant), 1.0) + self.assertEqual(quant.in_date, initial_incoming_date) + + def test_in_date_2(self): + """ Check that editing a done move line for a tracked product and changing its lot + correctly restores the original lot with its incoming date and remove the new lot + with its incoming date. + """ + lot1 = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': self.product3.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': self.product3.id, + }) + # receive lot1 + move1 = self.env['stock.move'].create({ + 'name': 'test_in_date_1', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product3.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + 'picking_type_id': self.env.ref('stock.picking_type_in').id, + }) + move1.action_confirm() + move1.action_assign() + move1.move_line_ids.lot_id = lot1 + move1.move_line_ids.qty_done = 1 + move1.action_done() + + # receive lot2 + move2 = self.env['stock.move'].create({ + 'name': 'test_in_date_1', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product3.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + 'picking_type_id': self.env.ref('stock.picking_type_in').id, + }) + move2.action_confirm() + move2.action_assign() + move2.move_line_ids.lot_id = lot2 + move2.move_line_ids.qty_done = 1 + move2.action_done() + + initial_in_date_lot2 = self.env['stock.quant'].search([ + ('product_id', '=', self.product3.id), + ('lot_id', '=', lot2.id), + ]).in_date + + # Edit lot1's incoming date. + quant_lot1 = self.env['stock.quant'].search([ + ('product_id', '=', self.product3.id), + ('lot_id', '=', lot1.id), + ]) + from datetime import datetime, timedelta + initial_in_date_lot1 = datetime.now() - timedelta(days=5) + quant_lot1.in_date = initial_in_date_lot1 + + # Move one quant to pack location + move3 = self.env['stock.move'].create({ + 'name': 'test_in_date_1', + 'location_id': self.stock_location.id, + 'location_dest_id': self.pack_location.id, + 'product_id': self.product3.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move3.action_confirm() + move3.action_assign() + move3.move_line_ids.qty_done = 1 + move3.action_done() + quant_in_pack = self.env['stock.quant'].search([ + ('product_id', '=', self.product3.id), + ('location_id', '=', self.pack_location.id), + ]) + # As lot1 has an older date and FIFO is set by default, it's the one that should be + # in pack. + self.assertEqual(len(quant_in_pack), 1) + from odoo.fields import Datetime + self.assertEqual(quant_in_pack.in_date, Datetime.to_string(initial_in_date_lot1)) + self.assertEqual(quant_in_pack.lot_id, lot1) + + # Now, edit the move line and actually move the other lot + move3.move_line_ids.lot_id = lot2 + + # Check that lot1 correctly is back to stock with its right in_date + quant_lot1 = self.env['stock.quant'].search([ + ('product_id', '=', self.product3.id), + ('lot_id', '=', lot1.id), + ]) + self.assertEqual(quant_lot1.location_id, self.stock_location) + self.assertEqual(quant_lot1.in_date, Datetime.to_string(initial_in_date_lot1)) + + # Check that lo2 is in pack with is right in_date + quant_lot2 = self.env['stock.quant'].search([ + ('product_id', '=', self.product3.id), + ('lot_id', '=', lot2.id), + ]) + self.assertEqual(quant_lot2.location_id, self.pack_location) + self.assertEqual(quant_lot2.in_date, initial_in_date_lot2) + + def test_in_date_3(self): + """ Check that, when creating a move line on a done stock move, the lot and its incoming + date are correctly moved to the destination location. + """ + lot1 = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': self.product3.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': self.product3.id, + }) + # receive lot1 + move1 = self.env['stock.move'].create({ + 'name': 'test_in_date_1', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product3.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + 'picking_type_id': self.env.ref('stock.picking_type_in').id, + }) + move1.action_confirm() + move1.action_assign() + move1.move_line_ids.lot_id = lot1 + move1.move_line_ids.qty_done = 1 + move1.action_done() + + # receive lot2 + move2 = self.env['stock.move'].create({ + 'name': 'test_in_date_1', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product3.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + 'picking_type_id': self.env.ref('stock.picking_type_in').id, + }) + move2.action_confirm() + move2.action_assign() + move2.move_line_ids.lot_id = lot2 + move2.move_line_ids.qty_done = 1 + move2.action_done() + + initial_in_date_lot2 = self.env['stock.quant'].search([ + ('product_id', '=', self.product3.id), + ('lot_id', '=', lot2.id), + ]).in_date + + # Edit lot1's incoming date. + quant_lot1 = self.env['stock.quant'].search([ + ('product_id', '=', self.product3.id), + ('lot_id', '=', lot1.id), + ]) + from datetime import datetime, timedelta + initial_in_date_lot1 = datetime.now() - timedelta(days=5) + quant_lot1.in_date = initial_in_date_lot1 + + # Move one quant to pack location + move3 = self.env['stock.move'].create({ + 'name': 'test_in_date_1', + 'location_id': self.stock_location.id, + 'location_dest_id': self.pack_location.id, + 'product_id': self.product3.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + }) + move3.action_confirm() + move3.action_assign() + move3.move_line_ids.qty_done = 1 + move3.action_done() + + # Now, also move lot2 + self.env['stock.move.line'].create({ + 'move_id': move3.id, + 'product_id': move3.product_id.id, + 'qty_done': 1, + 'product_uom_id': move3.product_uom.id, + 'location_id': move3.location_id.id, + 'location_dest_id': move3.location_dest_id.id, + 'lot_id': lot2.id, + }) + + quants = self.env['stock.quant'].search([ + ('product_id', '=', self.product3.id), + ]) + self.assertEqual(len(quants), 2) + from odoo.fields import Datetime + for quant in quants: + if quant.lot_id == lot1: + self.assertEqual(quant.in_date, Datetime.to_string(initial_in_date_lot1)) + elif quant.lot_id == lot2: + self.assertEqual(quant.in_date, initial_in_date_lot2) diff --git a/addons/stock/tests/test_quant.py b/addons/stock/tests/test_quant.py index 2e046600d69..0dd7af02db7 100644 --- a/addons/stock/tests/test_quant.py +++ b/addons/stock/tests/test_quant.py @@ -5,6 +5,8 @@ from odoo.exceptions import ValidationError from odoo.tests.common import TransactionCase from odoo.exceptions import AccessError, UserError +from datetime import datetime, timedelta + class StockQuant(TransactionCase): def setUp(self): @@ -588,3 +590,131 @@ class StockQuant(TransactionCase): quant.sudo(self.demo_user).write({'quantity': 2.0}) with self.assertRaises(AccessError): quant.sudo(self.demo_user).unlink() + + def test_in_date_1(self): + """ Check that no incoming date is set when updating the quantity of an untracked quant. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + }) + quantity, in_date = self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0) + self.assertEqual(quantity, 1) + self.assertEqual(in_date, None) + + def test_in_date_2(self): + """ Check that an incoming date is correctly set when updating the quantity of a tracked + quant. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + 'tracking': 'serial', + }) + lot1 = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': product1.id, + }) + quantity, in_date = self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0, lot_id=lot1) + self.assertEqual(quantity, 1) + self.assertNotEqual(in_date, None) + + def test_in_date_3(self): + """ Check that the FIFO strategies correctly applies when you have multiple lot received + at different times for a tracked product. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + 'tracking': 'serial', + }) + lot1 = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': product1.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': product1.id, + }) + in_date_lot1 = datetime.now() + in_date_lot2 = datetime.now() - timedelta(days=5) + self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0, lot_id=lot1, in_date=in_date_lot1) + self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0, lot_id=lot2, in_date=in_date_lot2) + + quants = self.env['stock.quant']._update_reserved_quantity(product1, stock_location, 1) + + # Default removal strategy is FIFO, so lot2 should be received as it was received earlier. + self.assertEqual(quants[0][0].lot_id.id, lot2.id) + + def test_in_date_4(self): + """ Check that the LIFO strategies correctly applies when you have multiple lot received + at different times for a tracked product. + """ + stock_location = self.env.ref('stock.stock_location_stock') + lifo_strategy = self.env['product.removal'].search([('method', '=', 'lifo')]) + stock_location.removal_strategy_id = lifo_strategy + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + 'tracking': 'serial', + }) + lot1 = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': product1.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'lot2', + 'product_id': product1.id, + }) + in_date_lot1 = datetime.now() + in_date_lot2 = datetime.now() - timedelta(days=5) + self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0, lot_id=lot1, in_date=in_date_lot1) + self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0, lot_id=lot2, in_date=in_date_lot2) + + quants = self.env['stock.quant']._update_reserved_quantity(product1, stock_location, 1) + + # Removal strategy is LIFO, so lot1 should be received as it was received later. + self.assertEqual(quants[0][0].lot_id.id, lot1.id) + + def test_in_date_5(self): + """ Receive the same lot at different times, once they're in the same location, the quants + are merged and only the earliest incoming date is kept. + """ + stock_location = self.env.ref('stock.stock_location_stock') + product1 = self.env['product.product'].create({ + 'name': 'Product A', + 'type': 'product', + 'tracking': 'lot', + }) + lot1 = self.env['stock.production.lot'].create({ + 'name': 'lot1', + 'product_id': product1.id, + }) + + in_date1 = datetime.now() + self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0, lot_id=lot1, in_date=in_date1) + + quant = self.env['stock.quant'].search([ + ('product_id', '=', product1.id), + ('location_id', '=', stock_location.id), + ]) + self.assertEqual(len(quant), 1) + self.assertEqual(quant.quantity, 1) + self.assertEqual(quant.lot_id.id, lot1.id) + from odoo.fields import Datetime + self.assertEqual(quant.in_date, Datetime.to_string(in_date1)) + + in_date2 = datetime.now() - timedelta(days=5) + self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0, lot_id=lot1, in_date=in_date2) + + quant = self.env['stock.quant'].search([ + ('product_id', '=', product1.id), + ('location_id', '=', stock_location.id), + ]) + self.assertEqual(len(quant), 1) + self.assertEqual(quant.quantity, 2) + self.assertEqual(quant.lot_id.id, lot1.id) + self.assertEqual(quant.in_date, Datetime.to_string(in_date2))