[REF] stock: quant: use in_date field
Set the `in_date` field to now when creating a tracked quant.
Set the correct `in_date` field when moving a quant (this is done by
adding a kw argument to `_update_available_quantity` and adapting the
code that moves quant in stock.move.line).
If quants from the same lot go together in the same location, we merge
them and keep the older date (as the user will have no way to tell the
difference anyway).
We add multiple test cases:
- `in_date` are only set on tracked quants
- FIFO and LIFO are correctly applied
- keep the oldest `in_date` when merging quants
- editing a done move line or creating one in a done picking is
correctly working
Joint work with sle-odoo.
This commit is contained in:
committed by
Simon Lejeune
parent
3d4238f71e
commit
0fbe90b227
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user