19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_generate_serial_numbers
from . import test_stock_lot

View file

@ -0,0 +1,281 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from freezegun import freeze_time
from odoo import Command
from odoo.addons.stock.tests.test_generate_serial_numbers import StockGenerateCommon
from odoo.addons.stock.tests.test_picking_tours import TestStockPickingTour
from odoo.tools.misc import get_lang
class TestStockLot(StockGenerateCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.product_lot = cls.env['product.product'].create({
'name': 'Tracked by Lot Numbers',
'tracking': 'lot',
'is_storable': True,
'use_expiration_date': True,
})
def _import_lots(self, lots, move):
location_id = move.location_id
move_lines_vals = move.split_lots(lots)
move_lines_commands = move._generate_serial_move_line_commands(move_lines_vals, location_dest_id=location_id)
move.update({'move_line_ids': move_lines_commands})
def test_set_multiple_lot_name_with_expiration_date_01(self):
""" In a move line's `lot_name` field, pastes a list of lots and expiration dates.
Checks the values are correctly interpreted and the expiration dates are correctly created
depending of the user lang's date format.
"""
user_lang = self.env['res.lang'].browse([get_lang(self.env).id])
# Try first with the "day/month/year" date format.
user_lang.date_format = "%d/%m/%y"
list_lot_and_qty = [
{'lot_name': "ln01", "date": "03/05/25", "datetime": datetime.strptime('2025-05-03', "%Y-%m-%d")},
{'lot_name': "ln02", "date": "06/05/25", "datetime": datetime.strptime('2025-05-06', "%Y-%m-%d")},
{'lot_name': "ln03", "date": "03/06/25", "datetime": datetime.strptime('2025-06-03', "%Y-%m-%d")},
{'lot_name': "ln04", "date": "06/06/25", "datetime": datetime.strptime('2025-06-06', "%Y-%m-%d")},
{'lot_name': "ln05", "date": "03/07/25", "datetime": datetime.strptime('2025-07-03', "%Y-%m-%d")},
]
list_as_string = '\n'.join([f'{line["lot_name"]};{line["date"]}' for line in list_lot_and_qty])
move = self.get_new_move(product=self.product_lot)
self._import_lots(list_as_string, move)
self.assertEqual(len(move.move_line_ids), len(list_lot_and_qty))
for i, move_line in enumerate(move.move_line_ids):
self.assertEqual(move_line.lot_name, list_lot_and_qty[i]['lot_name'])
self.assertEqual(move_line.quantity, 1)
self.assertEqual(move_line.expiration_date, list_lot_and_qty[i]["datetime"])
# Same test but with with the "month/day/year" date format this time.
user_lang.date_format = "%m/%d/%y"
list_lot_and_qty = [
{'lot_name': "ln01", "date": "03/05/25", "datetime": datetime.strptime('2025-03-05', "%Y-%m-%d")},
{'lot_name': "ln02", "date": "06/05/25", "datetime": datetime.strptime('2025-06-05', "%Y-%m-%d")},
{'lot_name': "ln03", "date": "03/06/25", "datetime": datetime.strptime('2025-03-06', "%Y-%m-%d")},
{'lot_name': "ln04", "date": "06/06/25", "datetime": datetime.strptime('2025-06-06', "%Y-%m-%d")},
{'lot_name': "ln05", "date": "03/07/25", "datetime": datetime.strptime('2025-03-07', "%Y-%m-%d")},
]
list_as_string = '\n'.join([f'{line["lot_name"]};{line["date"]}' for line in list_lot_and_qty])
move = self.get_new_move(product=self.product_lot)
self._import_lots(list_as_string, move)
self.assertEqual(len(move.move_line_ids), len(list_lot_and_qty))
for i, move_line in enumerate(move.move_line_ids):
self.assertEqual(move_line.lot_name, list_lot_and_qty[i]['lot_name'])
self.assertEqual(move_line.quantity, 1)
self.assertEqual(move_line.expiration_date, list_lot_and_qty[i]["datetime"])
def test_set_multiple_lot_name_with_expiration_date_02_product_dont_use_expiration_date(self):
""" In a move line's `lot_name` field, pastes a list of lots and expiration dates.
Checks the values are correctly interpreted and since the product doesn't use expiration
date, the expiration dates should be ignored.
"""
self.product_lot.use_expiration_date = False
user_lang = self.env['res.lang'].browse([get_lang(self.env).id])
# Try first with the "day/month/year" date format.
user_lang.date_format = "%d/%m/%y"
list_lot_and_qty = [
{'lot_name': "ln01", "date": "03/05/25", "datetime": datetime.strptime('2025-05-03', "%Y-%m-%d")},
{'lot_name': "ln02", "date": "06/05/25", "datetime": datetime.strptime('2025-05-06', "%Y-%m-%d")},
{'lot_name': "ln03", "date": "03/06/25", "datetime": datetime.strptime('2025-06-03', "%Y-%m-%d")},
{'lot_name': "ln04", "date": "06/06/25", "datetime": datetime.strptime('2025-06-06', "%Y-%m-%d")},
{'lot_name': "ln05", "date": "03/07/25", "datetime": datetime.strptime('2025-07-03', "%Y-%m-%d")},
]
list_as_string = '\n'.join([f'{line["lot_name"]};{line["date"]}' for line in list_lot_and_qty])
move = self.get_new_move(product=self.product_lot)
self._import_lots(list_as_string, move)
self.assertEqual(len(move.move_line_ids), len(list_lot_and_qty))
for i, move_line in enumerate(move.move_line_ids):
self.assertEqual(move_line.lot_name, list_lot_and_qty[i]['lot_name'])
self.assertEqual(move_line.quantity, 1)
self.assertEqual(move_line.expiration_date, False)
def test_set_multiple_lot_name_with_expiration_date_03_adaptive_date_format(self):
""" Checks if the given dates don't follow the user lang's date format, the created expiration
date will follow the first given date's format (at least in the scope of the limitations).
"""
user_lang = self.env['res.lang'].browse([get_lang(self.env).id])
# Month first in the system but day in the first place in the given dates.
user_lang.date_format = "%m/%d/%y"
list_lot_and_qty = [
{'lot_name': "ln01", "date": "30/05/25", "datetime": datetime.strptime('2025-05-30', "%Y-%m-%d")},
{'lot_name': "ln02", "date": "06/05/25", "datetime": datetime.strptime('2025-05-06', "%Y-%m-%d")},
{'lot_name': "ln03", "date": "01/06/25", "datetime": datetime.strptime('2025-06-01', "%Y-%m-%d")},
{'lot_name': "ln04", "date": "06/06/25", "datetime": datetime.strptime('2025-06-06', "%Y-%m-%d")},
{'lot_name': "ln05", "date": "01/07/25", "datetime": datetime.strptime('2025-07-01', "%Y-%m-%d")},
]
list_as_string = '\n'.join([f'{line["lot_name"]};{line["date"]}' for line in list_lot_and_qty])
move = self.get_new_move(product=self.product_lot)
self._import_lots(list_as_string, move)
self.assertEqual(len(move.move_line_ids), len(list_lot_and_qty))
for i, move_line in enumerate(move.move_line_ids):
self.assertEqual(move_line.lot_name, list_lot_and_qty[i]['lot_name'])
self.assertEqual(move_line.quantity, 1)
self.assertEqual(move_line.expiration_date, list_lot_and_qty[i]["datetime"])
# Now, tries with day first but the year is at the first place in the given dates.
user_lang.date_format = "%d/%m/%y"
list_lot_and_qty = [
{'lot_name': "ln01", "date": "89/05/04", "datetime": datetime.strptime('1989-05-04', "%Y-%m-%d")},
{'lot_name': "ln02", "date": "10/05/06", "datetime": datetime.strptime('2010-05-06', "%Y-%m-%d")},
{'lot_name': "ln03", "date": "12/06/15", "datetime": datetime.strptime('2012-06-15', "%Y-%m-%d")},
{'lot_name': "ln04", "date": "30/06/06", "datetime": datetime.strptime('2030-06-06', "%Y-%m-%d")},
{'lot_name': "ln05", "date": "04/07/08", "datetime": datetime.strptime('2004-07-08', "%Y-%m-%d")},
]
list_as_string = '\n'.join([f'{line["lot_name"]};{line["date"]}' for line in list_lot_and_qty])
move = self.get_new_move(product=self.product_lot)
self._import_lots(list_as_string, move)
self.assertEqual(len(move.move_line_ids), len(list_lot_and_qty))
for i, move_line in enumerate(move.move_line_ids):
self.assertEqual(move_line.lot_name, list_lot_and_qty[i]['lot_name'])
self.assertEqual(move_line.quantity, 1)
self.assertEqual(move_line.expiration_date, list_lot_and_qty[i]["datetime"])
def test_set_multiple_lot_name_with_expiration_date_04_written_months(self):
""" Checks the expiration date is correctly created when the month is written in letters.
"""
list_lot_and_qty = [
{'lot_name': "ln01", "date": "01 march 2077", "datetime": datetime.strptime('2077-03-01', "%Y-%m-%d")},
{'lot_name': "ln02", "date": "11 april 2077", "datetime": datetime.strptime('2077-04-11', "%Y-%m-%d")},
{'lot_name': "ln03", "date": "10 december 2077", "datetime": datetime.strptime('2077-12-10', "%Y-%m-%d")},
]
list_as_string = '\n'.join([f'{line["lot_name"]};{line["date"]}' for line in list_lot_and_qty])
move = self.get_new_move(product=self.product_lot)
self._import_lots(list_as_string, move)
self.assertEqual(len(move.move_line_ids), len(list_lot_and_qty))
for i, move_line in enumerate(move.move_line_ids):
self.assertEqual(move_line.lot_name, list_lot_and_qty[i]['lot_name'])
self.assertEqual(move_line.quantity, 1)
self.assertEqual(move_line.expiration_date, list_lot_and_qty[i]["datetime"])
@freeze_time('2025-9-13')
def test_set_multiple_lot_name_with_expiration_date_05_import_dates(self):
""" When importing lot names, quantities and expiration dates, make sure the date is not
replaced by the computed date and if there's no date, it takes the computed date. """
self.product_lot.expiration_time = 10
receipt_picking = self.env['stock.picking'].create({
'picking_type_id': self.warehouse.in_type_id.id,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': self.warehouse.lot_stock_id.id,
'state': 'draft',
'move_ids': [Command.create({
'product_id': self.product_lot.id,
'product_uom_qty': 20,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': self.warehouse.lot_stock_id.id,
})]
})
action_context = {
'default_company_id': self.env.company.id,
'default_picking_id': receipt_picking.id,
'default_picking_type_id': self.warehouse.in_type_id.id,
'default_location_id': receipt_picking.location_id.id,
'default_location_dest_id': receipt_picking.location_dest_id.id,
'default_product_id': self.product_lot.id,
'default_tracking': 'lot',
}
move_line_vals = self.env['stock.move'].action_generate_lot_line_vals(
action_context, 'import', None, 0, 'lot1;10;2025-12-31\nlot2;7\nlot3;3;1970-1-1'
)
self.assert_move_line_vals_values(move_line_vals, [
{'quantity': 10, 'lot_name': 'lot1', 'expiration_date': datetime.strptime('2025-12-31', "%Y-%m-%d")},
{'quantity': 7, 'lot_name': 'lot2', 'expiration_date': datetime.strptime('2025-9-23', "%Y-%m-%d")},
{'quantity': 3, 'lot_name': 'lot3', 'expiration_date': datetime.strptime('1970-1-1', "%Y-%m-%d")},
])
@freeze_time('2023-04-17')
def test_set_multiple_lot_name_with_expiration_date_05_wrong_given_date(self):
""" This test ensure when the given dates aren't correctly written, the
full string is used as the lot's name.
"""
today = datetime(day=17, month=4, year=2023)
list_lot_and_qty = [
"ln01\t31/12", # Day is missing but the date is valid.
"ln02\t1989-04", # Day is missing but the date is valid.
"ln03\t01", # Single number: will be used as the quantity.
"ln04\t1989", # Single number: will be used as the quantity.
"ln05\t1989.04", # Signle number (with decimal): will be used as the quantity.
"ln06\t1989+04", # Wrong, all the string will be used as the lot name.
"ln07\tdacember", # Typo, all the string will be used as the lot name.
"ln08\tdecember", # Day and year are missing but the date is valid.
]
list_as_string = '\n'.join(list_lot_and_qty)
move = self.get_new_move(product=self.product_lot)
self._import_lots(list_as_string, move)
self.assertEqual(len(move.move_line_ids), len(list_lot_and_qty))
self.assertEqual(move.move_line_ids[0].lot_name, "ln01")
self.assertEqual(move.move_line_ids[0].quantity, 1)
self.assertEqual(move.move_line_ids[0].expiration_date, datetime(day=31, month=12, year=2023))
self.assertEqual(move.move_line_ids[1].lot_name, "ln02")
self.assertEqual(move.move_line_ids[1].quantity, 1)
self.assertEqual(move.move_line_ids[1].expiration_date, datetime(day=17, month=4, year=1989))
self.assertEqual(move.move_line_ids[2].lot_name, "ln03")
self.assertEqual(move.move_line_ids[2].quantity, 1)
self.assertEqual(move.move_line_ids[2].expiration_date, today)
self.assertEqual(move.move_line_ids[3].lot_name, "ln04")
self.assertEqual(move.move_line_ids[3].quantity, 1989)
self.assertEqual(move.move_line_ids[3].expiration_date, today)
self.assertEqual(move.move_line_ids[4].lot_name, "ln05")
self.assertEqual(move.move_line_ids[4].quantity, 1989.04)
self.assertEqual(move.move_line_ids[4].expiration_date, today)
self.assertEqual(move.move_line_ids[5].lot_name, "ln06\t1989+04")
self.assertEqual(move.move_line_ids[5].quantity, 1)
self.assertEqual(move.move_line_ids[5].expiration_date, today)
self.assertEqual(move.move_line_ids[6].lot_name, "ln07\tdacember")
self.assertEqual(move.move_line_ids[6].quantity, 1)
self.assertEqual(move.move_line_ids[6].expiration_date, today)
self.assertEqual(move.move_line_ids[7].lot_name, "ln08")
self.assertEqual(move.move_line_ids[7].quantity, 1)
self.assertEqual(move.move_line_ids[7].expiration_date, datetime(day=17, month=12, year=2023))
def test_set_multiple_lot_name_with_expiration_date_06_one_line(self):
""" Checks the pasted data are correctly parsed even if the user pastes only one line.
"""
user_lang = self.env['res.lang'].browse([get_lang(self.env).id])
user_lang.date_format = "%d/%m/%y"
for lot_name in ["lot-001;20;4 Aug 2048", "lot-001\t04/08/2048\t20"]:
move = self.get_new_move(product=self.product_lot)
self._import_lots(lot_name, move)
self.assertEqual(move.move_line_ids.lot_name, "lot-001")
self.assertEqual(move.move_line_ids.quantity, 20)
self.assertEqual(move.move_line_ids.expiration_date, datetime(day=4, month=8, year=2048))
class TestProductExpiryTour(TestStockPickingTour):
@freeze_time("2020-06-01")
def test_generate_serial_with_expiration(self):
"""
Ensure that serial/lot numbers generated using the 'Generate Serials/Lots' button in Detailed
Operations have expiration dates set.
"""
product_exp = self.env['product.product'].create({
'name': 'Product Exp',
'is_storable': True,
'tracking': 'serial',
'use_expiration_date': True,
'expiration_time': 2,
})
self.env['stock.move'].create({
'product_id': product_exp.id,
'product_uom_qty': 2,
'product_uom': product_exp.uom_id.id,
'picking_id': self.receipt.id,
})
self.receipt.action_confirm()
url = self._get_picking_url(self.receipt.id)
self.start_tour(url, 'test_generate_serial_with_expiration', login='admin')

View file

@ -4,10 +4,10 @@
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from odoo import fields
from odoo import fields, Command
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.stock.tests.common import TestStockCommon
from odoo.tests.common import Form
from odoo.tests import Form
class TestStockLot(TestStockCommon):
@ -18,7 +18,7 @@ class TestStockLot(TestStockCommon):
# Creates a tracked product with expiration dates.
cls.apple_product = cls.ProductObj.create({
'name': 'Apple',
'type': 'product',
'is_storable': True,
'tracking': 'lot',
'use_expiration_date': True,
'expiration_time': 10,
@ -33,7 +33,7 @@ class TestStockLot(TestStockCommon):
# create product
self.productAAA = self.ProductObj.create({
'name': 'Product AAA',
'type': 'product',
'is_storable': True,
'tracking':'lot',
'company_id': self.env.company.id,
})
@ -43,23 +43,22 @@ class TestStockLot(TestStockCommon):
'name': 'Lot 1 ProductAAA',
'product_id': self.productAAA.id,
'alert_date': fields.Date.to_string(datetime.today() - relativedelta(days=15)),
'company_id': self.env.company.id,
})
picking_in = self.PickingObj.create({
'picking_type_id': self.picking_type_in,
'location_id': self.supplier_location,
'location_dest_id': self.stock_location
'picking_type_id': self.picking_type_in.id,
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'state': 'draft',
})
move_a = self.MoveObj.create({
'name': self.productAAA.name,
'product_id': self.productAAA.id,
'product_uom_qty': 33,
'product_uom': self.productAAA.uom_id.id,
'picking_id': picking_in.id,
'location_id': self.supplier_location,
'location_dest_id': self.stock_location
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
})
self.assertEqual(picking_in.move_ids.state, 'draft', 'Wrong state of move line.')
@ -68,17 +67,18 @@ class TestStockLot(TestStockCommon):
# Replace pack operation of incoming shipments.
picking_in.action_assign()
move_a.move_line_ids.qty_done = 33
move_a.move_line_ids.quantity = 33
move_a.move_line_ids.lot_id = self.lot1_productAAA.id
# Transfer Incoming Shipment.
move_a.picked = True
picking_in._action_done()
# run scheduled tasks
self.env['stock.lot']._alert_date_exceeded()
# check a new activity has been created
activity_id = self.env.ref('product_expiry.mail_activity_type_alert_date_reached').id
activity_id = self.env.ref('mail.mail_activity_data_todo').id
activity_count = self.env['mail.activity'].search_count([
('activity_type_id', '=', activity_id),
('res_model_id', '=', self.env.ref('stock.model_stock_lot').id),
@ -130,7 +130,7 @@ class TestStockLot(TestStockCommon):
# create product
self.productBBB = self.ProductObj.create({
'name': 'Product BBB',
'type': 'product',
'is_storable': True,
'tracking':'lot'
})
@ -139,22 +139,23 @@ class TestStockLot(TestStockCommon):
'name': 'Lot 1 ProductBBB',
'product_id': self.productBBB.id,
'alert_date': fields.Date.to_string(datetime.today() + relativedelta(days=15)),
'company_id': self.env.company.id,
})
picking_in = self.PickingObj.create({
'picking_type_id': self.picking_type_in,
'location_id': self.supplier_location,
'location_dest_id': self.stock_location})
'picking_type_id': self.picking_type_in.id,
'location_id': self.supplier_location.id,
'state': 'draft',
'location_dest_id': self.stock_location.id,
})
move_b = self.MoveObj.create({
'name': self.productBBB.name,
'product_id': self.productBBB.id,
'product_uom_qty': 44,
'product_uom': self.productBBB.uom_id.id,
'picking_id': picking_in.id,
'location_id': self.supplier_location,
'location_dest_id': self.stock_location})
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
})
self.assertEqual(picking_in.move_ids.state, 'draft', 'Wrong state of move line.')
picking_in.action_confirm()
@ -162,7 +163,7 @@ class TestStockLot(TestStockCommon):
# Replace pack operation of incoming shipments.
picking_in.action_assign()
move_b.move_line_ids.qty_done = 44
move_b.move_line_ids.quantity = 44
move_b.move_line_ids.lot_id = self.lot1_productBBB.id
# Transfer Incoming Shipment.
@ -172,7 +173,7 @@ class TestStockLot(TestStockCommon):
self.env['stock.lot']._alert_date_exceeded()
# check a new activity has not been created
activity_id = self.env.ref('product_expiry.mail_activity_type_alert_date_reached').id
activity_id = self.env.ref('mail.mail_activity_data_todo').id
activity_count = self.env['mail.activity'].search_count([
('activity_type_id', '=', activity_id),
('res_model_id', '=', self.env.ref('stock.model_stock_lot').id),
@ -184,24 +185,26 @@ class TestStockLot(TestStockCommon):
""" Test Scheduled Task on lot without an alert_date does not create an activity """
# create product
self.productCCC = self.ProductObj.create({'name': 'Product CCC', 'type': 'product', 'tracking':'lot'})
self.productCCC = self.ProductObj.create({'name': 'Product CCC', 'is_storable': True, 'tracking': 'lot'})
# create a new lot with with alert date in the past
self.lot1_productCCC = self.LotObj.create({'name': 'Lot 1 ProductCCC', 'product_id': self.productCCC.id, 'company_id': self.env.company.id})
self.lot1_productCCC = self.LotObj.create({'name': 'Lot 1 ProductCCC', 'product_id': self.productCCC.id})
picking_in = self.PickingObj.create({
'picking_type_id': self.picking_type_in,
'location_id': self.supplier_location,
'location_dest_id': self.stock_location})
'picking_type_id': self.picking_type_in.id,
'location_id': self.supplier_location.id,
'state': 'draft',
'location_dest_id': self.stock_location.id,
})
move_c = self.MoveObj.create({
'name': self.productCCC.name,
'product_id': self.productCCC.id,
'product_uom_qty': 44,
'product_uom': self.productCCC.uom_id.id,
'picking_id': picking_in.id,
'location_id': self.supplier_location,
'location_dest_id': self.stock_location})
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
})
self.assertEqual(picking_in.move_ids.state, 'draft', 'Wrong state of move line.')
picking_in.action_confirm()
@ -209,7 +212,7 @@ class TestStockLot(TestStockCommon):
# Replace pack operation of incoming shipments.
picking_in.action_assign()
move_c.move_line_ids.qty_done = 55
move_c.move_line_ids.quantity = 55
move_c.move_line_ids.lot_id = self.lot1_productCCC.id
# Transfer Incoming Shipment.
@ -219,7 +222,7 @@ class TestStockLot(TestStockCommon):
self.env['stock.lot']._alert_date_exceeded()
# check a new activity has not been created
activity_id = self.env.ref('product_expiry.mail_activity_type_alert_date_reached').id
activity_id = self.env.ref('mail.mail_activity_data_todo').id
activity_count = self.env['mail.activity'].search_count([
('activity_type_id', '=', activity_id),
('res_model_id', '=', self.env.ref('stock.model_stock_lot').id),
@ -251,7 +254,6 @@ class TestStockLot(TestStockCommon):
lot_form = Form(self.LotObj)
lot_form.name = 'Apple Box #1'
lot_form.product_id = self.apple_product
lot_form.company_id = self.env.company
apple_lot = lot_form.save()
# ...then checks date fields have the expected values.
check_expiration_dates(self.apple_product, apple_lot, today_date, time_gap)
@ -298,21 +300,21 @@ class TestStockLot(TestStockCommon):
# Receives a tracked production using expiration date.
picking_form = Form(self.env['stock.picking'])
picking_form.partner_id = partner
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
with picking_form.move_ids_without_package.new() as move:
picking_form.picking_type_id = self.picking_type_in
with picking_form.move_ids.new() as move:
move.product_id = self.apple_product
move.product_uom_qty = 4
receipt = picking_form.save()
receipt.action_confirm()
# Defines a date during the receipt.
move_form = Form(receipt.move_ids_without_package, view="stock.view_stock_move_operations")
with move_form.move_line_ids.new() as line:
move_form = Form(receipt.move_ids, view="stock.view_stock_move_operations")
with move_form.move_line_ids.edit(0) as line:
line.lot_name = 'Apple Box #2'
line.expiration_date = expiration_date
line.qty_done = 4
move = move_form.save()
move.picked = True
receipt._action_done()
# Get back the lot created when the picking was done...
apple_lot = self.env['stock.lot'].search(
@ -346,20 +348,19 @@ class TestStockLot(TestStockCommon):
# Receives a tracked production using expiration date.
picking_form = Form(self.env['stock.picking'])
picking_form.partner_id = partner
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
with picking_form.move_ids_without_package.new() as move:
picking_form.picking_type_id = self.picking_type_in
with picking_form.move_ids.new() as move:
move.product_id = self.apple_product
move.product_uom_qty = 4
move.quantity = 4
move.picked = True
receipt = picking_form.save()
receipt.action_confirm()
# Defines a date during the receipt.
move = receipt.move_ids_without_package[0]
move = receipt.move_ids[0]
line = move.move_line_ids[0]
self.assertEqual(move.use_expiration_date, True)
line.lot_name = 'Apple Box #3'
line.expiration_date = expiration_date
line.qty_done = 4
receipt._action_done()
# Get back the lot created when the picking was done...
@ -390,13 +391,11 @@ class TestStockLot(TestStockCommon):
lot_form = Form(self.LotObj) # Creates the lot.
lot_form.name = 'good-apple-lot'
lot_form.product_id = self.apple_product
lot_form.company_id = self.env.company
good_lot = lot_form.save()
lot_form = Form(self.LotObj) # Creates the lot.
lot_form.name = 'expired-apple-lot-01'
lot_form.product_id = self.apple_product
lot_form.company_id = self.env.company
expired_lot_1 = lot_form.save()
lot_form = Form(expired_lot_1) # Edits the lot to make it expired.
lot_form.expiration_date = datetime.today() - timedelta(days=10)
@ -405,23 +404,24 @@ class TestStockLot(TestStockCommon):
# Case #1: make a delivery with no expired lot.
picking_form = Form(self.env['stock.picking'])
picking_form.partner_id = partner
picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
with picking_form.move_ids_without_package.new() as move:
picking_form.picking_type_id = self.picking_type_out
with picking_form.move_ids.new() as move:
move.product_id = self.apple_product
move.product_uom_qty = 4
# Saves and confirms it...
delivery_1 = picking_form.save()
delivery_1.action_confirm()
# ... then create a move line with the non-expired lot and valids the picking.
delivery_1.move_line_ids_without_package = [(5, 0), (0, 0, {
delivery_1.move_line_ids = [(5, 0), (0, 0, {
'company_id': self.env.company.id,
'location_id': delivery_1.move_ids.location_id.id,
'location_dest_id': delivery_1.move_ids.location_dest_id.id,
'lot_id': good_lot.id,
'product_id': self.apple_product.id,
'product_uom_id': self.apple_product.uom_id.id,
'qty_done': 4,
'quantity': 4,
})]
delivery_1.move_ids.picked = True
res = delivery_1.button_validate()
# Validate a delivery for good products must not raise anything.
self.assertEqual(res, True)
@ -429,8 +429,8 @@ class TestStockLot(TestStockCommon):
# Case #2: make a delivery with one non-expired lot and one expired lot.
picking_form = Form(self.env['stock.picking'])
picking_form.partner_id = partner
picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
with picking_form.move_ids_without_package.new() as move:
picking_form.picking_type_id = self.picking_type_out
with picking_form.move_ids.new() as move:
move.product_id = self.apple_product
move.product_uom_qty = 8
# Saves and confirms it...
@ -438,14 +438,14 @@ class TestStockLot(TestStockCommon):
delivery_2.action_confirm()
# ... then create a move line for the non-expired lot and for an expired
# lot and valids the picking.
delivery_2.move_line_ids_without_package = [(5, 0), (0, 0, {
delivery_2.move_line_ids = [(5, 0), (0, 0, {
'company_id': self.env.company.id,
'location_id': delivery_2.move_ids.location_id.id,
'location_dest_id': delivery_2.move_ids.location_dest_id.id,
'lot_id': good_lot.id,
'product_id': self.apple_product.id,
'product_uom_id': self.apple_product.uom_id.id,
'qty_done': 4,
'quantity': 4,
}), (0, 0, {
'company_id': self.env.company.id,
'location_id': delivery_2.move_ids.location_id.id,
@ -453,8 +453,9 @@ class TestStockLot(TestStockCommon):
'lot_id': expired_lot_1.id,
'product_id': self.apple_product.id,
'product_uom_id': self.apple_product.uom_id.id,
'qty_done': 4,
'quantity': 4,
})]
delivery_2.move_ids.picked = True
res = delivery_2.button_validate()
# Validate a delivery containing expired products must raise a confirmation wizard.
self.assertNotEqual(res, True)
@ -463,23 +464,24 @@ class TestStockLot(TestStockCommon):
# Case #3: make a delivery with only on expired lot.
picking_form = Form(self.env['stock.picking'])
picking_form.partner_id = partner
picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
with picking_form.move_ids_without_package.new() as move:
picking_form.picking_type_id = self.picking_type_out
with picking_form.move_ids.new() as move:
move.product_id = self.apple_product
move.product_uom_qty = 4
# Saves and confirms it...
delivery_3 = picking_form.save()
delivery_3.action_confirm()
# ... then create two move lines with expired lot and valids the picking.
delivery_3.move_line_ids_without_package = [(5, 0), (0, 0, {
delivery_3.move_line_ids = [(5, 0), (0, 0, {
'company_id': self.env.company.id,
'location_id': delivery_3.move_ids.location_id.id,
'location_dest_id': delivery_3.move_ids.location_dest_id.id,
'lot_id': expired_lot_1.id,
'product_id': self.apple_product.id,
'product_uom_id': self.apple_product.uom_id.id,
'qty_done': 4,
'quantity': 4,
})]
delivery_3.move_ids.picked = True
res = delivery_3.button_validate()
# Validate a delivery containing expired products must raise a confirmation wizard.
self.assertNotEqual(res, True)
@ -499,12 +501,11 @@ class TestStockLot(TestStockCommon):
lot_form = Form(self.LotObj)
lot_form.name = 'LOT001'
lot_form.product_id = self.apple_product
lot_form.company_id = self.env.company
apple_lot = lot_form.save()
quant = self.StockQuantObj.with_context(inventory_mode=True).create({
'product_id': self.apple_product.id,
'location_id': self.stock_location,
'location_id': self.stock_location.id,
'quantity': 10,
'lot_id': apple_lot.id,
})
@ -519,66 +520,35 @@ class TestStockLot(TestStockCommon):
the latter should be applied on the SML
"""
exp_date = fields.Datetime.today() + relativedelta(days=15)
sml_exp_date = fields.Datetime.today() + relativedelta(days=10)
lot = self.env['stock.lot'].create({
'name': 'Lot 1',
'product_id': self.apple_product.id,
'expiration_date': fields.Datetime.to_string(exp_date),
'company_id': self.env.company.id,
})
move = self.env['stock.move'].create({
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.apple_product.id,
'product_uom': self.apple_product.uom_id.id,
})
sml = self.env['stock.move.line'].create({
'location_id': self.supplier_location,
'location_dest_id': self.stock_location,
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.apple_product.id,
'qty_done': 3,
'quantity': 3,
'product_uom_id': self.apple_product.uom_id.id,
'lot_id': lot.id,
'expiration_date': fields.Datetime.to_string(sml_exp_date),
'company_id': self.env.company.id,
'move_id': move.id,
})
self.assertEqual(sml.expiration_date, sml_exp_date)
sml.lot_id = lot
self.assertEqual(sml.expiration_date, exp_date)
exp_date = exp_date + relativedelta(days=10)
lot.expiration_date = exp_date
self.assertEqual(sml.expiration_date, exp_date)
def test_apply_lot_without_date_on_sml(self):
"""
When assigning a lot to a SML, if the lot has no expiration date,
dates on lot and SML should be correctly set
"""
#create lot without expiration date
lot = self.env['stock.lot'].create({
'name': 'Lot 1',
'product_id': self.apple_product.id,
'company_id': self.env.company.id,
})
sml = self.env['stock.move.line'].create({
'location_id': self.supplier_location,
'location_dest_id': self.stock_location,
'product_id': self.apple_product.id,
'qty_done': 3,
'product_uom_id': self.apple_product.uom_id.id,
'lot_id': lot.id,
'company_id': self.env.company.id,
})
today_date = datetime.today()
time_gap = timedelta(seconds=10)
exp_date = today_date + timedelta(days=self.apple_product.expiration_time)
self.assertAlmostEqual(sml.expiration_date, exp_date, delta=time_gap)
self.assertAlmostEqual(
lot.expiration_date, exp_date, delta=time_gap)
self.assertAlmostEqual(
lot.use_date, exp_date - timedelta(days=self.apple_product.use_time), delta=time_gap)
self.assertAlmostEqual(
lot.removal_date, exp_date - timedelta(days=self.apple_product.removal_time), delta=time_gap)
self.assertAlmostEqual(
lot.alert_date, exp_date - timedelta(days=self.apple_product.alert_time), delta=time_gap)
def test_apply_same_date_on_expiry_fields(self):
expiration_time = 10
self.apple_product.write({
@ -590,7 +560,6 @@ class TestStockLot(TestStockCommon):
lot = self.env['stock.lot'].create({
'product_id': self.apple_product.id,
'company_id': self.env.company.id,
})
delta = timedelta(seconds=10)
@ -601,6 +570,88 @@ class TestStockLot(TestStockCommon):
self.assertAlmostEqual(lot.removal_date, expiration_date, delta=delta, msg=err_msg)
self.assertAlmostEqual(lot.alert_date, expiration_date, delta=delta, msg=err_msg)
def test_no_expiration_date(self):
"""
When use_expiration_date is set to True on the Product, but the lot have an expiration_date set to False,
the picking should be able to reserve on it because it is considered as 'non-perishable'
"""
lot_form = Form(self.LotObj)
lot_form.name = 'LOT001'
lot_form.product_id = self.apple_product
apple_lot = lot_form.save()
lot_form = Form(apple_lot)
lot_form.expiration_date = False
lot_form.use_date = False
lot_form.removal_date = False
lot_form.alert_date = False
apple_lot = lot_form.save()
self.StockQuantObj.with_context(inventory_mode=True).create({
'product_id': self.apple_product.id,
'location_id': self.stock_location.id,
'quantity': 100,
'lot_id': apple_lot.id,
})
self.assertEqual(self.apple_product.qty_available, 100, 'Wrong quantity.')
picking_out = self.PickingObj.create({
'picking_type_id': self.picking_type_out.id,
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'state': 'draft',
})
self.MoveObj.create({
'product_id': self.apple_product.id,
'product_uom_qty': 10,
'product_uom': self.apple_product.uom_id.id,
'picking_id': picking_out.id,
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
})
self.assertEqual(picking_out.move_ids.state, 'draft', 'Wrong state of move line.')
picking_out.action_confirm()
picking_out.action_assign()
self.assertEqual(picking_out.move_ids.state, 'assigned', 'Wrong state of move line.')
def test_no_lot(self):
"""
Try to reserve a move that for an expirable product that has both quants with and without lot attached.
"""
# Set the removal strategy to 'First Expiry First Out'
fefo_strategy = self.env['product.removal'].search(
[('method', '=', 'fefo')])
self.apple_product.categ_id.removal_strategy_id = fefo_strategy.id
apple_lot = self.LotObj.create({
'name': 'LOT001',
'product_id': self.apple_product.id,
})
self.StockQuantObj.with_context(inventory_mode=True).create([{
'product_id': self.apple_product.id,
'location_id': self.stock_location.id,
'quantity': 100,
}, {
'product_id': self.apple_product.id,
'location_id': self.stock_location.id,
'quantity': 100,
'lot_id': apple_lot.id,
}])
with Form(self.PickingObj) as picking_form:
picking_form.picking_type_id = self.picking_type_out
with picking_form.move_ids.new() as move:
move.product_id = self.apple_product
move.product_uom_qty = 10
picking_out = picking_form.save()
picking_out.action_assign()
self.assertEqual(picking_out.move_line_ids.lot_id, apple_lot)
def test_compute_expiration_date_from_scheduled_date(self):
partner = self.env['res.partner'].create({
'name': 'Apple\'s Joe',
@ -614,11 +665,98 @@ class TestStockLot(TestStockCommon):
picking_form = Form(self.env['stock.picking'])
picking_form.partner_id = partner
picking_form.scheduled_date = new_date
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.picking_type_id = self.picking_type_in
with picking_form.move_ids_without_package.new() as move:
with picking_form.move_ids.new() as move:
move.product_id = self.apple_product
move.product_uom_qty = 4
delivery = picking_form.save()
delivery.action_confirm()
self.assertAlmostEqual(delivery.move_line_ids[0].expiration_date, expiration_date, delta=delta)
def test_compute_display_name(self):
apple_lot1 = self.LotObj.create({
'name': 'LOT-00001',
'product_id': self.apple_product.id,
'expiration_date': False,
'alert_date': False,
})
apple_lot2 = self.LotObj.create({
'name': 'LOT-00002',
'product_id': self.apple_product.id,
'expiration_date': datetime.today() - timedelta(days=10),
})
apple_lot3 = self.LotObj.create({
'name': 'LOT-00003',
'product_id': self.apple_product.id,
'alert_date': datetime.today() - timedelta(days=10),
})
self.assertEqual(apple_lot1.with_context(formatted_display_name=True).display_name, "LOT-00001")
self.assertEqual(apple_lot2.with_context(formatted_display_name=True).display_name, "LOT-00002\t--Expired--")
self.assertEqual(apple_lot3.with_context(formatted_display_name=True).display_name, "LOT-00003\t--Expire on " + fields.Datetime.to_string(apple_lot3.expiration_date) + "--")
def test_proceed_except_expired_delivery_without_move_removal_date(self):
lot = self.LotObj.create({
'name': 'LOT-001',
'product_id': self.apple_product.id,
})
lot.removal_date = False
self.StockQuantObj.with_context(inventory_mode=True).create({
'product_id': self.apple_product.id,
'location_id': self.stock_location.id,
'quantity': 100,
'lot_id': lot.id,
})
picking = self.PickingObj.create({
'partner_id': self.partner_1.id,
'picking_type_id': self.picking_type_out.id,
'move_ids': [Command.create({
'product_id': self.apple_product.id,
'product_uom_qty': 2,
})],
})
picking.button_validate()
context = {
'button_validate_picking_ids': [picking.id],
'default_picking_ids': [picking.id],
'default_lot_ids': [lot.id],
}
wizard = self.env['expiry.picking.confirmation'].with_context(context).create({})
self.assertFalse(wizard.picking_ids.move_line_ids.removal_date)
wizard.process_no_expired()
def test_lot_dates_form_update(self):
"""
Ensure that we can edit the removal_date and expiration_date fields at the same time
Without triggering the compute method for the expiration when saving modifications.
"""
delta = timedelta(seconds=10)
today = datetime.today()
receipt = self.env['stock.picking'].create({
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'picking_type_id': self.picking_type_in.id,
'scheduled_date': today,
'move_ids': [
Command.create({
'product_id': self.apple_product.id,
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_uom_qty': 1,
}),
],
})
receipt.action_confirm()
self.assertAlmostEqual(receipt.move_line_ids.expiration_date, today + timedelta(days=10), delta=delta)
self.assertAlmostEqual(receipt.move_line_ids.removal_date, today + timedelta(days=8), delta=delta)
with Form(receipt.move_ids, view="stock.view_stock_move_operations") as move_form:
with move_form.move_line_ids.edit(0) as line_form:
line_form.lot_name = 'lot 1'
line_form.expiration_date = today + timedelta(days=15)
line_form.removal_date = today + timedelta(days=10)
self.assertAlmostEqual(receipt.move_line_ids.expiration_date, today + timedelta(days=15), delta=delta)
self.assertAlmostEqual(receipt.move_line_ids.removal_date, today + timedelta(days=10), delta=delta)