Odoo18-Base/addons/product_expiry/tests/test_stock_lot.py
2025-01-06 10:57:38 +07:00

678 lines
29 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from odoo import fields
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.stock.tests.common import TestStockCommon
from odoo.tests import Form
class TestStockLot(TestStockCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Creates a tracked product with expiration dates.
cls.apple_product = cls.ProductObj.create({
'name': 'Apple',
'is_storable': True,
'tracking': 'lot',
'use_expiration_date': True,
'expiration_time': 10,
'use_time': 5,
'removal_time': 2,
'alert_time': 6,
})
def test_00_stock_production_lot(self):
""" Test Scheduled Task on lot with an alert_date in the past creates an activity """
# create product
self.productAAA = self.ProductObj.create({
'name': 'Product AAA',
'is_storable': True,
'tracking':'lot',
'company_id': self.env.company.id,
})
# create a new lot with with alert date in the past
self.lot1_productAAA = self.LotObj.create({
'name': 'Lot 1 ProductAAA',
'product_id': self.productAAA.id,
'alert_date': fields.Date.to_string(datetime.today() - relativedelta(days=15)),
})
picking_in = self.PickingObj.create({
'picking_type_id': self.picking_type_in,
'location_id': self.supplier_location,
'location_dest_id': self.stock_location,
'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
})
self.assertEqual(picking_in.move_ids.state, 'draft', 'Wrong state of move line.')
picking_in.action_confirm()
self.assertEqual(picking_in.move_ids.state, 'assigned', 'Wrong state of move line.')
# Replace pack operation of incoming shipments.
picking_in.action_assign()
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_count = self.env['mail.activity'].search_count([
('activity_type_id', '=', activity_id),
('res_model_id', '=', self.env.ref('stock.model_stock_lot').id),
('res_id', '=', self.lot1_productAAA.id)
])
self.assertEqual(activity_count, 1, 'No activity created while there should be one')
# run the scheduler a second time
self.env['stock.lot']._alert_date_exceeded()
# check there is still only one activity, no additional activity is created if there is already an existing activity
activity_count = self.env['mail.activity'].search_count([
('activity_type_id', '=', activity_id),
('res_model_id', '=', self.env.ref('stock.model_stock_lot').id),
('res_id', '=', self.lot1_productAAA.id)
])
self.assertEqual(activity_count, 1, 'There should be one and only one activity')
# mark the activity as done
mail_activity = self.env['mail.activity'].search([
('activity_type_id', '=', activity_id),
('res_model_id', '=', self.env.ref('stock.model_stock_lot').id),
('res_id', '=', self.lot1_productAAA.id)
])
mail_activity.action_done()
# check there is no more activity (because it is already done)
activity_count = self.env['mail.activity'].search_count([
('activity_type_id', '=', activity_id),
('res_model_id', '=', self.env.ref('stock.model_stock_lot').id),
('res_id', '=', self.lot1_productAAA.id)
])
self.assertEqual(activity_count, 0,"As activity is done, there shouldn't be any related activity")
# run the scheduler a third time
self.env['stock.lot']._alert_date_exceeded()
# check there is no activity created
activity_count = self.env['mail.activity'].search_count([
('activity_type_id', '=', activity_id),
('res_model_id', '=', self.env.ref('stock.model_stock_lot').id),
('res_id', '=',self.lot1_productAAA.id)
])
self.assertEqual(activity_count, 0, "As there is already an activity marked as done, there shouldn't be any related activity created for this lot")
def test_01_stock_production_lot(self):
""" Test Scheduled Task on lot with an alert_date in future does not create an activity """
# create product
self.productBBB = self.ProductObj.create({
'name': 'Product BBB',
'is_storable': True,
'tracking':'lot'
})
# create a new lot with with alert date in the past
self.lot1_productBBB = self.LotObj.create({
'name': 'Lot 1 ProductBBB',
'product_id': self.productBBB.id,
'alert_date': fields.Date.to_string(datetime.today() + relativedelta(days=15)),
})
picking_in = self.PickingObj.create({
'picking_type_id': self.picking_type_in,
'location_id': self.supplier_location,
'state': 'draft',
'location_dest_id': self.stock_location})
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})
self.assertEqual(picking_in.move_ids.state, 'draft', 'Wrong state of move line.')
picking_in.action_confirm()
self.assertEqual(picking_in.move_ids.state, 'assigned', 'Wrong state of move line.')
# Replace pack operation of incoming shipments.
picking_in.action_assign()
move_b.move_line_ids.quantity = 44
move_b.move_line_ids.lot_id = self.lot1_productBBB.id
# Transfer Incoming Shipment.
picking_in._action_done()
# run scheduled tasks
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_count = self.env['mail.activity'].search_count([
('activity_type_id', '=', activity_id),
('res_model_id', '=', self.env.ref('stock.model_stock_lot').id),
('res_id', '=', self.lot1_productBBB.id)
])
self.assertEqual(activity_count, 0, "An activity has been created while it shouldn't")
def test_02_stock_production_lot(self):
""" Test Scheduled Task on lot without an alert_date does not create an activity """
# create product
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})
picking_in = self.PickingObj.create({
'picking_type_id': self.picking_type_in,
'location_id': self.supplier_location,
'state': 'draft',
'location_dest_id': self.stock_location})
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})
self.assertEqual(picking_in.move_ids.state, 'draft', 'Wrong state of move line.')
picking_in.action_confirm()
self.assertEqual(picking_in.move_ids.state, 'assigned', 'Wrong state of move line.')
# Replace pack operation of incoming shipments.
picking_in.action_assign()
move_c.move_line_ids.quantity = 55
move_c.move_line_ids.lot_id = self.lot1_productCCC.id
# Transfer Incoming Shipment.
picking_in._action_done()
# run scheduled tasks
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_count = self.env['mail.activity'].search_count([
('activity_type_id', '=', activity_id),
('res_model_id', '=', self.env.ref('stock.model_stock_lot').id),
('res_id', '=', self.lot1_productCCC.id)
])
self.assertEqual(activity_count, 0, "An activity has been created while it shouldn't")
def test_03_onchange_expiration_date(self):
""" Updates the `expiration_date` of the lot production and checks other date
fields are updated as well. """
def check_expiration_dates(product, lot, start_date, delta):
self.assertAlmostEqual(
start_date + timedelta(days=product.expiration_time),
lot.expiration_date, delta=delta)
self.assertAlmostEqual(
lot.expiration_date - timedelta(days=product.use_time),
lot.use_date, delta=delta)
self.assertAlmostEqual(
lot.expiration_date - timedelta(days=product.removal_time),
lot.removal_date, delta=delta)
self.assertAlmostEqual(
lot.expiration_date - timedelta(days=product.alert_time),
lot.alert_date, delta=delta)
# Keeps track of the current datetime and set a delta for the compares.
today_date = datetime.today()
time_gap = timedelta(seconds=10)
# Creates a new lot number and saves it...
lot_form = Form(self.LotObj)
lot_form.name = 'Apple Box #1'
lot_form.product_id = self.apple_product
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)
difference = timedelta(days=20)
new_expiration_date = apple_lot.expiration_date + difference
new_start_date = new_expiration_date - timedelta(days=self.apple_product.expiration_time)
random_date = new_expiration_date + difference
# Modifies the lot `expiration_date` several times, without saving...
lot_form = Form(apple_lot)
lot_form.expiration_date = new_expiration_date
lot_form.expiration_date = random_date
lot_form.expiration_date = new_expiration_date
apple_lot = lot_form.save()
# ...then checks all other date fields were correctly updated.
check_expiration_dates(self.apple_product, apple_lot, new_start_date, time_gap)
# Remove all dates, save, update expiration date twice, then save again
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
lot_form.save()
lot_form.expiration_date = random_date
lot_form.expiration_date = new_expiration_date
apple_lot = lot_form.save()
# ...then check all other date fields were correctly updated.
check_expiration_dates(self.apple_product, apple_lot, new_start_date, time_gap)
def test_04_expiration_date_on_receipt(self):
""" Test we can set an expiration date on receipt and all expiration
date will be correctly set. """
partner = self.env['res.partner'].create({
'name': 'Apple\'s Joe',
'company_id': self.env.ref('base.main_company').id,
})
expiration_date = datetime.today() + timedelta(days=30)
time_gap = timedelta(seconds=10)
# 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:
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.edit(0) as line:
line.lot_name = 'Apple Box #2'
line.expiration_date = expiration_date
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(
[('product_id', '=', self.apple_product.id)],
limit=1,
)
# ... and checks all date fields are correctly set.
self.assertAlmostEqual(
apple_lot.expiration_date, expiration_date, delta=time_gap)
self.assertAlmostEqual(
apple_lot.use_date, expiration_date - timedelta(days=self.apple_product.use_time), delta=time_gap)
self.assertAlmostEqual(
apple_lot.removal_date, expiration_date - timedelta(days=self.apple_product.removal_time), delta=time_gap)
self.assertAlmostEqual(
apple_lot.alert_date, expiration_date - timedelta(days=self.apple_product.alert_time), delta=time_gap)
def test_04_2_expiration_date_on_receipt(self):
""" Test we can set an expiration date on receipt even if all expiration
date related fields aren't set on product. """
partner = self.env['res.partner'].create({
'name': 'Apple\'s Joe',
'company_id': self.env.ref('base.main_company').id,
})
# Unset some fields.
self.apple_product.expiration_time = False
self.apple_product.removal_time = False
expiration_date = datetime.today() + timedelta(days=30)
time_gap = timedelta(seconds=10)
# 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:
move.product_id = self.apple_product
move.quantity = 4
move.picked = True
receipt = picking_form.save()
# Defines a date during the receipt.
move = receipt.move_ids_without_package[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
receipt._action_done()
# Get back the lot created when the picking was done...
apple_lot = self.env['stock.lot'].search(
[('product_id', '=', self.apple_product.id)],
limit=1,
)
# ... and checks all date fields are correctly set.
self.assertAlmostEqual(
apple_lot.expiration_date, expiration_date, delta=time_gap,
msg="Must be define even if the product's `expiration_time` isn't set.")
self.assertAlmostEqual(
apple_lot.use_date, expiration_date - timedelta(days=self.apple_product.use_time), delta=time_gap)
self.assertEqual(
apple_lot.removal_date, expiration_date,
"Must same as expiration_date as the `removal_time` isn't set on product.")
self.assertAlmostEqual(
apple_lot.alert_date, expiration_date - timedelta(days=self.apple_product.alert_time), delta=time_gap)
def test_05_confirmation_on_delivery(self):
""" Test when user tries to delivery expired lot, he/she gets a
confirmation wizard. """
partner = self.env['res.partner'].create({
'name': 'Cider & Son',
'company_id': self.env.ref('base.main_company').id,
})
# Creates 3 lots (1 non-expired lot, 2 expired lots)
lot_form = Form(self.LotObj) # Creates the lot.
lot_form.name = 'good-apple-lot'
lot_form.product_id = self.apple_product
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
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)
expired_lot_1 = lot_form.save()
# 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:
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, {
'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,
'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)
# 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:
move.product_id = self.apple_product
move.product_uom_qty = 8
# Saves and confirms it...
delivery_2 = picking_form.save()
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, {
'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,
'quantity': 4,
}), (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': expired_lot_1.id,
'product_id': self.apple_product.id,
'product_uom_id': self.apple_product.uom_id.id,
'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)
self.assertEqual(res['res_model'], 'expiry.picking.confirmation')
# 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:
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, {
'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,
'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)
self.assertEqual(res['res_model'], 'expiry.picking.confirmation')
def test_edit_removal_date_in_inventory_mode(self):
""" Try to edit removal_date with the inventory mode.
"""
user_group_stock_manager = self.env.ref('stock.group_stock_manager')
self.demo_user = mail_new_test_user(
self.env,
name='Demo user',
login='userdemo',
email='d.d@example.com',
groups='stock.group_stock_manager',
)
lot_form = Form(self.LotObj)
lot_form.name = 'LOT001'
lot_form.product_id = self.apple_product
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,
'quantity': 10,
'lot_id': apple_lot.id,
})
# Try to write on quant with inventory mode
new_date = datetime.today() + timedelta(days=15)
quant.with_user(self.demo_user).with_context(inventory_mode=True).write({'removal_date': new_date})
self.assertEqual(quant.removal_date, new_date)
def test_apply_lot_date_on_sml(self):
"""
When assigning a lot to a SML, if the lot has an expiration date,
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),
})
move = self.env['stock.move'].create({
'name': 'move_test',
'location_id': self.supplier_location,
'location_dest_id': self.stock_location,
'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,
'product_id': self.apple_product.id,
'quantity': 3,
'product_uom_id': self.apple_product.uom_id.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)
def test_apply_same_date_on_expiry_fields(self):
expiration_time = 10
self.apple_product.write({
'expiration_time': expiration_time,
'use_time': 0,
'removal_time': 0,
'alert_time': 0,
})
lot = self.env['stock.lot'].create({
'product_id': self.apple_product.id,
})
delta = timedelta(seconds=10)
expiration_date = datetime.today() + timedelta(days=expiration_time)
err_msg = "The time on the product is set to 0, it means that the corresponding date should be the same as the expiration one"
self.assertAlmostEqual(lot.expiration_date, expiration_date, delta=delta)
self.assertAlmostEqual(lot.use_date, expiration_date, delta=delta, msg=err_msg)
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,
'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,
'location_id': self.stock_location,
'location_dest_id': self.customer_location,
'state': 'draft',
})
self.MoveObj.create({
'name': self.apple_product.name,
'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,
'location_dest_id': self.customer_location
})
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,
'quantity': 100,
}, {
'product_id': self.apple_product.id,
'location_id': self.stock_location,
'quantity': 100,
'lot_id': apple_lot.id,
}])
with Form(self.PickingObj) as picking_form:
picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
with picking_form.move_ids_without_package.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',
'company_id': self.env.ref('base.main_company').id,
})
delta = timedelta(seconds=10)
new_date = datetime.today() + timedelta(days=42)
expiration_date = new_date + timedelta(days=self.apple_product.expiration_time)
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')
with picking_form.move_ids_without_package.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)